簡單實(shí)現(xiàn)一下Flutter的Stepper做一個(gè)側(cè)邊進(jìn)度條

因?yàn)?flutter 提供的 Stepper 無法滿足業(yè)務(wù)需求,于是只好自己實(shí)現(xiàn)一個(gè)了

flutter Stepper 的樣式

原生 Stepper

我實(shí)現(xiàn)的 Stepper

我實(shí)現(xiàn)的 Stepper

這個(gè)或許根本不叫 Stepper 吧,也沒有什么步驟,只是當(dāng)前的配送進(jìn)度,不需要數(shù)字步驟,希望所有內(nèi)容都能顯示出來,原生的則是有數(shù)字表示第幾步,把當(dāng)前步驟外的其他的內(nèi)容都隱藏了。

那么開始進(jìn)行分析,整個(gè)需求中,有點(diǎn)難度的也就是這個(gè)左邊的進(jìn)度線了。我們把進(jìn)度看做一個(gè) ListView ,每條進(jìn)度都是一個(gè) Item

item

先來看怎么布局這個(gè)Item,一開始我是想在最外層做成一個(gè) Row 布局,像這樣

image.png

左邊是圓和線,右邊是內(nèi)容,然而我太天真了,左邊的 線 高度沒法跟隨右邊的高度,即右邊有多高,左邊就有多高。也就是我必須給左邊的View設(shè)置一個(gè)高度,否則就沒法顯示出來。。。絕望ing,如果我左邊寫死了高度,右邊的內(nèi)容因?yàn)橛脩糇煮w過大而高度超過左邊的線,那么兩個(gè) Item 之間的線就沒法連在一起了。

然后我看到了 Flutter 的 Stepper ,雖然不符合需求,但是人家左邊的線是 Item 和 Item 相連的,我就看了下他的源碼,豁然開朗,人家的布局是個(gè) Colum 。整體看起來是這樣的。

image.png

這樣的話,就好理解了,Colum 的第一個(gè) child 我們稱為 Head , 第二個(gè) child 我們稱為 Body 。

Head 的布局如圖是個(gè) Row,左邊是圓和線,右邊是個(gè) Text。
Body 的布局是個(gè) Container , 包含了一個(gè) Column ,Column 里面就是兩個(gè)Text。相信小伙伴們已經(jīng)想到了,Body左邊的那條線就是 Container 的 border

圓和線我選擇自己繪制,練習(xí)一下,下面是線和圓的自定義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;//圓點(diǎn)亮的顏色
  static const Color _normalColor = Colors.grey;//圓沒點(diǎn)亮的顏色

  final bool showTop; //是否顯示圓上面的線
  final bool showBottom;//是否顯示圓下面的線
  final bool isLight;//圓形是否點(diǎn)亮

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

  @override
  void paint(Canvas canvas, Size size) {
    double lineWidth = 2; // 豎線的寬度
    double centerX = size.width / 2; //容器X軸的中心點(diǎn)
    Paint linePain = Paint();// 創(chuàng)建一個(gè)畫線的畫筆
    linePain.color = showTop ? Colors.grey : Colors.transparent;
    linePain.strokeWidth = lineWidth;
    linePain.strokeCap = StrokeCap.square;//畫線的頭是方形的
    //畫圓上面的線
    canvas.drawLine(Offset(centerX, 0), Offset(centerX, _topHeight), linePain);
    //依據(jù)下面的線是否顯示來設(shè)置是否透明
    linePain.color = showBottom ? Colors.grey : Colors.transparent;
    // 畫圓下面的線
    canvas.drawLine(
        Offset(centerX, _topHeight), Offset(centerX, size.height), linePain);
    // 創(chuàng)建畫圓的畫筆
    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;
  }
}

左側(cè)的圓和線是3個(gè)部分,分別是圓的上面那條線,和圓,以及圓下面的那條線,
通過 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(
        '天天樂超市(限時(shí)降價(jià))已取貨',
        style: TextStyle(fontSize: 18),
        overflow: TextOverflow.ellipsis,
      ),
    ))
  ],
)

