因为 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;
}
}