Flutter 70: 圖解自定義 ACEStepper 步進(jìn)器

??????小菜前幾天嘗試了 Flutter Stepper 簡(jiǎn)單實(shí)用伤锚,但樣式等方面也有局限性,Stepper 的使用小菜在上一篇中有過(guò)嘗試 圖解基本 Stepper 步進(jìn)器券册,現(xiàn)在小菜嘗試在此基礎(chǔ)上增加一些新特性;

  1. Step 之間的連線支持 直線和圓點(diǎn)虛線嘶卧,且顏色尺寸均可自定義;
  2. Step Header Icon 中支持 自定義文字/icon/本地圖片/網(wǎng)絡(luò)圖片,且尺寸顏色均可分別自定義;
  3. 橫向 Stepper 支持滑動(dòng)藻懒,不限制整體寬度;
  4. Step 中按鈕支持單個(gè)顯隱性處理视译;
  5. Stepper 中每個(gè) Step 內(nèi)容支持全部展示和單獨(dú)展示嬉荆;
  6. 其他自定義 ThemeData

??????小菜準(zhǔn)備在 Stepper 基礎(chǔ)上進(jìn)行擴(kuò)展酷含,首先要了解 Stepper 的構(gòu)成鄙早,根據(jù)一切都是 Widget 的思想,小菜繪制了一個(gè)基本的構(gòu)成圖:

新特性擴(kuò)展

1. 圓點(diǎn)虛線

??????Step 之間的連線只有直線有些單調(diào)椅亚,針對(duì)不同實(shí)際場(chǎng)景限番,小菜嘗試圓點(diǎn)虛線;

  1. 定義連線類(lèi)型呀舔,nomal 為直線扳缕,circle 為圓點(diǎn)虛線;
enum LineType { normal, circle }
  1. 繪制圓點(diǎn)虛線,小菜準(zhǔn)備支持自定義連線寬度(直線/虛線)躯舔,因此圓點(diǎn)半徑根據(jù)寬度獲得,圓點(diǎn)之間的距離小菜嘗試的是一個(gè)圓點(diǎn)大小省古,在一段長(zhǎng)度中繪制 _circleLength / radius / 4 - 1 個(gè)圓點(diǎn)即可粥庄,小菜之所以 -1 是因?yàn)樵谶B線交接處,首尾之間的圓點(diǎn)過(guò)近(可自由設(shè)置)豺妓;
class _LinePainter extends CustomPainter {
  final Color color;
  final double radius;
  final ACEStepperType type;

  _LinePainter({this.color, this.radius, this.type});

  @override
  bool hitTest(Offset point) => true;

  @override
  bool shouldRepaint(_LinePainter oldPainter) => oldPainter.color != color;

  @override
  void paint(Canvas canvas, Size size) {
    double _circleLength = (type == ACEStepperType.horizontal) ? size.width.toDouble() : size.height.toDouble();
    double _circleSize = _circleLength / radius / 4 > 2 ? _circleLength / radius / 4 - 1 : _circleLength / radius / 4;
    Path _path = Path();
    for (int i = 0; i < _circleSize; i++) {
      _path.addArc(Rect.fromCircle(center: Offset(
                  type == ACEStepperType.horizontal ? radius + 4 * radius * i : radius,
                  type == ACEStepperType.horizontal ? radius : radius + 4 * radius * i),
              radius: radius), 0.0, 2 * pi);
    }
    canvas.drawPath(_path, Paint()..color = color..strokeCap = StrokeCap.round..style = PaintingStyle.fill);
  }
}
  1. 場(chǎng)景繪制直線或圓角虛線惜互;
class StepperLine extends StatelessWidget {
  final Color color;
  final LineType lineType;
  final ACEStepperType type;

  StepperLine({@required this.color, this.type = ACEStepperType.horizontal,  this.lineType = LineType.normal});

