CustomPainter — to abstrakcyjna klasa z Flutter SDK, zapewniająca programistom pełną kontrolę nad renderowaniem niestandardowej grafiki 2D na Canvas. Programista tworzy podklasę CustomPainter i implementuje dwie obowiązkowe metody: paint(Canvas, Size) i shouldRepaint(covariant CustomPainter oldDelegate). Według Flutter API Documentation (2026), CustomPainter jest używany razem z widgetem CustomPaint, który deleguje renderowanie do przekazanego obiektu CustomPainter na każdej klatce animacji lub zmiany stanu.
Najważniejsze
CustomPainter — to podstawowa klasa Flutter do implementacji dowolnej grafiki 2D. Zastępuje ona podejście z nadpisywaniem onDraw w Android, zapewniając bardziej elastyczny i deklaratywny model renderowania. Programista tworzy podklasę CustomPainter, nadpisuje metodę paint, gdzie otrzymuje Canvas i Size, i wykonuje na Canvas wszystkie operacje rysowania. Widget CustomPaint zarządza wywołaniem paint i integruje wynik z drzewem widgetów.
Kluczowa różnica między CustomPainter a bezpośrednim rysowaniem przez Canvas — oddzielenie logiki renderowania (paint) i wyzwalacza przerysowania (shouldRepaint). System Flutter wywołuje shouldRepaint za każdym razem, gdy nadrzędny widget się zmienia. Jeśli shouldRepaint zwraca false, paint nie jest wywoływany, co oszczędza zasoby. Ten mechanizm jest analogiczny do shouldRebuild w RenderObject i jest częścią architektury Flutter z jej trzema drzewami (Widget, Element, RenderObject).
CustomPaint — to widget, który przyjmuje parametry painter (warstwa tła) i foregroundPainter (warstwa przednia). Oba parametry przyjmują obiekty CustomPainter. Malarz tła rysuje przed dzieckiem CustomPaint, a foregroundPainter — po. Pozwala to używać CustomPaint jako kontenera z dekoracyjnym tłem lub nakładką, nie tracąc możliwości osadzania zwykłych widgetów w środku.
Canvas — to centralny obiekt do rysowania we Flutter, hermetyzujący powierzchnię graficzną. Udostępnia około 40 metod rysowania: drawLine, drawCircle, drawRect, drawPath, drawArc, drawImage, drawPicture, drawPoints i inne. Canvas we Flutter odpowiada analogicznej klasie w Android Canvas, ale jest dostosowany do architektury Dart i Flutter Engine. Canvas działa w układzie współrzędnych, gdzie początek (0, 0) to lewy górny róg.
Paint — nie mylić z metodą paint() — to klasa określająca styl rysowania: kolor (color), grubość linii (strokeWidth), styl (style: fill lub stroke), przezroczystość, tryb mieszania (blendMode) i maskę wygładzania. Paint jest konfigurowany przed rysowaniem i przekazywany do metod Canvas. Ważne: Paint to zmienny obiekt, ale zaleca się utworzenie go raz i ponowne użycie, zmieniając tylko niezbędne parametry.
| Metoda Canvas | Przeznaczenie | Parametry |
|---|---|---|
| drawCircle | Rysowanie okręgu | Offset center, double radius, Paint paint |
| drawLine | Rysowanie odcinka | Offset p1, Offset p2, Paint paint |
| drawRect | Rysowanie prostokąta | Rect rect, Paint paint |
| drawPath | Rysowanie dowolnego konturu | Path path, Paint paint |
| drawArc | Rysowanie łuku lub wycinka | Rect rect, double startAngle, double sweepAngle, bool useCenter, Paint paint |
| drawRRect | Prostokąt z zaokrąglonymi narożnikami | RRect rrect, Paint paint |
| drawOval | Rysowanie owalu | Rect rect, Paint paint |
| drawImage | Rysowanie obrazu z Image | Image image, Offset p, Paint paint |
Canvas obsługuje transformacje układu współrzędnych: translate(double dx, double dy) przesuwa początek współrzędnych; rotate(double radians) obraca układ; scale(double sx, double sy) skaluje; skew(double sx, double sy) pochyla. Przed transformacją zaleca się zapisanie stanu Canvas przez save(), a po zakończeniu przywrócenie przez restore(). Zapobiega to kumulacji transformacji w kolejnych wywołaniach paint.
paint(Canvas canvas, Size size) — obowiązkowa metoda, w której implementowana jest cała logika rysowania. Parametr canvas — powierzchnia graficzna do rysowania. Parametr size — rozmiar obszaru przeznaczonego do rysowania (szerokość i wysokość w pikselach logicznych). Canvas jest wstępnie skonfigurowany tak, że początek współrzędnych (0,0) odpowiada lewemu górnemu rogowi obszaru rysowania. Wszystkie współrzędne są określane względem tego początku.
shouldRepaint(covariant CustomPainter oldDelegate) — obowiązkowa metoda optymalizująca częstotliwość przerysowania. Flutter wywołuje shouldRepaint przy każdej aktualizacji drzewa widgetów, która dotyczy CustomPainter. Jeśli metoda zwraca false, już narysowany obraz jest uznawany za ważny i paint nie jest wywoływany. Typowa implementacja: return oldDelegate.someField != someField — porównanie pól starego i nowego malarza w celu określenia potrzeby przerysowania.
Dla animowanych lub stale zmieniających się obrazów (zegary, wskaźniki postępu, wykresy) shouldRepaint powinien zwracać true przy każdej zmianie stanu. Jeśli obiekt jest statyczny (logo, ikona, tło), shouldRepaint zwraca false po pierwszym renderowaniu. Flutter automatycznie buforuje wynik paint i przerysowuje tylko przy zmianie rozmiaru, przezroczystości lub wywołaniu shouldRepaint.
Stwórzmy GaugePainter — niestandardowy okrągły wskaźnik z animowanym wypełnieniem. CustomPainter przyjmuje bieżącą wartość (progress od 0.0 do 1.0) i rysuje łuk z gradientem. W metodzie shouldRepaint porównywana jest wartość progress w celu optymalizacji. CustomPaint jest umieszczany w drzewie widgetów z parametrem size w celu ograniczenia obszaru rysowania.
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;
}
}
Przykład rysuje wykres falowy przy użyciu Path. Path pozwala budować dowolne kontury z linii, krzywych Beziera i łuków. Po zbudowaniu kontur jest wypełniany kolorem lub obrysowywany linią. W przykładzie wykres jest budowany na podstawie tablicy punktów imitujących kształt fali.
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 (tła) rysuje przed dzieckiem CustomPaint. Jeśli CustomPaint ma dziecko (parametr child), malarz tła jest wyświetlany pod nim. To nadaje się do dekoracji: paski postępu, wzory tła, znaki wodne. foregroundPainter rysuje po dziecku CustomPaint, czyli nad nim. Jest używany do nakładek: refleksów, masek, linijek pomiarowych, adnotacji.
Różnica między painter a foregroundPainter jest kluczowa dla Z-order. Jeśli trzeba narysować coś za zawartością — użyj painter. Jeśli nad zawartością — foregroundPainter. W niektórych przypadkach można użyć obu malarzy jednocześnie: tło przez painter, nakładkę przez foregroundPainter. Pozwala to oddzielić logikę różnych warstw graficznych na dwie oddzielne klasy CustomPainter.
Parametr size w metodzie paint jest taki sam dla obu malarzy i jest określany przez rozmiar obszaru przeznaczonego pod CustomPaint. Rozmiar można ustawić jawnie przez parametr size w CustomPaint lub niejawnie przez nadrzędny kontener. Jeśli size jest równy Size.zero, paint nie jest wywoływany — Flutter pomija renderowanie dla zerowego obszaru. To zachowanie należy uwzględnić przy animowanym pojawianiu się elementu.
Główna zasada wydajności CustomPainter — nie tworzyć obiektów wewnątrz metody paint. Canvas, Paint, Path, Rect, Offset i inne obiekty rysowania powinny być tworzone w konstruktorze CustomPainter lub buforowane. Tworzenie obiektów wewnątrz paint prowadzi do alokacji na każdej klatce, co przy 60 klatkach na sekundę powoduje częste zbieranie pamięci i szarpanie animacji (jank).
Buforowanie Path znacznie przyspiesza renderowanie powtarzających się złożonych konturów. Jeśli kontur nie zmienia się między klatkami (np. siatka tła), utwórz Path raz i używaj go wielokrotnie. RepaintBoundary — widget-opakowanie, który zapobiega przerysowaniu CustomPainter przy zmianie nadrzędnego drzewa widgetów. Jeśli CustomPainter jest statyczny, opakuj go w RepaintBoundary w celu izolacji.
Dla animowanych CustomPainter z częstym przerysowaniem używaj Ticker lub AnimationController z shouldRepaint zwracającym true przy zmianie animowanej wartości. Optymalnie jest porównywać animowane pole w shouldRepaint, a nie przerysowywać na każdej klatce bez potrzeby. Rect caching dla złożonych gradientów: jeśli gradient nie zmienia kolorów, można buforować Shader przez Paint.shader.
Często zadawane pytania
CustomPainter — to klasa-kontrakt określająca, co i jak rysować na Canvas. Canvas — sam obiekt rysowania, który udostępnia metody (drawCircle, drawLine itd.). CustomPainter otrzymuje Canvas przez metodę paint i zarządza jego użyciem. Jeden Canvas może być używany przez różne CustomPainter w różnym czasie.
Nie, CustomPainter nie ma sensu bez CustomPaint. CustomPaint to widget, który wywołuje metodę paint CustomPainter i wyświetla wynik na ekranie. Bez CustomPaint Canvas nie zostanie utworzony, a paint nie zostanie wykonany. Można jednak zaimplementować własny RenderObject z Canvas, ale jest to podejście niższego poziomu.
Użyj AnimationController z Ticker, przekazując animowaną wartość do CustomPainter przez parametry konstruktora. Przy zmianie wartości wywołuj setState() w widgetcie zawierającym CustomPaint, co wyzwala shouldRepaint. Jeśli shouldRepaint zwraca true, system wywołuje paint dla nowej klatki animacji.
Przyczyną prawie zawsze jest shouldRepaint zwracający false. Sprawdź, czy pola oldDelegate są poprawnie porównywane z nowymi wartościami. Jeśli używany jest mutable object, shouldRepaint może nie zauważyć zmian, ponieważ referencja do obiektu pozostaje ta sama. Użyj == lub niezmiennych obiektów do poprawnego porównania.
Tak, CustomPainter jest używany do rysowania niestandardowych dekoracji w złożonych listach. Należy jednak pamiętać, że przy zmianie kolejności elementów RepaintBoundary wewnątrz ListView buforuje renderowanie każdego elementu. CustomPainter powinien być lekki i nie zawierać ciężkich operacji w paint, aby nie spowalniać animacji przeciągania.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również