CustomPainter — 什么是paint和shouldRepaint方法在Flutter中

作者: IT Sectr 发布日期: 2026-07-22 阅读时间: 10 分钟

CustomPainter — 是Flutter SDK中的一个抽象类,为开发人员提供了在Canvas上绘制自定义2D图形的完全控制。开发人员创建CustomPainter子类并实现两个必需方法:paint(Canvas, Size)和shouldRepaint(covariant CustomPainter oldDelegate)。根据Flutter API Documentation(2026),CustomPainter与CustomPaint小部件一起使用,后者在动画的每一帧或状态变化时将绘制委托给传递的CustomPainter对象。

要点

  • CustomPainter — Flutter抽象类,通过paint和shouldRepaint方法定义Canvas上的自定义绘制
  • CustomPaint — 接受CustomPainter作为painter或foregroundPainter参数并在屏幕上显示结果的小部件
  • Canvas — 提供绘图方法的对象(drawLine、drawCircle、drawPath、drawRect、drawArc等)
  • shouldRepaint — 优化方法,确定在更新时是否需要重新绘制对象
  • size — 绘图区域的大小,从父级小部件传递到paint(Canvas canvas, Size size)

什么是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可以用作具有装饰性背景或覆盖层的容器,而不会丢失在内部嵌入常规小部件的能力。

Flutter中的Canvas和Paint

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变换

Canvas支持坐标系的变换:translate(double dx, double dy)移动坐标原点;rotate(double radians)旋转系统;scale(double sx, double sy)缩放;skew(double sx, double sy)倾斜。在变换之前,建议通过save()保存Canvas状态,完成后通过restore()恢复。这可以防止在后续的paint调用中累积变换。

paint和shouldRepaint方法

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时重绘。

在Dart中使用CustomPainter进行自定义绘制的示例

让我们创建GaugePainter——一个带有动画填充的自定义圆形指示器。CustomPainter接收当前值(progress从0.0到1.0)并绘制带有渐变的弧线。在shouldRepaint方法中,比较progress值以进行优化。CustomPaint放置在小部件树中,带有size参数以限制绘制区域。

dart
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进行复杂形状的CustomPainter

该示例使用Path绘制波形图。Path允许从线条、贝塞尔曲线和弧线构建任意轮廓。构建后,轮廓用颜色填充或用线勾勒。在示例中,图形基于模拟波形的一系列点构建。

dart
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;
    }
}

ForegroundPainter和backgroundPainter:区别

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性能优化

主要规则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。

常见问题

Flutter中CustomPainter和Canvas有什么区别?

CustomPainter — 是一个契约类,定义了在Canvas上绘制什么以及如何绘制。Canvas — 是提供方法(drawCircle、drawLine等)的绘制对象本身。CustomPainter通过paint方法接收Canvas并管理其使用。一个Canvas可以在不同时间被不同的CustomPainter使用。

可以在没有CustomPaint的情况下使用CustomPainter吗?

不能,CustomPainter在没有CustomPaint的情况下没有意义。CustomPaint是调用CustomPainter的paint方法并在屏幕上显示结果的小部件。没有CustomPaint,Canvas将不会被创建,paint也不会被执行。但是,您可以实现自己的带有Canvas的RenderObject,但这是较低级别的方法。

如何制作CustomPainter动画?

使用AnimationController和Ticker,通过构造函数参数将动画值传递给CustomPainter。当值更改时,在包含CustomPaint的小部件中调用setState(),这将触发shouldRepaint。如果shouldRepaint返回true,系统将为新的动画帧调用paint。

为什么CustomPainter在数据更改时不更新?

原因几乎总是shouldRepaint返回false。检查oldDelegate的字段是否与新的值正确比较。如果使用可变对象,shouldRepaint可能不会注意到更改,因为对象的引用保持不变。使用==或不可变对象进行正确比较。

可以在ReorderableListView中使用CustomPainter进行动画吗?

可以,CustomPainter用于在复杂列表中绘制自定义装饰。但是,请注意在重新排序元素时,ListView内部的RepaintBoundary会缓存每个元素的绘制。CustomPainter应该轻量级,并且不要在paint中包含重操作,以免拖慢拖拽动画。

总结

  • CustomPainter — Flutter抽象类,用于使用paint和shouldRepaint方法进行自定义2D绘制
  • CustomPaint — 接受painter(背景)和foregroundPainter(前景)以显示图形的小部件
  • Canvas — 具有绘制方法的对象:drawLine、drawCircle、drawPath、drawRect、drawArc等
  • Paint — 定义绘制样式的类:颜色、粗细、样式(fill/stroke)、透明度和混合模式
  • shouldRepaint — 优化方法,仅在影响绘制的字段更改时返回true
  • 禁止在paint内部创建对象——所有Paint、Path、Rect必须为60 fps进行缓存
  • RepaintBoundary — 用于将CustomPainter与上层小部件树的重绘隔离开的小部件

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读