简单实现一下Flutter的Stepper做一个侧边进度条

因为 flutter 提供的 Stepper 无法满足业务需求,于是只好自己实现一个了

flutter Stepper 的样式

原生 Stepper

我实现的 Stepper

我实现的 Stepper

这个或许根本不叫 Stepper 吧,也没有什么步骤,只是当前的配送进度,不需要数字步骤,希望所有内容都能显示出来,原生的则是有数字表示第几步,把当前步骤外的其他的内容都隐藏了。

那么开始进行分析,整个需求中,有点难度的也就是这个左边的进度线了。我们把进度看做一个 ListView ,每条进度都是一个 Item

item

先来看怎么布局这个Item,一开始我是想在最外层做成一个 Row 布局,像这样

image.png

左边是圆和线,右边是内容,然而我太天真了,左边的 线 高度没法跟随右边的高度,即右边有多高,左边就有多高。也就是我必须给左边的View设置一个高度,否则就没法显示出来。。。绝望ing,如果我左边写死了高度,右边的内容因为用户字体过大而高度超过左边的线,那么两个 Item 之间的线就没法连在一起了。

然后我看到了 Flutter 的 Stepper ,虽然不符合需求,但是人家左边的线是 Item 和 Item 相连的,我就看了下他的源码,豁然开朗,人家的布局是个 Colum 。整体看起来是这样的。

image.png

这样的话,就好理解了,Colum 的第一个 child 我们称为 Head , 第二个 child 我们称为 Body 。

Head 的布局如图是个 Row,左边是圆和线,右边是个 Text。
Body 的布局是个 Container , 包含了一个 Column ,Column 里面就是两个Text。相信小伙伴们已经想到了,Body左边的那条线就是 Container 的 border

圆和线我选择自己绘制,练习一下,下面是线和圆的自定义View代码


class LeftLineWidget extends StatelessWidget {
  final bool showTop;
  final bool showBottom;
  final bool isLight;

  const LeftLineWidget(this.showTop, this.showBottom, this.isLight);

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: EdgeInsets.symmetric(horizontal: 16),//圆和线的左右外边距
      width: 16,
      child: CustomPaint(
        painter: LeftLinePainter(showTop, showBottom, isLight),
      ),
    );
  }
}

class LeftLinePainter extends CustomPainter {
  static const double _topHeight = 16; //圆上的线高度
  static const Color _lightColor = XColors.mainColor;//圆点亮的颜色
  static const Color _normalColor = Colors.grey;//圆没点亮的颜色

  final bool showTop; //是否显示圆上面的线
  final bool showBottom;//是否显示圆下面的线
  final bool isLight;//圆形是否点亮

  const LeftLinePainter(this.showTop, this.showBottom, this.isLight);

  @override
  void paint(Canvas canvas, Size size) {
    double lineWidth = 2; // 竖线的宽度
    double centerX = size.width / 2; //容器X轴的中心点
    Paint linePain = Paint();// 创建一个画线的画笔
    linePain.color = showTop ? Colors.grey : Colors.transparent;
    linePain.strokeWidth = lineWidth;
    linePain.strokeCap = StrokeCap.square;//画线的头是方形的
    //画圆上面的线
    canvas.drawLine(Offset(centerX, 0), Offset(centerX, _topHeight), linePain);
    //依据下面的线是否显示来设置是否透明
    linePain.color = showBottom ? Colors.grey : Colors.transparent;
    // 画圆下面的线
    canvas.drawLine(
        Offset(centerX, _topHeight), Offset(centerX, size.height), linePain);
    // 创建画圆的画笔
    Paint circlePaint = Paint();
    circlePaint.color = isLight ? _lightColor : _normalColor;
    circlePaint.style = PaintingStyle.fill;
    // 画中间的圆
    canvas.drawCircle(Offset(centerX, _topHeight), centerX, circlePaint);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    if(oldDelegate is LeftLinePainter){
      LeftLinePainter old = oldDelegate;
      if(old.showBottom!=showBottom){
        return true;
      }
      if(old.showTop!=showTop){
        return true;
      }
      if(old.isLight!=isLight){
        return true;
      }
      return false;
    }
    return true;
  }
}