  @override
  Widget build(BuildContext context) {
    double _width = (type == ACEStepperType.horizontal) ? _kLineHeight : _kLineWidth;
    double _height = (type == ACEStepperType.horizontal) ? _kLineWidth : _kLineHeight;
    double _diameter = (type == ACEStepperType.horizontal) ? _height : _width;
    return lineType == LineType.normal
        ? Container(width: _width, height: _height, color: color)
        : Container(width: _width, height: _height, child: CustomPaint(painter: _LinePainter(color: color, radius: _diameter * 0.5, type: type)));
  }
}

2. Header Icon 內(nèi)容自定義

??????Step Header Icon 有四種屬性,但展示內(nèi)容除了數(shù)組下標(biāo)遞增其余 Icon 不可變琳拭,小菜增加了自定義文本/Icon/本地圖片/網(wǎng)絡(luò)圖片的展示训堆,并非單一的數(shù)組下標(biāo);

  1. 定義 Header 類(lèi)型白嘁;text 為展示文本內(nèi)容坑鱼,iconIconDataass_url 為本地圖片路徑絮缅,net_url 為網(wǎng)絡(luò)圖片鲁沥,均不設(shè)置默認(rèn)為遞增的數(shù)組下標(biāo);
enum IconType { text, icon, ass_url, net_url }
  1. 繪制圓環(huán)耕魄;
class _CirclePainter extends CustomPainter {
  final Color color;
  final double size;

  _CirclePainter({this.color, this.size});

  @override
  bool hitTest(Offset point) => true;

  @override
  bool shouldRepaint(_CirclePainter oldPainter) => oldPainter.color != color;

  @override
  void paint(Canvas canvas, Size size) {
    final double radius = this.size * 0.5;
    canvas.drawArc(Rect.fromCircle(center: Offset(radius, radius), radius: radius),
        0.0, 2 * pi, false, Paint()..color = color..strokeCap = StrokeCap.round..strokeWidth = 1.0..style = PaintingStyle.stroke);
  }
}
  1. 繪制 Header 內(nèi)容画恰;
Widget _buildIcon(IconType type, CircleData circleData, int index) {
  Color contentActiveColor = widget.themeData == null ? _kContentActiveColor : widget.themeData.contentActiveColor ?? _kContentActiveColor;
  Color contentColor = widget.themeData == null ? _kContentColor : widget.themeData.contentColor ?? _kContentColor;
  Color _color = widget.steps[index].isActive ? contentActiveColor : contentColor;
  switch (type) {
    case IconType.text:
      return Text(circleData.circleText ?? (index + 1).toString(), style: TextStyle(color: _color));
      break;
    case IconType.icon:
      return circleData.circleIcon != null ? Icon(circleData.circleIcon, size: _kCircleIconSize, color: _color) : Text(circleData.circleText ?? (index + 1).toString(), style: TextStyle(color: _color));
      break;
    case IconType.ass_url:
      return circleData.circleAssUrl != null ? Padding(padding: EdgeInsets.all(_kCirclePadding), child: Image.asset(circleData.circleAssUrl, color: _color))
          : Text(circleData.circleText ?? (index + 1).toString(), style: TextStyle(color: _color));
      break;
    case IconType.net_url:
      return circleData.circleNetUrl != null ? Padding(padding: EdgeInsets.all(_kCirclePadding), child: Image.network(circleData.circleNetUrl))
          : Text(circleData.circleText ?? (index + 1).toString(), style: TextStyle(color: _color));
      break;
    default:
      return Text((index + 1).toString(), style: TextStyle(color: _color));
      break;
  }
}
  1. 將繪制 Icon 放置在圓環(huán)內(nèi);
Widget _buildCircle(IconType type, double size, CircleData circleData, int index) {
  Color circleActiveColor = widget.themeData == null ? _kCircleActiveColor : widget.themeData.circleActiveColor ?? _kCircleActiveColor;
  Color circleColor = widget.themeData == null ? _kCircleColor : widget.themeData.circleColor ?? _kCircleColor;
  return Stack(children: <Widget>[
    Container(child: CustomPaint(painter: _CirclePainter(color: widget.steps[index].isActive ? circleActiveColor : circleColor, size: size))),
    Container(width: size, height: size, child: Center(child: _buildIcon(type, circleData, index)))
  ]);
}

