簡單實現一下Flutter的Stepper做一個側邊進度條

jethroHuang發表於2019-03-22

因為 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)

最後看起來像這樣:

簡單實現一下Flutter的Stepper做一個側邊進度條

多複製幾個

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;
  }
}

複製程式碼

相關文章