Flutter 必知必会系列 —— 随心所欲的自定义绘制最终章
这是我参与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;
}
}
效果如下:
粉色的就是背景画笔的内容,蓝色的是 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
,响应的流程如下:
当然了,除了机制上的支持,动画最重要的还是动画的路径是什么? 通过路径的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 必知必会系列 —— 三棵树最终章 和 自定义绘制的阅读记录,下个系列开始路由吧~~