3. 橫向滑動(dòng)

??????分析源碼吸奴,Stepper 橫向方式是將 Step 放置在 Row 中允扇,此時(shí)若 Step 數(shù)量過(guò)多會(huì)造成寬度溢出;小菜調(diào)整存儲(chǔ)方式则奥,將自定義的 ACEStepper 放置在橫向 ListView 中考润,不會(huì)限制寬度,放置多個(gè) ACEStep 可橫向滑動(dòng)逞度;

Widget _buildHorizontal() {
  return Column(children: <Widget>[
    Container(height: widget.headerHeight <= 0.0 ? _kHeaderHeight : widget.headerHeight,
        child: ListView(primary: false, shrinkWrap: true, scrollDirection: Axis.horizontal,
            children: <Widget>[
              for (int i = 0; i < widget.steps.length; i += 1)
                Column(key: _keys[i], children: <Widget>[
                  InkWell(child: _buildHorizontalHeader(i), onTap: () => (widget.onStepTapped != null) ? widget.onStepTapped(i) : null)
                ])
            ])),
    Expanded(child: ListView(children: <Widget>[
      Container(child: widget.steps[widget.currentStep].content ?? SizedBox.shrink()),
      _buildVerticalControls()
    ]))
  ]);
}

4. 單個(gè)按鈕顯隱性

??????縱向 StepperControls 按鈕是默認(rèn)展示的额划,小菜為了適應(yīng)更多場(chǎng)景,允許按鈕單獨(dú)展示档泽;

Widget _buildVerticalControls() {
  return (widget.controlsBuilder != null) ? widget.controlsBuilder(context, onStepContinue: widget.onStepContinue, onStepCancel: widget.onStepCancel)
      : Container(child: Row(children: <Widget>[
          widget.isContinue ? FlatButton( onPressed: widget.onStepContinue, child: Text('繼續(xù)')) : SizedBox.shrink(),
          widget.isCancel ? FlatButton(onPressed: widget.onStepCancel, child: Text('取消')) : SizedBox.shrink()
        ]));
}

5. Content 內(nèi)容展示

??????Stepper 中選中單個(gè) Step 時(shí)會(huì)展示 Content 內(nèi)容俊戳,但小菜嘗試做一個(gè)物流信息時(shí)間軸,Content 內(nèi)容都要展示馆匿,因此添加一個(gè)狀態(tài)抑胎,允許用戶是否全部展示 Content

Widget _buildVerticalBody(int index) {
  double circleDiameter = widget.themeData == null ? _kCircleDiameter : widget.themeData.circleDiameter ?? _kCircleDiameter;
  return Stack(children: <Widget>[
    PositionedDirectional(
        start: _kTopTipsWidth + (circleDiameter - _kLineWidth) * 0.5, top: Size.zero.width, bottom: Size.zero.width - 2,
        child: _isLast(index) ? SizedBox.shrink() : AspectRatio(aspectRatio: 1, child: SizedBox.expand(child: _buildLine(index, false)))),
    widget.isAllContent ? Container(
            margin: EdgeInsets.only(left: _kTopTipsWidth + _kCircleMargin * 2 + circleDiameter),
            child: Column(crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[widget.steps[index].content ?? SizedBox.shrink(),  _buildVerticalControls()  ]))
        : AnimatedCrossFade(firstChild: SizedBox.shrink(),
            secondChild: Container(margin: EdgeInsetsDirectional.only(start: _kTopTipsWidth + _kCircleMargin * 2 + circleDiameter),
                child: Column(children: <Widget>[widget.steps[index].content ?? SizedBox.shrink(), _buildVerticalControls() ])),
            crossFadeState: _isCurrent(index) ? CrossFadeState.showSecond : CrossFadeState.showFirst,
            duration: Duration(milliseconds: 1))
  ]);
}

6. 自定義 ThemeData

??????為了擴(kuò)展 Stepper 展示效果的靈活性渐北,小菜添加了 ThemeData 主題靈活展示各位置顏色等阿逃;