編譯運(yùn)行后截圖

image.png

(這里截圖跟之前不一樣是因?yàn)槲矣謫为?dú)建立了一個(gè)demo)

接下來寫下面的 Body

Container(
  //這里寫左邊的那條線
  decoration: BoxDecoration(
    border:Border(left: BorderSide(
      width: 2,// 寬度跟 Head 部分的線寬度一致,下面顏色也是
      color: Colors.grey
    ))
  ),
  margin: EdgeInsets.only(left: 23), //這里的 left 的計(jì)算在代碼塊下面解釋怎么來的
  padding: EdgeInsets.fromLTRB(22,0,16,16),
  child: Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: <Widget>[
      Text('配送員:吳立亮 18888888888'),
      Text('時(shí)間:2018-12-17 09:55:22')
    ],
  ),
)

這里說一下 margin 的 left 參數(shù)值是怎么計(jì)算的。
設(shè)置這個(gè)是為了 Body 的左邊框跟上面 Head 的線能對齊連上,不能錯(cuò)開。
首先我們的 LeftLineWidget 是有個(gè) margin 的,他的左右外邊距是16,自身的寬度是16。因?yàn)榫€在中間,所以寬度要除以2。那就是:左外邊距+寬度除以2 left = 16 + 16/2 算出來是24。

可是我們這里寫的23,是因?yàn)?strong>邊框的線的寬度是從容器的邊界往里面走的。我們算出來的邊距會(huì)讓 Body 的容器邊界在上面的線中間??雌饋硐襁@樣。

image.png

所以還要減去線寬的一半,線寬是2,除以2等于1, 最后left = 16+(16/2)-(2/2)=23,翻譯成中文 left = LeftLineWidget左邊距+(LeftLineWidget寬度?2)-(LeftLineWidget線寬?2)

最后看起來像這樣:


多復(fù)制幾個(gè)


image

最后一item要隱藏邊框,把邊框線顏色設(shè)置為透明即可。

渲染樹是這樣的

渲染樹

最后奉上完整代碼:

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(
                        '天天樂超市(限時(shí)降價(jià))已取貨',
                        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('時(shí)間: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(
                        '天天樂超市(限時(shí)降價(jià))已取貨',
                        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('時(shí)間: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(
                        '天天樂超市(限時(shí)降價(jià))已取貨',
                        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('時(shí)間: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;
  }
}

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 概述 在網(wǎng)易云課堂學(xué)習(xí)李南江老師的《從零玩轉(zhuǎn)HTML5前端+跨平臺開發(fā)》時(shí),所整理的筆記。筆記內(nèi)容為根據(jù)個(gè)人需求所...
    墨荀閱讀 2,477評論 0 7
  • 一、CSS入門 1、css選擇器 選擇器的作用是“用于確定(選定)要進(jìn)行樣式設(shè)定的標(biāo)簽(元素)”。 有若干種形式的...
    寵辱不驚丶?xì)q月靜好閱讀 1,746評論 0 6
  • 第一部分 HTML&CSS整理答案 1. 什么是HTML5? 答:HTML5是最新的HTML標(biāo)準(zhǔn)。 注意:講述HT...
    kismetajun閱讀 28,886評論 1 45
  • 瀏覽器與服務(wù)器的基本概念 瀏覽器(安裝在電腦里面的一個(gè)軟件) 作用: ①將網(wǎng)頁內(nèi)容渲染呈現(xiàn)給用戶查看。 ②讓用戶通...
    云還灬閱讀 1,295評論 0 0
  • HTML 5 HTML5概述 因特網(wǎng)上的信息是以網(wǎng)頁的形式展示給用戶的,因此網(wǎng)頁是網(wǎng)絡(luò)信息傳遞的載體。網(wǎng)頁文件是用...
    阿啊阿吖丁閱讀 4,983評論 0 0

友情鏈接更多精彩內(nèi)容