CustomPainter est une classe abstraite du SDK Flutter qui offre aux développeurs un contrôle total sur le rendu de graphiques 2D personnalisés sur Canvas. Le développeur crée une sous-classe de CustomPainter et implémente deux méthodes obligatoires : paint(Canvas, Size) et shouldRepaint(covariant CustomPainter oldDelegate). Selon la documentation de l'API Flutter (2026), CustomPainter est utilisé conjointement avec le widget CustomPaint, qui délègue le rendu à l'objet CustomPainter fourni à chaque image d'animation ou changement d'état.
Points Clés
CustomPainter est une classe de base de Flutter pour implémenter des graphiques 2D personnalisés. Elle remplace l'approche de surcharge de onDraw dans Android, offrant un modèle de rendu plus flexible et déclaratif. Le développeur crée une sous-classe de CustomPainter, surcharge la méthode paint où il reçoit Canvas et Size, et exécute toutes les opérations de dessin sur Canvas. Le widget CustomPaint gère l'appel à paint et intègre le résultat dans l'arbre de widgets.
La différence clé entre CustomPainter et le dessin direct via Canvas est la séparation de la logique de rendu (paint) et du déclencheur de redessin (shouldRepaint). Le système Flutter appelle shouldRepaint chaque fois que le widget parent change. Si shouldRepaint renvoie false, paint n'est pas appelé, ce qui économise des ressources. Ce mécanisme est similaire à shouldRebuild dans RenderObject et fait partie de l'architecture Flutter avec ses trois arbres (Widget, Element, RenderObject).
CustomPaint est un widget qui accepte les paramètres painter (couche d'arrière-plan) et foregroundPainter (couche de premier plan). Les deux paramètres acceptent des objets CustomPainter. Le painter d'arrière-plan est dessiné avant le widget enfant de CustomPaint, et le foregroundPainter après. Cela permet d'utiliser CustomPaint comme conteneur avec un fond décoratif ou une superposition sans perdre la possibilité d'intégrer des widgets normaux à l'intérieur.
Canvas est l'objet central de dessin dans Flutter, encapsulant la surface graphique. Il fournit environ 40 méthodes de dessin : drawLine, drawCircle, drawRect, drawPath, drawArc, drawImage, drawPicture, drawPoints et autres. Canvas dans Flutter correspond à une classe similaire dans Android Canvas mais est adapté à l'architecture Dart et au Flutter Engine. Canvas fonctionne dans un système de coordonnées où l'origine (0, 0) est le coin supérieur gauche.
Paint — à ne pas confondre avec la méthode paint() — est une classe qui définit le style de dessin : couleur (color), épaisseur de trait (strokeWidth), style (style : fill ou stroke), opacité, mode de mélange (blendMode) et masque d'anticrénelage. Paint est configuré avant le dessin et passé aux méthodes de Canvas. Important : Paint est un objet mutable, mais il est recommandé de le créer une fois et de le réutiliser, en modifiant uniquement les paramètres nécessaires.
| Méthode Canvas | Objectif | Paramètres |
|---|---|---|
| drawCircle | Dessine un cercle | Offset center, double radius, Paint paint |
| drawLine | Dessine un segment de ligne | Offset p1, Offset p2, Paint paint |
| drawRect | Dessine un rectangle | Rect rect, Paint paint |
| drawPath | Dessine un tracé arbitraire | Path path, Paint paint |
| drawArc | Dessine un arc ou secteur | Rect rect, double startAngle, double sweepAngle, bool useCenter, Paint paint |
| drawRRect | Rectangle aux coins arrondis | RRect rrect, Paint paint |
| drawOval | Dessine un ovale | Rect rect, Paint paint |
| drawImage | Dessine une image depuis Image | Image image, Offset p, Paint paint |
Canvas prend en charge les transformations du système de coordonnées : translate(double dx, double dy) déplace l'origine ; rotate(double radians) fait pivoter le système ; scale(double sx, double sy) met à l'échelle ; skew(double sx, double sy) incline. Avant une transformation, il est recommandé de sauvegarder l'état du Canvas via save() et de le restaurer après la fin via restore(). Cela empêche l'accumulation de transformations dans les appels paint ultérieurs.
paint(Canvas canvas, Size size) est la méthode obligatoire qui implémente toute la logique de dessin. Le paramètre canvas est la surface graphique pour le dessin. Le paramètre size est la taille de la zone allouée pour le dessin (largeur et hauteur en pixels logiques). Canvas est préconfiguré de sorte que l'origine (0,0) corresponde au coin supérieur gauche de la zone de dessin. Toutes les coordonnées sont spécifiées par rapport à cette origine.
shouldRepaint(covariant CustomPainter oldDelegate) est une méthode obligatoire qui optimise la fréquence de redessin. Flutter appelle shouldRepaint à chaque mise à jour de l'arbre de widgets qui affecte CustomPainter. Si la méthode renvoie false, l'image déjà rendue est considérée comme valide et paint n'est pas appelé. Une implémentation typique est : return oldDelegate.someField != someField — comparaison des champs de l'ancien et du nouveau painter pour déterminer la nécessité d'un redessin.
Pour les images animées ou en constante évolution (horloges, indicateurs de chargement, graphiques), shouldRepaint doit renvoyer true à chaque changement d'état. Si l'objet est statique (logo, icône, fond), shouldRepaint renvoie false après le premier rendu. Flutter met automatiquement en cache le résultat de paint et ne redessine que lorsque la taille, l'opacité ou l'appel shouldRepaint changent.
Créons GaugePainter — un indicateur circulaire personnalisé avec remplissage animé. CustomPainter accepte la valeur actuelle (progression de 0,0 à 1,0) et dessine un arc avec un dégradé. La méthode shouldRepaint compare la valeur de progression pour l'optimisation. CustomPaint est placé dans l'arbre de widgets avec le paramètre size pour limiter la zone de dessin.
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;
}
}
L'exemple dessine un graphique d'ondes en utilisant Path. Path permet de construire des contours arbitraires à partir de lignes, de courbes de Bézier et d'arcs. Après avoir construit le contour, il est rempli de couleur ou tracé avec une ligne. Dans l'exemple, le graphique est construit à partir d'un tableau de points simulant une forme d'onde.
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 (arrière-plan) est dessiné avant le widget enfant de CustomPaint. Si CustomPaint a un widget enfant (paramètre child), le painter d'arrière-plan est affiché en dessous. Cela convient pour les décorations : barres de progression, motifs d'arrière-plan, filigranes. foregroundPainter est dessiné après le widget enfant, c'est-à-dire par-dessus. Il est utilisé pour les superpositions : reflets, masques, règles de mesure, annotations.
La différence entre painter et foregroundPainter est cruciale pour l'ordre Z. Si vous devez dessiner quelque chose derrière le contenu — utilisez painter. Si par-dessus le contenu — foregroundPainter. Dans certains cas, vous pouvez utiliser les deux painters simultanément : arrière-plan via painter, superposition d'étiquette via foregroundPainter. Cela permet de séparer la logique de différentes couches graphiques en deux classes CustomPainter distinctes.
Le paramètre size dans la méthode paint est le même pour les deux painters et est déterminé par la taille de la zone allouée pour CustomPaint. La taille peut être définie explicitement via le paramètre size dans CustomPaint ou implicitement via le conteneur parent. Si la taille est Size.zero, paint n'est pas appelé — Flutter ignore le rendu pour une zone de taille nulle. Ce comportement doit être pris en compte lors de l'animation de l'apparition d'éléments.
La règle principale des performances de CustomPainter est de ne pas créer d'objets à l'intérieur de la méthode paint. Canvas, Paint, Path, Rect, Offset et autres objets de dessin doivent être créés dans le constructeur de CustomPainter ou mis en cache. Créer des objets dans paint entraîne des allocations à chaque image, ce qui à 60 images par seconde provoque des collectes de déchets fréquentes et des saccades d'animation (jank).
La mise en cache de Path accélère considérablement le rendu des tracés complexes répétés. Si le tracé ne change pas entre les images (par exemple, une grille d'arrière-plan), créez le Path une fois et réutilisez-le plusieurs fois. RepaintBoundary est un widget enveloppant qui empêche le redessin de CustomPainter lorsque l'arbre de widgets supérieur change. Si CustomPainter est statique, enveloppez-le dans un RepaintBoundary pour l'isoler.
Pour les CustomPainter animés avec des redessins fréquents, utilisez Ticker ou AnimationController avec shouldRepaint renvoyant true à chaque changement de la valeur animée. Il est optimal de comparer le champ animé dans shouldRepaint plutôt que de redessiner à chaque image inutilement. Mise en cache de Rect pour les dégradés complexes : si les couleurs du dégradé ne changent pas, vous pouvez mettre en cache le Shader via Paint.shader.
Foire aux questions
CustomPainter est une classe de contrat qui définit quoi et comment dessiner sur Canvas. Canvas est l'objet de dessin lui-même, qui fournit des méthodes (drawCircle, drawLine, etc.). CustomPainter reçoit Canvas via la méthode paint et contrôle son utilisation. Un même Canvas peut être utilisé par différents CustomPainter à différents moments.
Non, CustomPainter n'a pas de sens sans CustomPaint. CustomPaint est le widget qui appelle la méthode paint de CustomPainter et affiche le résultat à l'écran. Sans CustomPaint, Canvas ne sera pas créé et paint ne s'exécutera pas. Vous pouvez cependant implémenter votre propre RenderObject avec Canvas, mais c'est une approche de plus bas niveau.
Utilisez un AnimationController avec un Ticker, en passant la valeur animée à CustomPainter via les paramètres du constructeur. Lorsque la valeur change, appelez setState() dans le widget contenant CustomPaint, ce qui déclenche l'appel à shouldRepaint. Si shouldRepaint renvoie true, le système appelle paint pour la nouvelle image d'animation.
La cause est presque toujours shouldRepaint qui renvoie false. Vérifiez que les champs de oldDelegate sont correctement comparés avec les nouvelles valeurs. Si un objet mutable est utilisé, shouldRepaint peut ne pas détecter les changements car la référence à l'objet reste la même. Utilisez == ou des objets immutables pour une comparaison correcte.
Oui, CustomPainter est utilisé pour rendre des décorations personnalisées dans des listes complexes. Cependant, notez que lors de la réorganisation des éléments, le RepaintBoundary à l'intérieur de ListView met en cache le rendu de chaque élément. CustomPainter doit être léger et ne pas contenir d'opérations lourdes dans paint pour ne pas ralentir l'animation de glissement.
Résumé
Nous développerons une application mobile clé en main
IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.
Lisez aussi