class ACEStepThemeData {
  final Color circleColor,      // 圓環(huán)默認(rèn)顏色
      circleActiveColor,        // 圓環(huán)選中顏色
      contentColor,             // 圓環(huán)內(nèi)容默認(rèn)顏色
      contentActiveColor,       // 圓環(huán)內(nèi)容選中顏色
      lineColor;                // 連線顏色
  final double circleDiameter;  // 圓環(huán)直徑

  ACEStepThemeData(
      {this.circleColor = _kCircleColor,
      this.lineColor = _kLineColor,
      this.circleActiveColor = _kCircleActiveColor,
      this.contentColor = _kContentColor,
      this.contentActiveColor = _kContentActiveColor,
      this.circleDiameter = _kCircleDiameter});
}

源碼介紹

const ACEStepper(
  {Key key,
  @required this.steps,                 // ACEStep 數(shù)組
  this.physics,                         // 滑動(dòng)動(dòng)畫(huà)
  this.type = ACEStepperType.vertical,  // 方向:橫向/縱向
  this.currentStep = 0,                 // 當(dāng)前 ACEStep
  this.onStepTapped,                    // ACEStep 點(diǎn)擊回調(diào)
  this.onStepContinue,                  // ACEStep 繼續(xù)按鈕回調(diào)
  this.onStepCancel,                    // ACEStep 取消按鈕回調(diào)
  this.isContinue = true,               // 繼續(xù)按鈕顯隱性
  this.isCancel = true,                 // 取消按鈕顯隱性
  this.headerHeight,                    // 橫向 Header 高度
  this.controlsBuilder,                 // 自定義控件
  this.themeData,                       // 主題樣式
  this.isAllContent = false});          // 內(nèi)容是否全部展示

const ACEStep(
    {@required this.title,              // 標(biāo)題 Widget
    @required this.circleData,          // 標(biāo)題圖標(biāo)內(nèi)容
    this.content,                       // 內(nèi)容 Widget
    this.subtitle,                      // 副標(biāo)題 Widget
    this.toptips,                       // 頂部提示 Widget
    this.lineType = LineType.normal,    // 連線方式
    this.iconType = IconType.text,      // 標(biāo)題圖標(biāo)方式
    this.isActive = false});            // 是否高亮

??????分析源碼,小菜自定義的 ACEStepperStepper 用法類(lèi)似,只是增加了擴(kuò)展項(xiàng)恃锉,具體的使用請(qǐng)到 GitHub搀菩;

注意事項(xiàng)

1. Header 連接方式

??????Step Header Icon 的連接是由兩條固定長(zhǎng)度的連線與圓環(huán)的拼接,連線處在第一個(gè)和最后一個(gè)時(shí)隱藏展示破托;因此造成一個(gè)問(wèn)題肪跋,當(dāng) Title / subTitle 內(nèi)容設(shè)置過(guò)大時(shí),會(huì)造成 HeaderContent 連線不銜接土砂;小菜暫未找到合適的處理方式州既,希望有解決方案的朋友多多指導(dǎo)!

2. Content 連接方式

??????在縱向 StepperContent 的展示對(duì)應(yīng)的連線是單獨(dú)的連線萝映,與上下兩個(gè) Header 進(jìn)行銜接吴叶;但 Content 大小并不固定,而小菜繪制的圓點(diǎn)虛線需要獲取其高度進(jìn)行繪制序臂;小菜分析源碼通過(guò) State / AspectRatio 進(jìn)行處理蚌卤,AspectRatio 的研究會(huì)在后續(xù)博客中學(xué)習(xí)研究;

Widget _buildVerticalBody(int index) {
  return Stack(children: <Widget>[
    PositionedDirectional(
        start: _kTopTipsWidth + (circleDiameter - _kLineWidth) * 0.5, top: Size.zero.width, bottom: Size.zero.width - 2,
        child: _isLast(index) ? SizedBox.shrink() : AspectRatio(aspectRatio: 1, child: SizedBox.expand(child: _buildLine(index, false)))),
        Container(margin: EdgeInsets.only(left: _kTopTipsWidth + _kCircleMargin * 2 + circleDiameter),
            child: Column(crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[widget.steps[index].content ?? SizedBox.shrink(),  _buildVerticalControls()]))
  ]);
}

3. 橫向 Header 高度

??????小菜在處理橫向 ACEStepper Header 時(shí)用 ListView 存放 ACEStepper贸宏,解決了橫向溢出的問(wèn)題造寝;但將 HeaderContent 放在 Column 中是會(huì)涉及到 ListView 高度錯(cuò)誤的問(wèn)題,小菜采用 Expend 方式也未很好處理吭练,目前設(shè)置了基本的高度诫龙;有更好方案的朋友請(qǐng)多指導(dǎo)!


??????小菜對(duì) ACEStepper 的自定義還不夠成熟鲫咽,還有很多需要優(yōu)化的地方签赃,有建議的地方請(qǐng)多多指導(dǎo)!

來(lái)源: 阿策小和尚

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末分尸,一起剝皮案震驚了整個(gè)濱河市锦聊,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌箩绍,老刑警劉巖孔庭,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異材蛛,居然都是意外死亡圆到,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)卑吭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)芽淡,“玉大人,你說(shuō)我怎么就攤上這事豆赏≌醴疲” “怎么了富稻?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)白胀。 經(jīng)常有香客問(wèn)我椭赋,道長(zhǎng),這世上最難降的妖魔是什么或杠? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任纹份,我火速辦了婚禮,結(jié)果婚禮上廷痘,老公的妹妹穿的比我還像新娘。我一直安慰自己件已,他們只是感情好笋额,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著篷扩,像睡著了一般兄猩。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上鉴未,一...
    開(kāi)封第一講書(shū)人閱讀 48,970評(píng)論 1 284
  • 那天枢冤,我揣著相機(jī)與錄音,去河邊找鬼铜秆。 笑死淹真,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的连茧。 我是一名探鬼主播核蘸,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼啸驯!你這毒婦竟也來(lái)了客扎?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤罚斗,失蹤者是張志新(化名)和其女友劉穎徙鱼,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體针姿,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡袱吆,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了搓幌。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片杆故。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖溉愁,靈堂內(nèi)的尸體忽然破棺而出处铛,到底是詐尸還是另有隱情饲趋,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布撤蟆,位于F島的核電站奕塑,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏家肯。R本人自食惡果不足惜龄砰,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望讨衣。 院中可真熱鬧换棚,春花似錦、人聲如沸反镇。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)歹茶。三九已至夕玩,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間惊豺,已是汗流浹背燎孟。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留尸昧,地道東北人揩页。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像彻磁,于是被迫代替她去往敵國(guó)和親碍沐。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • 我想要的大學(xué)生活衷蜓,不是她們的“不在圖書(shū)館累提,就在去圖書(shū)館的路上” 我想要的大學(xué)生活很簡(jiǎn)單,就是每天把單詞記完磁浇,作業(yè)寫(xiě)...
    阿欒_serene閱讀 303評(píng)論 0 0
  • 演講無(wú)處不在斋陪,我與Ta的私生活就從那時(shí)候開(kāi)始了…… 表達(dá)自我并影響他人的最好方式,一個(gè)是語(yǔ)言交流置吓,那就是演講无虚;一個(gè)...
    李羅伯閱讀 464評(píng)論 1 2
  • 當(dāng)我慢慢將你忘卻 你也漸漸將我模糊 情已走遠(yuǎn) 話已隨風(fēng) 不再有牽掛 不再糾纏絲毫 各不相欠 淡淡的回憶里 只有淺淺...
    君酒濃閱讀 259評(píng)論 0 1
  • 我以為,到了談戀愛(ài)的時(shí)間后衍锚,你會(huì)屬于我友题,后來(lái)才知道,當(dāng)初的我戴质,是有多傻度宦! 帶著一顆踢匣,終于可以和...
    葉寒清閱讀 299評(píng)論 1 2