CustomPainter é uma classe abstrata do Flutter SDK que dá aos desenvolvedores controle total sobre a renderização de gráficos 2D personalizados no Canvas. O desenvolvedor cria uma subclasse de CustomPainter e implementa dois métodos obrigatórios: paint(Canvas, Size) e shouldRepaint(covariant CustomPainter oldDelegate). De acordo com a Documentação da API Flutter (2026), o CustomPainter é usado em conjunto com o widget CustomPaint, que delega a renderização ao objeto CustomPainter fornecido em cada quadro de animação ou mudança de estado.
Principais Pontos
CustomPainter é uma classe base do Flutter para implementar gráficos 2D personalizados. Ela substitui a abordagem de sobrescrever onDraw no Android, fornecendo um modelo de renderização mais flexível e declarativo. O desenvolvedor cria uma subclasse de CustomPainter, sobrescreve o método paint onde recebe Canvas e Size, e executa todas as operações de desenho no Canvas. O widget CustomPaint gerencia a chamada de paint e integra o resultado na árvore de widgets.
A principal diferença entre CustomPainter e o desenho direto através do Canvas é a separação da lógica de renderização (paint) e do gatilho de redesenho (shouldRepaint). O sistema Flutter chama shouldRepaint toda vez que o widget pai muda. Se shouldRepaint retornar false, paint não é chamado, economizando recursos. Este mecanismo é semelhante ao shouldRebuild no RenderObject e faz parte da arquitetura do Flutter com suas três árvores (Widget, Element, RenderObject).
CustomPaint é um widget que aceita os parâmetros painter (camada de fundo) e foregroundPainter (camada frontal). Ambos os parâmetros aceitam objetos CustomPainter. O painter de fundo é desenhado antes do widget filho do CustomPaint, e o foregroundPainter depois. Isso permite usar CustomPaint como um contêiner com fundo decorativo ou sobreposição sem perder a capacidade de incorporar widgets comuns dentro dele.
Canvas é o objeto central de desenho no Flutter, que encapsula a superfície gráfica. Ele fornece cerca de 40 métodos de desenho: drawLine, drawCircle, drawRect, drawPath, drawArc, drawImage, drawPicture, drawPoints e outros. O Canvas no Flutter corresponde a uma classe similar no Android Canvas, mas adaptado à arquitetura Dart e ao Flutter Engine. O Canvas opera em um sistema de coordenadas onde a origem (0, 0) é o canto superior esquerdo.
Paint — não confundir com o método paint() — é uma classe que define o estilo de desenho: cor (color), espessura da linha (strokeWidth), estilo (style: fill ou stroke), opacidade, modo de mesclagem (blendMode) e máscara de suavização. Paint é configurado antes do desenho e passado para os métodos do Canvas. Importante: Paint é um objeto mutável, mas é recomendado criá-lo uma vez e reutilizá-lo, alterando apenas os parâmetros necessários.
| Método do Canvas | Propósito | Parâmetros |
|---|---|---|
| drawCircle | Desenha um círculo | Offset center, double radius, Paint paint |
| drawLine | Desenha um segmento de reta | Offset p1, Offset p2, Paint paint |
| drawRect | Desenha um retângulo | Rect rect, Paint paint |
| drawPath | Desenha um traçado arbitrário | Path path, Paint paint |
| drawArc | Desenha um arco ou setor | Rect rect, double startAngle, double sweepAngle, bool useCenter, Paint paint |
| drawRRect | Retângulo com cantos arredondados | RRect rrect, Paint paint |
| drawOval | Desenha uma oval | Rect rect, Paint paint |
| drawImage | Desenha uma imagem a partir de Image | Image image, Offset p, Paint paint |
Canvas suporta transformações do sistema de coordenadas: translate(double dx, double dy) desloca a origem; rotate(double radians) rotaciona o sistema; scale(double sx, double sy) dimensiona; skew(double sx, double sy) inclina. Antes de uma transformação, é recomendado salvar o estado do Canvas via save() e restaurá-lo após a conclusão via restore(). Isso evita o acúmulo de transformações em chamadas subsequentes de paint.
paint(Canvas canvas, Size size) é o método obrigatório que implementa toda a lógica de desenho. O parâmetro canvas é a superfície gráfica para desenho. O parâmetro size é o tamanho da área alocada para desenho (largura e altura em pixels lógicos). O Canvas é pré-configurado para que a origem (0,0) corresponda ao canto superior esquerdo da área de desenho. Todas as coordenadas são especificadas em relação a esta origem.
shouldRepaint(covariant CustomPainter oldDelegate) é um método obrigatório que otimiza a frequência de redesenho. O Flutter chama shouldRepaint em cada atualização da árvore de widgets que afeta o CustomPainter. Se o método retornar false, a imagem já renderizada é considerada válida e paint não é chamado. Uma implementação típica é: return oldDelegate.someField != someField — comparação dos campos do painter antigo e novo para determinar a necessidade de redesenho.
Para imagens animadas ou em constante mudança (relógios, indicadores de carregamento, gráficos) shouldRepaint deve retornar true a cada mudança de estado. Se o objeto é estático (logotipo, ícone, fundo), shouldRepaint retorna false após a primeira renderização. O Flutter armazena automaticamente em cache o resultado de paint e redesenha apenas quando o tamanho, a opacidade ou a chamada shouldRepaint mudam.
Vamos criar GaugePainter — um medidor circular personalizado com preenchimento animado. CustomPainter aceita o valor atual (progresso de 0.0 a 1.0) e desenha um arco com gradiente. O método shouldRepaint compara o valor do progresso para otimização. CustomPaint é colocado na árvore de widgets com o parâmetro size para limitar a área de desenho.
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;
}
}
O exemplo desenha um gráfico de onda usando Path. Path permite construir contornos arbitrários a partir de linhas, curvas Bezier e arcos. Após construir o contorno, ele é preenchido com cor ou delineado com uma linha. No exemplo, o gráfico é construído a partir de uma matriz de pontos simulando uma 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 (fundo) é desenhado antes do widget filho do CustomPaint. Se CustomPaint tem um widget filho (parâmetro child), o painter de fundo é exibido abaixo dele. Isso é adequado para decorações: barras de progresso, padrões de fundo, marcas d'água. foregroundPainter é desenhado após o widget filho, ou seja, sobre ele. É usado para sobreposições: realces, máscaras, réguas de medição, anotações.
A diferença entre painter e foregroundPainter é crítica para a ordem Z. Se você precisa desenhar algo atrás do conteúdo — use painter. Se sobre o conteúdo — foregroundPainter. Em alguns casos, você pode usar ambos os painters simultaneamente: fundo via painter, sobreposição de rótulo via foregroundPainter. Isso permite separar a lógica de diferentes camadas gráficas em duas classes CustomPainter separadas.
O parâmetro size no método paint é o mesmo para ambos os painters e é determinado pelo tamanho da área alocada para CustomPaint. O tamanho pode ser definido explicitamente através do parâmetro size no CustomPaint ou implicitamente através do contêiner pai. Se o tamanho for Size.zero, paint não é chamado — o Flutter ignora a renderização para uma área de tamanho zero. Esse comportamento deve ser considerado ao animar a aparição de elementos.
A regra principal de desempenho do CustomPainter é não criar objetos dentro do método paint. Canvas, Paint, Path, Rect, Offset e outros objetos de desenho devem ser criados no construtor do CustomPainter ou armazenados em cache. Criar objetos dentro de paint leva a alocações em cada quadro, o que a 60 quadros por segundo causa coleta de lixo frequente e travamentos na animação (jank).
O cache de Path acelera significativamente a renderização de traçados complexos repetidos. Se o traçado não muda entre quadros (por exemplo, uma grade de fundo), crie o Path uma vez e reutilize-o várias vezes. RepaintBoundary é um widget invólucro que impede o redesenho do CustomPainter quando a árvore de widgets superior muda. Se o CustomPainter é estático, envolva-o em um RepaintBoundary para isolamento.
Para CustomPainter animados com redesenho frequente, use Ticker ou AnimationController com shouldRepaint retornando true a cada mudança do valor animado. É ideal comparar o campo animado em shouldRepaint em vez de redesenhar em cada quadro desnecessariamente. Cache de Rect para gradientes complexos: se as cores do gradiente não mudam, você pode armazenar em cache o Shader via Paint.shader.
Perguntas Frequentes
CustomPainter é uma classe de contrato que define o que e como desenhar no Canvas. Canvas é o próprio objeto de desenho, que fornece métodos (drawCircle, drawLine, etc.). CustomPainter recebe o Canvas através do método paint e controla seu uso. Um mesmo Canvas pode ser usado por diferentes CustomPainter em momentos diferentes.
Não, CustomPainter não faz sentido sem CustomPaint. CustomPaint é o widget que chama o método paint do CustomPainter e exibe o resultado na tela. Sem CustomPaint, o Canvas não será criado e paint não será executado. Você pode, no entanto, implementar seu próprio RenderObject com Canvas, mas esta é uma abordagem de nível inferior.
Use um AnimationController com um Ticker, passando o valor animado para CustomPainter através dos parâmetros do construtor. Quando o valor mudar, chame setState() no widget que contém CustomPaint, o que dispara a chamada shouldRepaint. Se shouldRepaint retornar true, o sistema chama paint para o novo quadro de animação.
A causa é quase sempre shouldRepaint retornando false. Verifique se os campos do oldDelegate estão sendo comparados corretamente com os novos valores. Se um objeto mutável for usado, shouldRepaint pode não detectar as alterações porque a referência ao objeto permanece a mesma. Use == ou objetos imutáveis para comparação correta.
Sim, CustomPainter é usado para renderizar decorações personalizadas em listas complexas. No entanto, observe que ao reordenar itens, o RepaintBoundary dentro de ListView armazena em cache a renderização de cada item. CustomPainter deve ser leve e não conter operações pesadas em paint para não retardar a animação de arrasto.
Resumo
Vamos desenvolver um aplicativo móvel chave na mão
A IT Sectr cria aplicativos para iOS e Android para startups e empresas desde 2017. Nós vamos aconselhá-lo e propor a melhor solução.
Leia também