CustomPainter — 是Flutter SDK中的一个抽象类,为开发人员提供了在Canvas上绘制自定义2D图形的完全控制。开发人员创建CustomPainter子类并实现两个必需方法:paint(Canvas, Size)和shouldRepaint(covariant CustomPainter oldDelegate)。根据Flutter API Documentation(2026),CustomPainter与CustomPaint小部件一起使用,后者在动画的每一帧或状态变化时将绘制委托给传递的CustomPainter对象。
要点
CustomPainter — 是Flutter中用于实现任意2D图形的基础类。它取代了在Android中重写onDraw的方法,提供了更灵活和声明式的绘制模型。开发人员创建CustomPainter子类,重写paint方法,在其中接收Canvas和Size,并在Canvas上执行所有绘图操作。CustomPaint小部件管理paint的调用并将结果集成到小部件树中。
CustomPainter与通过Canvas直接绘制之间的关键区别——绘制逻辑(paint)和重绘触发器(shouldRepaint)的分离。每当父级小部件发生变化时,Flutter系统都会调用shouldRepaint。如果shouldRepaint返回false,则不调用paint,从而节省资源。这种机制类似于RenderObject中的shouldRebuild,是Flutter架构及其三个树(Widget、Element、RenderObject)的一部分。
CustomPaint — 是一个接受painter(背景层)和foregroundPainter(前景层)参数的小部件。两个参数都接受CustomPainter对象。背景painter在CustomPaint的子级之前绘制,而foregroundPainter在其之后绘制。这使得CustomPaint可以用作具有装饰性背景或覆盖层的容器,而不会丢失在内部嵌入常规小部件的能力。
Canvas — 是Flutter中用于绘制的中心对象,封装了图形表面。它提供了大约40种绘制方法:drawLine、drawCircle、drawRect、drawPath、drawArc、drawImage、drawPicture、drawPoints等。Flutter中的Canvas对应于Android Canvas中的类似类,但已适应Dart和Flutter Engine的架构。Canvas在坐标系中工作,其中原点(0,0)位于左上角。
Paint — 不要与paint()方法混淆——是一个定义绘制样式的类:颜色(color)、线条宽度(strokeWidth)、样式(style:fill或stroke)、透明度、混合模式(blendMode)和抗锯齿遮罩。Paint在绘制前进行配置,并传递给Canvas的方法。重要提示:Paint是一个可变对象,但建议创建一次并重复使用,仅更改必要的参数。
| Canvas方法 | 用途 | 参数 |
|---|---|---|
| drawCircle | 绘制圆形 | Offset center, double radius, Paint paint |
| drawLine | 绘制线段 | Offset p1, Offset p2, Paint paint |
| drawRect | 绘制矩形 | Rect rect, Paint paint |
| drawPath | 绘制任意路径 | Path path, Paint paint |
| drawArc | 绘制弧线或扇形 | Rect rect, double startAngle, double sweepAngle, bool useCenter, Paint paint |
| drawRRect | 圆角矩形 | RRect rrect, Paint paint |
| drawOval | 绘制椭圆 | Rect rect, Paint paint |
| drawImage | 从Image绘制图像 | Image image, Offset p, Paint paint |
Canvas支持坐标系的变换:translate(double dx, double dy)移动坐标原点;rotate(double radians)旋转系统;scale(double sx, double sy)缩放;skew(double sx, double sy)倾斜。在变换之前,建议通过save()保存Canvas状态,完成后通过restore()恢复。这可以防止在后续的paint调用中累积变换。
paint(Canvas canvas, Size size) — 必需方法,在其中实现所有绘制逻辑。参数canvas——用于绘制的图形表面。参数size——分配给绘制的区域大小(以逻辑像素为单位的宽度和高度)。Canvas预先配置好,使得坐标原点(0,0)对应于绘制区域的左上角。所有坐标都相对于此原点设置。
shouldRepaint(covariant CustomPainter oldDelegate) — 必需方法,优化重绘频率。Flutter在每次影响CustomPainter的小部件树更新时调用shouldRepaint。如果该方法返回false,则已绘制的图像被视为有效,并且不调用paint。典型实现:return oldDelegate.someField != someField——比较旧和新painter的字段以确定是否需要重绘。
对于动画或不断变化的图像(时钟、加载指示器、图表),shouldRepaint应在每次状态更改时返回true。如果对象是静态的(徽标、图标、背景),shouldRepaint在首次绘制后返回false。Flutter会自动缓存paint的结果,并仅在大小、透明度更改或调用shouldRepaint时重绘。
让我们创建GaugePainter——一个带有动画填充的自定义圆形指示器。CustomPainter接收当前值(progress从0.0到1.0)并绘制带有渐变的弧线。在shouldRepaint方法中,比较progress值以进行优化。CustomPaint放置在小部件树中,带有size参数以限制绘制区域。
class GaugePainter extends CustomPainter {
final double progress;
final Color startColor;
final Color endColor;
GaugePainter({
required this.progress,
this.startColor = Colors.blue,
this.endColor = Colors.cyan,
});
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2,
size.height / 2);
final radius = center.dx - 12;
final bgPaint = Paint()
..color = Colors.grey.withOpacity(0.3)
..style = PaintingStyle.stroke
..strokeWidth = 16
..strokeCap = StrokeCap.round;
final progressPaint = Paint()
..shader = LinearGradient(
colors: [startColor, endColor],
).createShader(
Rect.fromCircle(
center, radius))
..style = PaintingStyle.stroke
..strokeWidth = 16
..strokeCap = StrokeCap.round;
canvas.drawCircle(center, radius, bgPaint);
canvas.drawArc(
Rect.fromCircle(center, radius),
-math.pi / 2,
2 * math.pi * progress,
false, progressPaint);
}
@override
bool shouldRepaint(
GaugePainter oldDelegate) {
return oldDelegate.progress != progress;
}
}
该示例使用Path绘制波形图。Path允许从线条、贝塞尔曲线和弧线构建任意轮廓。构建后,轮廓用颜色填充或用线勾勒。在示例中,图形基于模拟波形的一系列点构建。
class WavePainter extends CustomPainter {
final List<double> values;
WavePainter({required this.values});
@override
void paint(Canvas canvas, Size size) {
final path = Path();
final dx = size.width /
(values.length - 1);
path.moveTo(0, size.height);
for (var i = 0; i < values.length; i++) {
final x = i * dx;
final y = size.height *
(1 - values[i]);
path.lineTo(x, y);
}
path.lineTo(size.width, size.height);
path.close();
final fillPaint = Paint()
..color = Colors.blue.withOpacity(0.3)
..style = PaintingStyle.fill;
canvas.drawPath(path, fillPaint);
}
@override
bool shouldRepaint(
WavePainter oldDelegate) {
return oldDelegate.values != values;
}
}
painter(背景)在CustomPaint的子级之前绘制。如果CustomPaint有子级(child参数),则背景painter显示在其下方。这适用于装饰:进度条、背景图案、水印。foregroundPainter在CustomPaint的子级之后绘制,即在其上方。用于叠加层:高光、遮罩、测量标尺、注释。
区别在于painter和foregroundPainter之间的Z-order至关重要。如果需要在内容后面绘制内容——使用painter。如果在内容上方——使用foregroundPainter。在某些情况下,可以同时使用两个painter:通过painter绘制背景,通过foregroundPainter绘制标签覆盖层。这允许将不同图形层的逻辑分离到两个单独的CustomPainter类中。
size参数在paint方法中对于两个painter是相同的,由分配给CustomPaint的区域大小确定。大小可以通过CustomPaint中的size参数显式设置,或通过父容器隐式设置。如果size等于Size.zero,则不调用paint——Flutter会跳过零区域的绘制。在元素的动画出现时应考虑此行为。
主要规则CustomPainter的性能——不要在paint方法内部创建对象。Canvas、Paint、Path、Rect、Offset和其他绘图对象应在CustomPainter构造函数中创建或缓存。在paint内部创建对象会导致每帧的内存分配,在每秒60帧的情况下会导致频繁的垃圾回收和动画卡顿(jank)。
缓存Path可以显著加速重复复杂轮廓的绘制。如果轮廓在帧之间没有变化(例如背景网格),请一次创建Path并多次使用。RepaintBoundary——一个包装小部件,可在上层小部件树更改时防止CustomPainter重绘。如果CustomPainter是静态的,将其包装在RepaintBoundary中以进行隔离。
对于频繁重绘的动画CustomPainter,使用带有shouldRepaint的Ticker或AnimationController,在动画值更改时返回true。最佳做法是在shouldRepaint中比较动画字段,而不是在每一帧不必要地重绘。用于复杂渐变的Rect缓存:如果渐变不改变颜色,可以通过Paint.shader缓存Shader。
常见问题
CustomPainter — 是一个契约类,定义了在Canvas上绘制什么以及如何绘制。Canvas — 是提供方法(drawCircle、drawLine等)的绘制对象本身。CustomPainter通过paint方法接收Canvas并管理其使用。一个Canvas可以在不同时间被不同的CustomPainter使用。
不能,CustomPainter在没有CustomPaint的情况下没有意义。CustomPaint是调用CustomPainter的paint方法并在屏幕上显示结果的小部件。没有CustomPaint,Canvas将不会被创建,paint也不会被执行。但是,您可以实现自己的带有Canvas的RenderObject,但这是较低级别的方法。
使用AnimationController和Ticker,通过构造函数参数将动画值传递给CustomPainter。当值更改时,在包含CustomPaint的小部件中调用setState(),这将触发shouldRepaint。如果shouldRepaint返回true,系统将为新的动画帧调用paint。
原因几乎总是shouldRepaint返回false。检查oldDelegate的字段是否与新的值正确比较。如果使用可变对象,shouldRepaint可能不会注意到更改,因为对象的引用保持不变。使用==或不可变对象进行正确比较。
可以,CustomPainter用于在复杂列表中绘制自定义装饰。但是,请注意在重新排序元素时,ListView内部的RepaintBoundary会缓存每个元素的绘制。CustomPainter应该轻量级,并且不要在paint中包含重操作,以免拖慢拖拽动画。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。