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

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

左邊是圓和線,右邊是內(nèi)容,然而我太天真了,左邊的 線 高度沒法跟隨右邊的高度,即右邊有多高,左邊就有多高。也就是我必須給左邊的View設(shè)置一個(gè)高度,否則就沒法顯示出來。。。絕望ing,如果我左邊寫死了高度,右邊的內(nèi)容因?yàn)橛脩糇煮w過大而高度超過左邊的線,那么兩個(gè) Item 之間的線就沒法連在一起了。
然后我看到了 Flutter 的 Stepper ,雖然不符合需求,但是人家左邊的線是 Item 和 Item 相連的,我就看了下他的源碼,豁然開朗,人家的布局是個(gè) Colum 。整體看起來是這樣的。

這樣的話,就好理解了,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è)部分,分別是圓的上面那條線,和圓,以及圓下面的那條線,
通過 showTop 和 showBottom 來控制上面那條線和下面那條線是否顯示。
圓和線解決了,我就把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)行后截圖

(這里截圖跟之前不一樣是因?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 的容器邊界在上面的線中間??雌饋硐襁@樣。

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

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

最后一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;
}
}