Flutter 的自定义 UI 系列(五)-示例:timeline时间轴效果

2,604 阅读3分钟


自定义UI系列专栏



前言-时间轴

Canvas 除了可以绘制图表外,还可以有其他更多的用途,比如通过自定义绘制后实现我们常用的 timeline 时间轴的效果。
通常时间轴有如下的特点:

  • 是个列表,并且列表的 item 高度 不固定
  • 列表的一侧有轴线标识 时间轴的实现方式,在web、Android、IOS里各自都有很多的实现方式,这篇文章将在 Flutter 上借助 Containerdecoration属性,通过 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,
  ),
);

tiemline1.png

如上图三,为 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;
    }
  }
//省略更多  ....
}

BorderDirectionalpaint 这个方法,会根据我们设置的各种属性,调用对应的绘制方法,来绘制不同的边框效果。最终将会被 BorderDirectionalcreateBoxPainter 方法中,生成的 _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 的继承重写

BoxBorderpaint方法的参数

  • 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,
    );
  }
}

Containerdecoration 属性,设置为 BoxDecoration ,并且 border 为我们自定义的 TimeLineBoxBorder .

Container(
  width: 100,
  height: 100,
  decoration: BoxDecoration(border: TimeLineBoxBorder(), color: Colors.grey.shade400),
);

最后效果:

timeline5.png

这样,我们把通过重新 BoxBorderpaint,实现了自定义的 border 。再把这个 Container 应用到 ListView 中就是了时间轴的效果的效果。

写在最后

除了重写 border 外,我们也可以重写 BoxDecorationcreateBoxPainter 的方法,来自定义个 BoxPainter, 这样能实现其他更多的 Container 背景和装饰边框。


附上一些效果图:

timeline1.png

timeline4.png

示例代码 github