左侧的圆和线是3个部分,分别是圆的上面那条线,和圆,以及圆下面的那条线,
通过 showTopshowBottom 来控制上面那条线和下面那条线是否显示。

圆和线解决了,我就把Head组装起来

Row(
  crossAxisAlignment: CrossAxisAlignment.start,
  children: <Widget>[
    // 圆和线
    Container( 
      height: 32,
      child: LeftLineWidget(false, true, true),
    ),
    Expanded(child: Container(
      padding: EdgeInsets.only(top: 4),
      child: Text(
        '天天乐超市(限时降价)已取货',
        style: TextStyle(fontSize: 18),
        overflow: TextOverflow.ellipsis,
      ),
    ))
  ],
)

编译运行后截图

image.png

(这里截图跟之前不一样是因为我又单独建立了一个demo)

接下来写下面的 Body

Container(
  //这里写左边的那条线
  decoration: BoxDecoration(
    border:Border(left: BorderSide(
      width: 2,// 宽度跟 Head 部分的线宽度一致,下面颜色也是
      color: Colors.grey
    ))
  ),
  margin: EdgeInsets.only(left: 23), //这里的 left 的计算在代码块下面解释怎么来的
  padding: EdgeInsets.fromLTRB(22,0,16,16),
  child: Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: <Widget>[
      Text('配送员:吴立亮 18888888888'),
      Text('时间:2018-12-17 09:55:22')
    ],
  ),
)

这里说一下 margin 的 left 参数值是怎么计算的。
设置这个是为了 Body 的左边框跟上面 Head 的线能对齐连上,不能错开。
首先我们的 LeftLineWidget 是有个 margin 的,他的左右外边距是16,自身的宽度是16。因为线在中间,所以宽度要除以2。那就是:左外边距+宽度除以2 left = 16 + 16/2 算出来是24。

可是我们这里写的23,是因为边框的线的宽度是从容器的边界往里面走的。我们算出来的边距会让 Body 的容器边界在上面的线中间。看起来像这样。

image.png

所以还要减去线宽的一半,线宽是2,除以2等于1, 最后left = 16+(16/2)-(2/2)=23,翻译成中文 left = LeftLineWidget左边距+(LeftLineWidget宽度➗2)-(LeftLineWidget线宽➗2)

最后看起来像这样:


多复制几个


image

最后一item要隐藏边框,把边框线颜色设置为透明即可。

渲染树是这样的

渲染树

最后奉上完整代码:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Stepper',
      home: Scaffold(
        appBar: AppBar(
          elevation: 0,
          title: Text('自定义View'),
        ),
        body: ListView(
          shrinkWrap: true,
          children: <Widget>[
            Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                Row(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    Container(// 圆和线
                      height: 32,
                      child: LeftLineWidget(false, true, true),
                    ),
                    Expanded(child: Container(
                      padding: EdgeInsets.only(top: 4),
                      child: Text(
                        '天天乐超市(限时降价)已取货',
                        style: TextStyle(fontSize: 18),
                        overflow: TextOverflow.ellipsis,
                      ),
                    ))
                  ],
                ),
                Container(
                  decoration: BoxDecoration(
                    border:Border(left: BorderSide(
                      width: 2,
                      color: Colors.grey
                    ))
                  ),
                  margin: EdgeInsets.only(left: 23),
                  padding: EdgeInsets.fromLTRB(22,0,16,16),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: <Widget>[
                      Text('配送员:吴立亮 18888888888'),
                      Text('时间:2018-12-17 09:55:22')
                    ],
                  ),
                )
              ],
            ),
            Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                Row(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    Container(// 圆和线
                      height: 32,
                      child: LeftLineWidget(true, true, false),
                    ),
                    Expanded(child: Container(
                      padding: EdgeInsets.only(top: 4),
                      child: Text(
                        '天天乐超市(限时降价)已取货',
                        style: TextStyle(fontSize: 18),
                        overflow: TextOverflow.ellipsis,
                      ),
                    ))
                  ],
                ),
                Container(
                  decoration: BoxDecoration(
                      border:Border(left: BorderSide(
                          width: 2,
                          color: Colors.grey
                      ))
                  ),
                  margin: EdgeInsets.only(left: 23),
                  padding: EdgeInsets.fromLTRB(22,0,16,16),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: <Widget>[
                      Text('配送员:吴立亮 18888888888'),
                      Text('时间:2018-12-17 09:55:22')
                    ],
                  ),
                )
              ],
            ),
            Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                Row(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    Container(// 圆和线
                      height: 32,
                      child: LeftLineWidget(true, false, false),
                    ),
                    Expanded(child: Container(
                      padding: EdgeInsets.only(top: 4),
                      child: Text(
                        '天天乐超市(限时降价)已取货',
                        style: TextStyle(fontSize: 18),
                        overflow: TextOverflow.ellipsis,
                      ),
                    ))
                  ],
                ),
                Container(
                  decoration: BoxDecoration(
                      border:Border(left: BorderSide(
                          width: 2,
                          color: Colors.transparent
                      ))
                  ),
                  margin: EdgeInsets.only(left: 23),
                  padding: EdgeInsets.fromLTRB(22,0,16,16),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: <Widget>[
                      Text('配送员:吴立亮 18888888888'),
                      Text('时间:2018-12-17 09:55:22')
                    ],
                  ),
                )
              ],
            ),
          ],
        ),
      ),
    );
  }
}

class LeftLineWidget extends StatelessWidget {
  final bool showTop;
  final bool showBottom;
  final bool isLight;

  const LeftLineWidget(this.showTop, this.showBottom, this.isLight);

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: EdgeInsets.symmetric(horizontal: 16),
      width: 16,
      child: CustomPaint(
        painter: LeftLinePainter(showTop, showBottom, isLight),
      ),
    );
  }
}

class LeftLinePainter extends CustomPainter {
  static const double _topHeight = 16;
  static const Color _lightColor = Colors.deepPurpleAccent;
  static const Color _normalColor = Colors.grey;

  final bool showTop;
  final bool showBottom;
  final bool isLight;

  const LeftLinePainter(this.showTop, this.showBottom, this.isLight);

  @override
  void paint(Canvas canvas, Size size) {
    double lineWidth = 2;
    double centerX = size.width / 2;
    Paint linePain = Paint();
    linePain.color = showTop ? Colors.grey : Colors.transparent;
    linePain.strokeWidth = lineWidth;
    linePain.strokeCap = StrokeCap.square;
    canvas.drawLine(Offset(centerX, 0), Offset(centerX, _topHeight), linePain);
    Paint circlePaint = Paint();
    circlePaint.color = isLight ? _lightColor : _normalColor;
    circlePaint.style = PaintingStyle.fill;
    linePain.color = showBottom ? Colors.grey : Colors.transparent;
    canvas.drawLine(
        Offset(centerX, _topHeight), Offset(centerX, size.height), linePain);
    canvas.drawCircle(Offset(centerX, _topHeight), centerX, circlePaint);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return true;
  }
}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容

  • 概述 在网易云课堂学习李南江老师的《从零玩转HTML5前端+跨平台开发》时,所整理的笔记。笔记内容为根据个人需求所...
    墨荀阅读 2,331评论 0 7
  • 一、CSS入门 1、css选择器 选择器的作用是“用于确定(选定)要进行样式设定的标签(元素)”。 有若干种形式的...
    宠辱不惊丶岁月静好阅读 1,584评论 0 6
  • 第一部分 HTML&CSS整理答案 1. 什么是HTML5? 答:HTML5是最新的HTML标准。 注意:讲述HT...
    kismetajun阅读 27,424评论 1 45
  • 浏览器与服务器的基本概念 浏览器(安装在电脑里面的一个软件) 作用: ①将网页内容渲染呈现给用户查看。 ②让用户通...
    云还灬阅读 1,102评论 0 0
  • HTML 5 HTML5概述 因特网上的信息是以网页的形式展示给用户的,因此网页是网络信息传递的载体。网页文件是用...
    阿啊阿吖丁阅读 3,828评论 0 0