CustomPainter è una classe astratta dell'SDK Flutter che offre agli sviluppatori il controllo completo sul rendering di grafica 2D personalizzata su Canvas. Lo sviluppatore crea una sottoclasse di CustomPainter e implementa due metodi obbligatori: paint(Canvas, Size) e shouldRepaint(covariant CustomPainter oldDelegate). Secondo la Documentazione API Flutter (2026), CustomPainter viene utilizzato insieme al widget CustomPaint, che delega il rendering all'oggetto CustomPainter fornito ad ogni fotogramma di animazione o cambiamento di stato.
Punti Chiave
CustomPainter è una classe base di Flutter per implementare grafica 2D personalizzata. Sostituisce l'approccio di sovrascrittura di onDraw in Android, fornendo un modello di rendering più flessibile e dichiarativo. Lo sviluppatore crea una sottoclasse di CustomPainter, sovrascrive il metodo paint dove riceve Canvas e Size, ed esegue tutte le operazioni di disegno su Canvas. Il widget CustomPaint gestisce la chiamata a paint e integra il risultato nell'albero dei widget.
La differenza chiave tra CustomPainter e il disegno diretto tramite Canvas è la separazione della logica di rendering (paint) e del trigger di ridisegno (shouldRepaint). Il sistema Flutter chiama shouldRepaint ogni volta che il widget genitore cambia. Se shouldRepaint restituisce false, paint non viene chiamato, risparmiando risorse. Questo meccanismo è simile a shouldRebuild in RenderObject e fa parte dell'architettura Flutter con i suoi tre alberi (Widget, Element, RenderObject).
CustomPaint è un widget che accetta i parametri painter (livello di sfondo) e foregroundPainter (livello di primo piano). Entrambi i parametri accettano oggetti CustomPainter. Il painter di sfondo viene disegnato prima del widget figlio di CustomPaint, e il foregroundPainter dopo. Questo consente di utilizzare CustomPaint come contenitore con sfondo decorativo o sovrapposizione senza perdere la possibilità di incorporare widget normali al suo interno.
Canvas è l'oggetto centrale di disegno in Flutter, che incapsula la superficie grafica. Fornisce circa 40 metodi di disegno: drawLine, drawCircle, drawRect, drawPath, drawArc, drawImage, drawPicture, drawPoints e altri. Canvas in Flutter corrisponde a una classe simile in Android Canvas ma è adattato all'architettura Dart e al Flutter Engine. Canvas opera in un sistema di coordinate in cui l'origine (0, 0) è l'angolo superiore sinistro.
Paint — da non confondere con il metodo paint() — è una classe che definisce lo stile di disegno: colore (color), spessore della linea (strokeWidth), stile (style: fill o stroke), opacità, modalità di fusione (blendMode) e maschera di anti-aliasing. Paint viene configurato prima del disegno e passato ai metodi di Canvas. Importante: Paint è un oggetto mutabile, ma si consiglia di crearlo una volta e riutilizzarlo, modificando solo i parametri necessari.
| Metodo Canvas | Scopo | Parametri |
|---|---|---|
| drawCircle | Disegna un cerchio | Offset center, double radius, Paint paint |
| drawLine | Disegna un segmento di linea | Offset p1, Offset p2, Paint paint |
| drawRect | Disegna un rettangolo | Rect rect, Paint paint |
| drawPath | Disegna un percorso arbitrario | Path path, Paint paint |
| drawArc | Disegna un arco o settore | Rect rect, double startAngle, double sweepAngle, bool useCenter, Paint paint |
| drawRRect | Rettangolo con angoli arrotondati | RRect rrect, Paint paint |
| drawOval | Disegna un ovale | Rect rect, Paint paint |
| drawImage | Disegna un'immagine da Image | Image image, Offset p, Paint paint |
Canvas supporta le trasformazioni del sistema di coordinate: translate(double dx, double dy) sposta l'origine; rotate(double radians) ruota il sistema; scale(double sx, double sy) ridimensiona; skew(double sx, double sy) inclina. Prima di una trasformazione, si consiglia di salvare lo stato di Canvas tramite save() e di ripristinarlo dopo il completamento tramite restore(). Questo impedisce l'accumulo di trasformazioni nelle successive chiamate a paint.
paint(Canvas canvas, Size size) è il metodo obbligatorio che implementa tutta la logica di disegno. Il parametro canvas è la superficie grafica per il disegno. Il parametro size è la dimensione dell'area allocata per il disegno (larghezza e altezza in pixel logici). Canvas è preconfigurato in modo che l'origine (0,0) corrisponda all'angolo superiore sinistro dell'area di disegno. Tutte le coordinate sono specificate rispetto a questa origine.
shouldRepaint(covariant CustomPainter oldDelegate) è un metodo obbligatorio che ottimizza la frequenza di ridisegno. Flutter chiama shouldRepaint ad ogni aggiornamento dell'albero dei widget che interessa CustomPainter. Se il metodo restituisce false, l'immagine già renderizzata è considerata valida e paint non viene chiamato. Un'implementazione tipica è: return oldDelegate.someField != someField — confronto dei campi del vecchio e nuovo painter per determinare la necessità di ridisegno.
Per immagini animate o in costante cambiamento (orologi, indicatori di caricamento, grafici) shouldRepaint dovrebbe restituire true ad ogni cambiamento di stato. Se l'oggetto è statico (logo, icona, sfondo), shouldRepaint restituisce false dopo il primo rendering. Flutter memorizza automaticamente nella cache il risultato di paint e ridisegna solo quando cambiano la dimensione, l'opacità o la chiamata shouldRepaint.
Creiamo GaugePainter — un indicatore circolare personalizzato con riempimento animato. CustomPainter accetta il valore corrente (progresso da 0.0 a 1.0) e disegna un arco con gradiente. Il metodo shouldRepaint confronta il valore del progresso per l'ottimizzazione. CustomPaint viene inserito nell'albero dei widget con il parametro size per limitare l'area di disegno.
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'esempio disegna un grafico a onde utilizzando Path. Path consente di costruire contorni arbitrari da linee, curve di Bézier e archi. Dopo aver costruito il contorno, viene riempito con colore o delineato con una linea. Nell'esempio, il grafico è costruito da un array di punti che simulano una forma d'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 (sfondo) viene disegnato prima del widget figlio di CustomPaint. Se CustomPaint ha un widget figlio (parametro child), il painter di sfondo viene visualizzato sotto di esso. Questo è adatto per decorazioni: barre di progresso, motivi di sfondo, filigrane. foregroundPainter viene disegnato dopo il widget figlio, cioè sopra di esso. Viene utilizzato per sovrapposizioni: riflessi, maschere, righelli di misurazione, annotazioni.
La differenza tra painter e foregroundPainter è critica per l'ordine Z. Se hai bisogno di disegnare qualcosa dietro il contenuto — usa painter. Se sopra il contenuto — foregroundPainter. In alcuni casi, puoi utilizzare entrambi i painter contemporaneamente: sfondo tramite painter, etichetta di sovrapposizione tramite foregroundPainter. Questo consente di separare la logica di diversi livelli grafici in due classi CustomPainter separate.
Il parametro size nel metodo paint è lo stesso per entrambi i painter ed è determinato dalla dimensione dell'area allocata per CustomPaint. La dimensione può essere impostata esplicitamente tramite il parametro size in CustomPaint o implicitamente tramite il contenitore genitore. Se la dimensione è Size.zero, paint non viene chiamato — Flutter salta il rendering per un'area di dimensione zero. Questo comportamento deve essere considerato quando si anima la comparsa di elementi.
La regola principale delle prestazioni di CustomPainter è non creare oggetti all'interno del metodo paint. Canvas, Paint, Path, Rect, Offset e altri oggetti di disegno dovrebbero essere creati nel costruttore di CustomPainter o memorizzati nella cache. Creare oggetti all'interno di paint porta ad allocazioni ad ogni fotogramma, che a 60 fotogrammi al secondo causa frequenti garbage collection e scatti nell'animazione (jank).
La memorizzazione nella cache di Path accelera significativamente il rendering di percorsi complessi ripetuti. Se il percorso non cambia tra i fotogrammi (ad esempio, una griglia di sfondo), crea il Path una volta e riutilizzalo più volte. RepaintBoundary è un widget wrapper che impedisce il ridisegno di CustomPainter quando l'albero dei widget superiore cambia. Se CustomPainter è statico, avvolgilo in un RepaintBoundary per isolarlo.
Per CustomPainter animati con frequenti ridisegni, usa Ticker o AnimationController con shouldRepaint che restituisce true ad ogni cambiamento del valore animato. È ottimale confrontare il campo animato in shouldRepaint piuttosto che ridisegnare ad ogni fotogramma inutilmente. Memorizzazione nella cache di Rect per gradienti complessi: se i colori del gradiente non cambiano, puoi memorizzare nella cache lo Shader tramite Paint.shader.
Domande Frequenti
CustomPainter è una classe contratto che definisce cosa e come disegnare sul Canvas. Canvas è l'oggetto di disegno stesso, che fornisce metodi (drawCircle, drawLine, ecc.). CustomPainter riceve Canvas tramite il metodo paint e controlla il suo utilizzo. Una stessa Canvas può essere utilizzata da diversi CustomPainter in momenti diversi.
No, CustomPainter non ha senso senza CustomPaint. CustomPaint è il widget che chiama il metodo paint di CustomPainter e visualizza il risultato sullo schermo. Senza CustomPaint, Canvas non verrà creato e paint non verrà eseguito. Puoi tuttavia implementare il tuo RenderObject con Canvas, ma questo è un approccio di livello inferiore.
Usa un AnimationController con un Ticker, passando il valore animato a CustomPainter tramite i parametri del costruttore. Quando il valore cambia, chiama setState() nel widget che contiene CustomPaint, che attiva la chiamata a shouldRepaint. Se shouldRepaint restituisce true, il sistema chiama paint per il nuovo fotogramma di animazione.
La causa è quasi sempre shouldRepaint che restituisce false. Controlla se i campi di oldDelegate vengono confrontati correttamente con i nuovi valori. Se viene utilizzato un oggetto mutabile, shouldRepaint potrebbe non rilevare le modifiche perché il riferimento all'oggetto rimane lo stesso. Usa == o oggetti immutabili per un confronto corretto.
Sì, CustomPainter viene utilizzato per disegnare decorazioni personalizzate in liste complesse. Tuttavia, tieni presente che durante il riordinamento degli elementi, RepaintBoundary all'interno di ListView memorizza nella cache il rendering di ciascun elemento. CustomPainter dovrebbe essere leggero e non contenere operazioni pesanti in paint per non rallentare l'animazione di trascinamento.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche