因為 flutter 提供的 Stepper 無法滿足業(yè)務(wù)需求,于是只好自己實現(xiàn)一個了
flutter Stepper 的樣式
我實現(xiàn)的 Stepper
這個或許根本不叫 Stepper 吧,也沒有什么步驟褐着,只是當前的配送進度艺谆,不需要數(shù)字步驟唱逢,希望所有內(nèi)容都能顯示出來爷速,原生的則是有數(shù)字表示第幾步陵刹,把當前步驟外的其他的內(nèi)容都隱藏了迂求。
那么開始進行分析碾盐,整個需求中,有點難度的也就是這個左邊的進度線了揩局。我們把進度看做一個 ListView 毫玖,每條進度都是一個 Item
先來看怎么布局這個Item,一開始我是想在最外層做成一個 Row 布局,像這樣
左邊是圓和線付枫,右邊是內(nèi)容烹玉,然而我太天真了,左邊的 線 高度沒法跟隨右邊的高度阐滩,即右邊有多高春霍,左邊就有多高。也就是我必須給左邊的View設(shè)置一個高度叶眉,否則就沒法顯示出來址儒。。衅疙。絕望ing莲趣,如果我左邊寫死了高度,右邊的內(nèi)容因為用戶字體過大而高度超過左邊的線饱溢,那么兩個 Item 之間的線就沒法連在一起了喧伞。
然后我看到了 Flutter 的 Stepper ,雖然不符合需求绩郎,但是人家左邊的線是 Item 和 Item 相連的潘鲫,我就看了下他的源碼,豁然開朗肋杖,人家的布局是個 Colum 溉仑。整體看起來是這樣的。
這樣的話状植,就好理解了浊竟,Colum 的第一個 child 我們稱為 Head , 第二個 child 我們稱為 Body 。
Head 的布局如圖是個 Row津畸,左邊是圓和線振定,右邊是個 Text。
Body 的布局是個 Container 肉拓, 包含了一個 Column 后频,Column 里面就是兩個Text。相信小伙伴們已經(jīng)想到了暖途,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();// 創(chuàng)建一個畫線的畫筆
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個部分丧肴,分別是圓的上面那條線残揉,和圓胧后,以及圓下面的那條線芋浮,
通過 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(
'天天樂超市(限時降價)已取貨',
style: TextStyle(fontSize: 18),
overflow: TextOverflow.ellipsis,
),
))
],
)
編譯運行后截圖
(這里截圖跟之前不一樣是因為我又單獨建立了一個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 參數(shù)值是怎么計算的镇草。
設(shè)置這個是為了 Body 的左邊框跟上面 Head 的線能對齊連上,不能錯開瘤旨。
首先我們的 LeftLineWidget 是有個 margin 的梯啤,他的左右外邊距是16,自身的寬度是16存哲。因為線在中間因宇,所以寬度要除以2。那就是:左外邊距+寬度除以2 left = 16 + 16/2
算出來是24祟偷。
可是我們這里寫的23察滑,是因為邊框的線的寬度是從容器的邊界往里面走的。我們算出來的邊距會讓 Body 的容器邊界在上面的線中間修肠『爻剑看起來像這樣。
所以還要減去線寬的一半嵌施,線寬是2饲化,除以2等于1, 最后left = 16+(16/2)-(2/2)=23,翻譯成中文 left = LeftLineWidget左邊距+(LeftLineWidget寬度?2)-(LeftLineWidget線寬?2)
最后看起來像這樣:
多復(fù)制幾個
最后一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(
'天天樂超市(限時降價)已取貨',
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;
}
}