Perché serve CustomPaint
Flutter offre un ricco catalogo di widget pronti all'uso, ma prima o poi ci si imbatte in un requisito che nessun widget standard risolve: un grafico personalizzato, una firma digitale, un indicatore circolare animato o forme geometriche complesse. In questi casi la risposta è CustomPaint, che ci dà accesso diretto al Canvas, lo stesso strato di disegno su cui poggia l'intero framework.
I tre attori principali
Il disegno custom ruota attorno a tre elementi:
- CustomPaint: il widget che ospita il disegno e lo inserisce nell'albero dei widget.
- CustomPainter: la classe dove definiamo cosa disegnare, tramite il metodo
paint. - Canvas: l'oggetto che espone i metodi di disegno (linee, rettangoli, percorsi, testo).
Il primo CustomPainter
Per disegnare bisogna estendere CustomPainter e implementare due metodi: paint e shouldRepaint.
class CerchioPainter extends CustomPainter {
final Color colore;
CerchioPainter({required this.colore});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = colore
..style = PaintingStyle.fill;
final centro = Offset(size.width / 2, size.height / 2);
final raggio = size.width / 3;
canvas.drawCircle(centro, raggio, paint);
}
@override
bool shouldRepaint(covariant CerchioPainter oldDelegate) {
return oldDelegate.colore != colore;
}
}
Il parametro Size rappresenta lo spazio disponibile assegnato al CustomPaint. Usarlo per calcolare le coordinate rende il disegno responsivo.
Inserire il painter nell'interfaccia
CustomPaint(
size: const Size(200, 200),
painter: CerchioPainter(colore: Colors.indigo),
)
Se CustomPaint ha un figlio, il disegno funge da sfondo; per disegnare sopra il figlio si usa il parametro foregroundPainter.
L'oggetto Paint
Paint configura l'aspetto di ciò che disegniamo. Le proprietà più usate sono:
color: il colore.style:PaintingStyle.fill(riempimento) oPaintingStyle.stroke(contorno).strokeWidth: spessore della linea.strokeCap: stile delle estremità delle linee.shader: per applicare gradienti.isAntiAlias: smussatura dei bordi (attiva di default).
final paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 4
..strokeCap = StrokeCap.round
..shader = const LinearGradient(
colors: [Colors.blue, Colors.purple],
).createShader(Rect.fromLTWH(0, 0, 200, 200));
Disegnare percorsi con Path
Per forme libere si usa Path, che permette di concatenare segmenti, curve e archi. Ecco un esempio che disegna un triangolo:
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()..color = Colors.teal;
final path = Path()
..moveTo(size.width / 2, 0)
..lineTo(size.width, size.height)
..lineTo(0, size.height)
..close();
canvas.drawPath(path, paint);
}
Le curve di Bézier (quadraticBezierTo e cubicTo) consentono di creare forme morbide, utili per onde, fumetti o bordi arrotondati personalizzati.
Un esempio pratico: grafico a barre
Mettiamo insieme i concetti per disegnare un semplice grafico a barre.
class BarChartPainter extends CustomPainter {
final List<double> valori;
BarChartPainter(this.valori);
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()..color = Colors.amber;
final massimo = valori.reduce((a, b) => a > b ? a : b);
final larghezzaBarra = size.width / (valori.length * 2);
for (var i = 0; i < valori.length; i++) {
final altezza = (valori[i] / massimo) * size.height;
final x = (i * 2 + 0.5) * larghezzaBarra;
final rect = Rect.fromLTWH(
x,
size.height - altezza,
larghezzaBarra,
altezza,
);
canvas.drawRRect(
RRect.fromRectAndRadius(rect, const Radius.circular(4)),
paint,
);
}
}
@override
bool shouldRepaint(covariant BarChartPainter oldDelegate) {
return oldDelegate.valori != valori;
}
}
Disegnare testo sul canvas
Il Canvas non disegna testo direttamente: serve TextPainter.
final textPainter = TextPainter(
text: const TextSpan(
text: '42%',
style: TextStyle(color: Colors.black, fontSize: 18),
),
textDirection: TextDirection.ltr,
)..layout();
textPainter.paint(canvas, const Offset(10, 10));
Animare un CustomPainter
Combinando CustomPainter con un AnimationController si ottengono animazioni fluide. Basta passare il valore animato al painter e ridisegnare a ogni frame.
AnimatedBuilder(
animation: controller,
builder: (context, _) {
return CustomPaint(
size: const Size(200, 200),
painter: ProgressPainter(progresso: controller.value),
);
},
)
In questo modo shouldRepaint riceve a ogni tick il nuovo valore e Flutter ridisegna solo l'area interessata.
Considerazioni sulle performance
Il disegno custom è efficiente, ma alcune accortezze fanno la differenza:
- Implementa correttamente
shouldRepaint: ritornatruesolo quando qualcosa cambia davvero. - Evita di creare oggetti
PaintoPathcomplessi a ogni chiamata dipaintse possono essere precalcolati. - Per disegni statici e costosi valuta
RepaintBoundaryper isolare l'area dai rebuild circostanti. - Usa
canvas.save()ecanvas.restore()quando applichi trasformazioni (rotazioni, traslazioni) per non propagarle al resto del disegno.
Conclusione
CustomPaint apre le porte a interfacce che vanno oltre i widget standard: grafici, indicatori, illustrazioni e animazioni vettoriali. Padroneggiare l'API Canvas e l'oggetto Path ti permette di realizzare qualsiasi elemento visivo immaginabile, mantenendo le ottime performance del motore di rendering di Flutter.
