这是我参与2022首次更文挑战的第16天,活动详情查看:2022首次更文挑战

往期回眸:

Flutter 必知必会系列 —— 随心所欲的自定义绘制 I

Flutter 必知必会系列 —— 随心所欲的自定义绘制 II

Flutter 必知必会系列 —— 随心所欲的自定义绘制 III

前面三篇文章基本就把 Flutter 的自定义绘制介绍完了,通过这些 API 开发者可以绘制出完全自定义的内容,这些自定义的内容可能是很简单的,也可能是很复杂的。

这一篇就是前面的总结。

用谁画

自定义的绘制需要借助 Flutter 提供的组件 —— CustomPaint 组件。CustomPaint 接受下面几个比较重要的参数

参数名 参数类型 参数作用
key Key? 和 Widget 的 key 作用一样
painter CustomPainter? 背景图的画笔
foregroundPainter CustomPainter? 前景图的画笔
size Size 组件的尺寸
child Widget? 子 Widget

上面的参数有两个注意点,如下:

绘制内容的层级上:自底向上依次是 painter ——> child ——> foregroundPainter,上面的内容会把下面的内容覆盖。

组件的尺寸:如果设置了 child,那么就用 child 的尺寸,否则就用 size 的尺寸,size 的尺寸默认是 Size.zero

绘制的内容会产生遮盖,比如下面的代码:

CustomPaint(
    painter: MyPainter(false),
    child: Container(
      height: 200,
      width: 200,
      color: Colors.blue,
    ),
    foregroundPainter: MyPainter(true),
  )
class MyPainter extends CustomPainter {
  bool isForeground;
  MyPainter(this.isForeground);
  @override
  void paint(Canvas canvas, Size size) {
    if (isForeground) {
      canvas.clipRect(Rect.fromLTWH(0, 0, 100, 100));
    } else {
      canvas.clipRect(Rect.fromLTWH(0, 0, 300, 300));
    }
    Paint paint = Paint();
    paint.color = isForeground ? Colors.red : Colors.pinkAccent;
    paint.strokeWidth = 2;
    paint.style = PaintingStyle.stroke;
    canvas.drawPaint(paint);
  }
  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return false;
  }
}

效果如下:

Flutter 必知必会系列 —— 随心所欲的自定义绘制最终章

粉色的就是背景画笔的内容,蓝色的是 child 的内容,红色的就是前景画笔的内容。

注意 size 并不会真正的影响绘制边界,比如下面的代码:

Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      CustomPaint(
        painter: MyPainter(),
      ),
      Text("文本")
    ],
  )
class MyPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    Paint paint = Paint();
    paint.color = Colors.red;
    paint.strokeWidth = 2;
    paint.style = PaintingStyle.stroke;
    canvas.drawLine(Offset.zero, Offset(200, 200), paint);
  }
}

CustomPaint 的尺寸是 0,依然会画出一条 (0,0)(200, 200) 的红色斜线。所以官方文档有一句话:To enforce painting within those bounds, consider wrapping this CustomPaint with a ClipRect widget.
使用 ClipRect 包裹 CustomPaint 来强制 CustomPaint 是其本身的大小。

绘制的内容

绘制的内容可能是很简单的表格,也能是很复杂的图文。虽然自定义绘制有很强的灵活性,但是也需要很高的投入成本。
比如像一张具有颜色混合的图片,开发者费劲半天还不如UI直接切一张图呢~~。
所以绘制这一块,还是具体问题具体分析。

基本的绘制元

基本的绘制元包含了:点、线、矩形、多边形、图文等的绘制。

方法名 作用
drawLine 绘制直线
drawRect 绘制矩形
drawRRect 绘制圆角矩形
drawDRRect 绘制圆角矩形环
drawOval 绘制椭圆
drawCircle 绘制圆形
drawArc 绘制圆弧
drawImage 绘制图片
drawImageRect 绘制带有位置信息的部分图片
drawImageNine 绘制.9图片
drawParagraph 绘制文本
drawPoints 绘制点

除了这些基本的绘制,还可以借助路径绘制实现自定义的效果。路径绘制也是由基本的点线面组成,不同的是,一条路径上可以拼接多个子路径。比如:

Path path = Path();
path.lineTo(20, 20);
path.moveTo(30,30);
path.lineTo(40, 40);

上面代码的路径会有两条子路径。

