CustomPainter es una clase abstracta del SDK de Flutter que brinda a los desarrolladores control total sobre el renderizado de gráficos 2D personalizados en Canvas. El desarrollador crea una subclase de CustomPainter e implementa dos métodos obligatorios: paint(Canvas, Size) y shouldRepaint(covariant CustomPainter oldDelegate). Según la Documentación de la API de Flutter (2026), CustomPainter se utiliza junto con el widget CustomPaint, que delega el renderizado al objeto CustomPainter proporcionado en cada fotograma de animación o cambio de estado.
Puntos Clave
CustomPainter es una clase base de Flutter para implementar gráficos 2D personalizados. Reemplaza el enfoque de sobrescribir onDraw en Android, proporcionando un modelo de renderizado más flexible y declarativo. El desarrollador crea una subclase de CustomPainter, sobrescribe el método paint donde recibe Canvas y Size, y realiza todas las operaciones de dibujo en Canvas. El widget CustomPaint gestiona la llamada a paint e integra el resultado en el árbol de widgets.
La diferencia clave entre CustomPainter y el dibujo directo a través de Canvas es la separación de la lógica de renderizado (paint) y el desencadenante de redibujado (shouldRepaint). El sistema Flutter llama a shouldRepaint cada vez que el widget padre cambia. Si shouldRepaint devuelve false, no se llama a paint, lo que ahorra recursos. Este mecanismo es similar a shouldRebuild en RenderObject y forma parte de la arquitectura de Flutter con sus tres árboles (Widget, Element, RenderObject).
CustomPaint es un widget que acepta los parámetros painter (capa de fondo) y foregroundPainter (capa frontal). Ambos parámetros aceptan objetos CustomPainter. El painter de fondo se dibuja antes del widget hijo de CustomPaint, y el foregroundPainter después. Esto permite usar CustomPaint como contenedor con fondo decorativo o superposición sin perder la posibilidad de incrustar widgets normales en su interior.
Canvas es el objeto central de dibujo en Flutter, que encapsula la superficie gráfica. Proporciona alrededor de 40 métodos de dibujo: drawLine, drawCircle, drawRect, drawPath, drawArc, drawImage, drawPicture, drawPoints y otros. Canvas en Flutter corresponde a una clase similar en Android Canvas pero está adaptado a la arquitectura Dart y Flutter Engine. Canvas opera en un sistema de coordenadas donde el origen (0, 0) es la esquina superior izquierda.
Paint — que no debe confundirse con el método paint() — es una clase que define el estilo de dibujo: color, grosor de línea, estilo (fill o stroke), opacidad, modo de mezcla y máscara de suavizado. Paint se configura antes de dibujar y se pasa a los métodos de Canvas. Importante: Paint es un objeto mutable, pero se recomienda crearlo una vez y reutilizarlo, cambiando solo los parámetros necesarios.
| Método de Canvas | Propósito | Parámetros |
|---|---|---|
| drawCircle | Dibuja un círculo | Offset center, double radius, Paint paint |
| drawLine | Dibuja un segmento de línea | Offset p1, Offset p2, Paint paint |
| drawRect | Dibuja un rectángulo | Rect rect, Paint paint |
| drawPath | Dibuja un trazado arbitrario | Path path, Paint paint |
| drawArc | Dibuja un arco o sector | Rect rect, double startAngle, double sweepAngle, bool useCenter, Paint paint |
| drawRRect | Rectángulo con esquinas redondeadas | RRect rrect, Paint paint |
| drawOval | Dibuja un óvalo | Rect rect, Paint paint |
| drawImage | Dibuja una imagen desde Image | Image image, Offset p, Paint paint |
Canvas admite transformaciones del sistema de coordenadas: translate(double dx, double dy) desplaza el origen; rotate(double radians) rota el sistema; scale(double sx, double sy) escala; skew(double sx, double sy) sesga. Antes de una transformación, se recomienda guardar el estado de Canvas mediante save() y restaurarlo después mediante restore(). Esto evita la acumulación de transformaciones en llamadas posteriores a paint.
paint(Canvas canvas, Size size) es el método obligatorio que implementa toda la lógica de dibujo. El parámetro canvas es la superficie gráfica para dibujar. El parámetro size es el tamaño del área asignada para el dibujo (ancho y alto en píxeles lógicos). Canvas está preconfigurado para que el origen (0,0) corresponda a la esquina superior izquierda del área de dibujo. Todas las coordenadas se especifican en relación con este origen.
shouldRepaint(covariant CustomPainter oldDelegate) es un método obligatorio que optimiza la frecuencia de redibujado. Flutter llama a shouldRepaint en cada actualización del árbol de widgets que afecta a CustomPainter. Si el método devuelve false, la imagen ya renderizada se considera válida y no se llama a paint. Una implementación típica es: return oldDelegate.someField != someField — comparación de los campos del painter antiguo y nuevo para determinar la necesidad de redibujar.
Para imágenes animadas o en constante cambio (relojes, indicadores de carga, gráficos) shouldRepaint debe devolver true en cada cambio de estado. Si el objeto es estático (logotipo, icono, fondo), shouldRepaint devuelve false después del primer renderizado. Flutter almacena automáticamente en caché el resultado de paint y vuelve a dibujar solo cuando cambia el tamaño, la opacidad o la llamada a shouldRepaint.
Creemos GaugePainter — un indicador circular personalizado con relleno animado. CustomPainter acepta el valor actual (progreso de 0.0 a 1.0) y dibuja un arco con degradado. El método shouldRepaint compara el valor de progreso para optimizar. CustomPaint se coloca en el árbol de widgets con el parámetro size para limitar el área de dibujo.
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;
}
}
El ejemplo dibuja un gráfico de onda utilizando Path. Path permite construir contornos arbitrarios a partir de líneas, curvas Bezier y arcos. Después de construir el contorno, se rellena con color o se delinea con una línea. En el ejemplo, el gráfico se construye a partir de una matriz de puntos que simulan una forma de onda.
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 (fondo) se dibuja antes del widget hijo de CustomPaint. Si CustomPaint tiene un widget hijo (parámetro child), el painter de fondo se muestra debajo de él. Esto es adecuado para decoraciones: barras de progreso, patrones de fondo, marcas de agua. foregroundPainter se dibuja después del widget hijo, es decir, encima de él. Se usa para superposiciones: reflejos, máscaras, reglas de medición, anotaciones.
La diferencia entre painter y foregroundPainter es crítica para el orden Z. Si necesita dibujar algo detrás del contenido — use painter. Si encima del contenido — foregroundPainter. En algunos casos, puede usar ambos painters simultáneamente: fondo mediante painter, superposición de etiqueta mediante foregroundPainter. Esto permite separar la lógica de diferentes capas gráficas en dos clases CustomPainter separadas.
El parámetro size en el método paint es el mismo para ambos painters y está determinado por el tamaño del área asignada para CustomPaint. El tamaño se puede establecer explícitamente mediante el parámetro size en CustomPaint o implícitamente a través del contenedor padre. Si el tamaño es Size.zero, no se llama a paint — Flutter omite el renderizado para un área de tamaño cero. Este comportamiento debe tenerse en cuenta al animar la aparición de elementos.
La regla principal del rendimiento de CustomPainter es no crear objetos dentro del método paint. Canvas, Paint, Path, Rect, Offset y otros objetos de dibujo deben crearse en el constructor de CustomPainter o almacenarse en caché. Crear objetos dentro de paint provoca asignaciones en cada fotograma, lo que a 60 fotogramas por segundo causa recolección de basura frecuente y tirones en la animación (jank).
El almacenamiento en caché de Path acelera significativamente el renderizado de trazados complejos repetidos. Si el trazado no cambia entre fotogramas (por ejemplo, una cuadrícula de fondo), cree el Path una vez y reutilícelo múltiples veces. RepaintBoundary es un widget envoltorio que evita el redibujado de CustomPainter cuando cambia el árbol de widgets superior. Si CustomPainter es estático, envuélvalo en un RepaintBoundary para aislarlo.
Para CustomPainter animados con redibujados frecuentes, use Ticker o AnimationController con shouldRepaint devolviendo true en cada cambio del valor animado. Es óptimo comparar el campo animado en shouldRepaint en lugar de volver a dibujar en cada fotograma innecesariamente. Almacenamiento en caché de Rect para degradados complejos: si los colores del degradado no cambian, puede almacenar en caché el Shader mediante Paint.shader.
Preguntas Frecuentes
CustomPainter es una clase de contrato que define qué y cómo dibujar en Canvas. Canvas es el propio objeto de dibujo, que proporciona métodos (drawCircle, drawLine, etc.). CustomPainter recibe Canvas a través del método paint y controla su uso. Un mismo Canvas puede ser utilizado por diferentes CustomPainter en distintos momentos.
No, CustomPainter no tiene sentido sin CustomPaint. CustomPaint es el widget que llama al método paint de CustomPainter y muestra el resultado en pantalla. Sin CustomPaint, no se creará Canvas ni se ejecutará paint. Sin embargo, puede implementar su propio RenderObject con Canvas, pero este es un enfoque de nivel inferior.
Use un AnimationController con un Ticker, pasando el valor animado a CustomPainter a través de los parámetros del constructor. Cuando el valor cambie, llame a setState() en el widget que contiene CustomPaint, lo que desencadena la llamada a shouldRepaint. Si shouldRepaint devuelve true, el sistema llama a paint para el nuevo fotograma de animación.
La causa es casi siempre shouldRepaint devolviendo false. Verifique si los campos de oldDelegate se comparan correctamente con los nuevos valores. Si se usa un objeto mutable, shouldRepaint puede no detectar los cambios porque la referencia al objeto sigue siendo la misma. Use == u objetos inmutables para una comparación correcta.
Sí, CustomPainter se usa para renderizar decoraciones personalizadas en listas complejas. Sin embargo, tenga en cuenta que al reordenar elementos, RepaintBoundary dentro de ListView almacena en caché el renderizado de cada elemento. CustomPainter debe ser ligero y no contener operaciones pesadas en paint para no ralentizar la animación de arrastre.
Resumen
Desarrollaremos una aplicación móvil llave en mano
IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.
Lea también