因為 flutter 提供的 Stepper 無法滿足業務需求,於是只好自己實現一個了
flutter Stepper 的樣式
我實現的 Stepper
這個或許根本不叫 Stepper 吧,也沒有什麼步驟,只是當前的配送進度,不需要數字步驟,希望所有內容都能顯示出來,原生的則是有數字表示第幾步,把當前步驟外的其他的內容都隱藏了。
那麼開始進行分析,整個需求中,有點難度的也就是這個左邊的進度線了。我們把進度看做一個 ListView ,每條進度都是一個 Item
先來看怎麼佈局這個Item,一開始我是想在最外層做成一個 Row 佈局,像這樣
左邊是圓和線,右邊是內容,然而我太天真了,左邊的 線 高度沒法跟隨右邊的高度,即右邊有多高,左邊就有多高。也就是我必須給左邊的View設定一個高度,否則就沒法顯示出來。。。絕望ing,如果我左邊寫死了高度,右邊的內容因為使用者字型過大而高度超過左邊的線,那麼兩個 Item 之間的線就沒法連在一起了。
然後我看到了 Flutter 的 Stepper ,雖然不符合需求,但是人家左邊的線是 Item 和 Item 相連的,我就看了下他的原始碼,豁然開朗,人家的佈局是個 Colum 。整體看起來是這樣的。
這樣的話,就好理解了,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個部分,分別是圓的上面那條線,和圓,以及圓下面的那條線,
通過 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 引數值是怎麼計算的。
設定這個是為了 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)
最後看起來像這樣:
多複製幾個
最後一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;
}
}
複製程式碼