不管是基本的绘制还是路径的绘制,Flutter 只认识直角坐标下(x,y),而我们绘制的图形可能是极坐标描述的,这个时候需要进行角度的转换与坐标的转换。
比如: 漂亮的 = 50 *(e^cos - 2cos4 + (sin(/12))^5 ) 图像对应坐标生成代码如下:

void _initPoints() {
 points = [];
 for (int i = 0; i < 360; i++) {
  double thta = _convert(i);
  double p = _calY(thta);
  points.add(Offset(p * cos(thta), p * sin(thta)));
  }
}
​
double _calY(double thta) {
 return 50 * (pow(e, cos(x)) - 2 * cos(4 * x)) + pow(sin(x / 12), 5);
}
​
double _convert(int x) {
 return pi / 180 * x;
}

除了上面基本的图形绘制,自定义的绘制还需要手势、动画的支持。

响应手势的绘制

CustomPaint 背后的 RenderCustomPaint 虽有手势竞技场的检测,如下代码:

@override
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
  if (_foregroundPainter != null && (_foregroundPainter!.hitTest(position) ?? false))
    return true;
  return super.hitTestChildren(result, position: position);
}
@override
bool hitTestSelf(Offset position) {
  return _painter != null && (_painter!.hitTest(position) ?? true);
}

但是没有具体的响应处理逻辑,所以想要增加手势效果,需要在自定义的Widget 上面包裹一层手势组件 —— GestureDetector。如下的代码:

class _PaintWidgetState extends State<PaintWidget>
    with SingleTickerProviderStateMixin {
  late ValueNotifier<Offset> valueNotifier;
  @override
  void initState() {
    super.initState();
    valueNotifier = ValueNotifier(Offset.zero);
  }
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
            onTap: (){},
            onPanStart: (detail){},
            onPanUpdate: (detail){
              valueNotifier.value = detail.globalPosition;
            },
            child: CustomPaint(
              painter: MyPainter(valueNotifier),
            ),
      );
  }
}
class MyPainter extends CustomPainter {
  ValueNotifier<Offset> value;
  MyPainter(this.value):super(repaint: value);
  @override
  void paint(Canvas canvas, Size size) {
  }
  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return false;
  }
}

在手势的回调中,通过画笔中构造参数 repaint 的通知机制,进行手势事件的传递。
上面的代码中,只要触摸就会触发画笔的重绘制,达到手势响应的效果。

响应动画的绘制

除了静态的画面,实际的需求中也有动画,比如点击柱状图产生渐宽效果,进度条的渐进等效果。

动画的驱动可以通过我们熟知的 AnimationController 完成,和手势响应相似,画笔与动画的关联也是通过画笔构造参数的 repaint,响应的流程如下:

Flutter 必知必会系列 —— 随心所欲的自定义绘制最终章

当然了,除了机制上的支持,动画最重要的还是动画的路径是什么? 通过路径的API我们可以拿到动画进度下的某一段路径是什么。

Path extractPath(double start, double end, {bool startWithMoveTo = true})

end 就是动画进度与路径总长度的乘积

路径的长度在 PathMetric 度量中,结合两者就可以实现动画与路径的映射。

除此之外,动画的位置坐标点可以通过正切拿到。如下代码:

Tangent? tangent = element.getTangentForOffset(
    element.length * animation.value);

绘制的色彩

绘制的色彩比较麻烦,需要知道颜色混合、遮罩、模糊等。 大家可以通过设置画笔的属性实现。

属性 类型 作用
color Color 画笔颜色
colorFilter ColorFilter? 颜色过滤器,设置颜色混合模式、颜色矩阵
imageFilter ImageFilter? 图片过滤器,设置图片形状、高斯模糊
invertColors bool 是否颜色反转
shader Shader 着色器

通过这些色彩的设置,我们可以实现意想不到的效果。

着色器系列

linear sweep linear radial
Flutter 必知必会系列 —— 随心所欲的自定义绘制最终章
Flutter 必知必会系列 —— 随心所欲的自定义绘制最终章
Flutter 必知必会系列 —— 随心所欲的自定义绘制最终章
Flutter 必知必会系列 —— 随心所欲的自定义绘制最终章

颜色混合系列

Flutter 必知必会系列 —— 随心所欲的自定义绘制最终章

总结

通过这四篇文章,自定义绘制算是告一段落,绘制的内容可能简单也可能复杂,可能需要数学,也可能需要色彩。笔和纸告诉了我们,接下来就等大家发挥咯~。

目前已经完成了三棵树 Flutter 必知必会系列 —— 三棵树最终章 和 自定义绘制的阅读记录,下个系列开始路由吧~~