自定义UI系列专栏
前言-时间轴
Canvas 除了可以绘制图表外,还可以有其他更多的用途,比如通过自定义绘制后实现我们常用的 timeline 时间轴的效果。
通常时间轴有如下的特点:
- 是个列表,并且列表的 item 高度 不固定
- 列表的一侧有轴线标识
时间轴的实现方式,在web、Android、IOS里各自都有很多的实现方式,这篇文章将在 Flutter 上借助
Container
的decoration
属性,通过Canvas
自绘来实现时间轴的效果。
Container
Container
是 Flutter 中最常用的组件之一,其 decoration
可以为 Container
设置各式各样的边框。
Container(
width: 100,
height: 150,
decoration: BoxDecoration(
border: Border(left: BorderSide(color: Colors.blue, width: 2)),
color: Colors.grey.shade400,
),
);
如上图三,为
Container
只设置左侧边框的效果。因为边框高度是跟随Container
变化的,这正符合 timeline 的需求,所以我们可以用这个边框作为时间轴,用 Container 来实现 item 。
BoxDecoration
BoxDecoration
中的 border
是设置边框的属性,通常我们会用 BoxBorder
的子类 Border
或者 BorderDirectional
。以 BorderDirectional
为例,BorderDirectional
中除了一些属性设置方法外,有个 paint
方法,这个方法就是绘制边框的方法,我们看一下它的源码
void paint(
Canvas canvas,
Rect rect, {
TextDirection? textDirection,
BoxShape shape = BoxShape.rectangle,
BorderRadius? borderRadius,
}) {
if (isUniform) {
switch (top.style) {
case BorderStyle.none:
return;
case BorderStyle.solid:
switch (shape) {
case BoxShape.circle:
assert(borderRadius == null, 'A borderRadius can only be given for rectangular boxes.');
BoxBorder._paintUniformBorderWithCircle(canvas, rect, top);
break;
case BoxShape.rectangle:
if (borderRadius != null) {
BoxBorder._paintUniformBorderWithRadius(canvas, rect, top, borderRadius);
return;
}
BoxBorder._paintUniformBorderWithRectangle(canvas, rect, top);
break;
}
return;
}
}
//省略更多 ....
}
BorderDirectional
的paint
这个方法,会根据我们设置的各种属性,调用对应的绘制方法,来绘制不同的边框效果。最终将会被BorderDirectional
的createBoxPainter
方法中,生成的_BoxDecorationPainter
对象调用
class BoxDecoration extends Decoration {
//省略更多 ....
@override
BoxPainter createBoxPainter([ VoidCallback? onChanged ]) {
assert(onChanged != null || image == null);
return _BoxDecorationPainter(this, onChanged);
}
}
class _BoxDecorationPainter extends BoxPainter {
//省略更多 ....
/// Paint the box decoration into the given location on the given canvas.
@override
void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
//省略更多 ....
//这里就是调用 Border 的 paint 的位置
_decoration.border?.paint(
canvas,
rect,
shape: _decoration.shape,
borderRadius: _decoration.borderRadius as BorderRadius?,
textDirection: configuration.textDirection,
);
}
}
由此,我们可以继承 BoxBorder
并重写其 paint
方法,利用 paint
方法提供的 Canvas
来绘制我们想要的效果。
BoxBorder 的继承重写
BoxBorder
中 paint
方法的参数
- canvas: 画布
- rect :画布范围
- textDirection: 文本方向 直接上代码
class TimeLineBoxBorder extends BoxBorder {
@override
BorderSide get bottom => BorderSide.none;
@override
BorderSide get top => BorderSide.none;
@override
ShapeBorder scale(double t) => this;
@override
EdgeInsetsGeometry get dimensions => EdgeInsetsGeometry.infinity;
@override
bool get isUniform => false;
@override
void paint(Canvas canvas, Rect rect, {TextDirection? textDirection, BoxShape shape = BoxShape.rectangle, BorderRadius? borderRadius}) {
canvas.drawLine(
Offset(rect.left, rect.top),
Offset(rect.left, rect.bottom),
paintLine,
);
canvas.drawPoints(
PointMode.lines,
_points(rect),
paintPoint,
);
}
}
把
Container
的decoration
属性,设置为BoxDecoration
,并且border
为我们自定义的TimeLineBoxBorder
.
Container(
width: 100,
height: 100,
decoration: BoxDecoration(border: TimeLineBoxBorder(), color: Colors.grey.shade400),
);
最后效果:
这样,我们把通过重新
BoxBorder
的paint
,实现了自定义的border
。再把这个Container
应用到ListView
中就是了时间轴的效果的效果。
写在最后
除了重写
border
外,我们也可以重写BoxDecoration
的createBoxPainter
的方法,来自定义个BoxPainter
, 这样能实现其他更多的Container
背景和装饰边框。
附上一些效果图: