Flutter 如何实现一个边线按钮

114 阅读3分钟

最近看到 tolyui 中有个虚线按钮,比较有意思。就想看看它的源码实现。写篇小文章,记录一下学习的过程:

image.png


1. 按钮的装饰边线

代码里用了 ElevatedButton 组件,用 OutlineButtonPalette 呈现圆角的虚线效果。这里虚线的秘密应该是 step 和 span 的作用:

ElevatedButton(
  onPressed: () {},
  style: OutlineButtonPalette(
    borderPalette: border,
    foregroundPalette: foreground,
    borderRadius: BorderRadius.circular(20),
    step: 2,
    span: 2,
    backgroundPalette: bg,
  ).style,
  child: Text("Cancel"),
),

来看看这个属性在 OutlineButtonPalette 的作用:可以看出,在 dashGap 为 0 时,会使用 RoundedRectangleBorder 圆角矩形边线形状。反之会用 DashOutlineShapeBorder 边线形状。

image.png

再看一下,这个形状是他自定义的形状,原来形状还能自定义啊,真好玩。 DashOutlineShapeBorder 继承自 OutlinedBorder ,在构造函数中传入边线、圆角、虚线配置等数据:

class DashOutlineShapeBorder extends OutlinedBorder {
  const DashOutlineShapeBorder({
    super.side,
    this.borderRadius = BorderRadius.zero,
    required this.step,
    required this.span,
  });

2. 自定义装饰的绘制

里面最重要的是复写了 paint 方法,这里画什么,按钮就能展示什么边线;绘制过程中需要的参数,由构造方法传入。getOuterPath 方法可以得到边线的路径,最后通过 DashPainter 进行绘制,这样就能得到虚线边框了:

image.png

@override
void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) {
  final Paint paint = Paint()
    ..style = PaintingStyle.stroke
    ..strokeWidth = side.width
    ..color = side.color;
  Path path = getOuterPath(rect);
  DashPainter(step: step, span: span).paint(canvas, path, paint);
}

@override
Path getInnerPath(Rect rect, {TextDirection? textDirection}) {
  final RRect borderRect = borderRadius.resolve(textDirection).toRRect(rect);
  final RRect adjustedRect = borderRect.deflate(side.strokeInset);
  return Path()..addRRect(adjustedRect);
}

@override
Path getOuterPath(Rect rect, {TextDirection? textDirection}) {
  return Path()..addRRect(borderRadius.resolve(textDirection).toRRect(rect));
}

@override
void paintInterior(Canvas canvas, Rect rect, Paint paint, {TextDirection? textDirection}) {
  if (borderRadius == BorderRadius.zero) {
    canvas.drawRect(rect, paint);
  } else {
    canvas.drawRRect(borderRadius.resolve(textDirection).toRRect(rect), paint);
  }
}

3. 怎么实现虚线的绘制

这里的源码很简单,DashPainter 就像一个简单的配置类,step 和 span 分别是实线长和空格长。绘制过程中,主要使用 path.computeMetrics 方法测量路径:
PathMetric 对象可以通过 extractPath 方法截取指定长度的路径,拿到路径就可以使用 canvas drawPath 绘制路径。这里计算出总的段数,然后遍历绘制每个分段就行了:

// [step] the length of solid line 每段实线长
// [span] the space of each solid line  每段空格线长
class DashPainter {
  const DashPainter({
    this.step = 2,
    this.span = 2,
  });

  final double step;
  final double span;

  void paint(Canvas canvas, Path path, Paint paint) {
    final PathMetrics pms = path.computeMetrics();
    final double pointLineLength = paint.strokeWidth;
    final double partLength = step + span;

    for (PathMetric pm in pms) {
      final int count = pm.length ~/ partLength;
      for (int i = 0; i < count; i++) {
        canvas.drawPath(
          pm.extractPath(partLength * i, partLength * i + step),
          paint,
        );
      }
      final double tail = pm.length % partLength;
      canvas.drawPath(pm.extractPath(pm.length - tail, pm.length), paint);
    }
  }
}

4. 怎么把代码偷出来

了解了原理以后,就非常简单了,其实就是自定义了一个边线形状。如果不想用 tolyui ,只是简单实现一个虚线边线按钮。直接把 DashOutlineShapeBorder 拷贝出来,在 ElevatedButton 构造时通过 shape 指定边线形状为 DashOutlineShapeBorder 即可。tolyui 只是提供了 OutlineButtonPalette 帮助调色,形成 style 而已,并不是非常复杂

image.png

ElevatedButton(
  style: ElevatedButton.styleFrom(
      backgroundColor: Colors.white,
      foregroundColor: Colors.blue,
      shape: DashOutlineShapeBorder(
          step: 4,
          span: 2,
          borderRadius: BorderRadius.circular(6),
          side: BorderSide(color: Colors.blue))),
  onPressed: () {},
  child: Text('星星sweet'),
),

不止是按钮的边线,任何有 shape 字段的组件都可以通过这种方式来处理虚线边线。甚至还可以自定义装饰对象,使用这个里的绘制边线的小技巧即可。