CustomPainter — е абстрактен клас от Flutter SDK, предоставящ на разработчиците пълен контрол върху изобразяването на персонализирана 2D графика върху Canvas. Разработчикът създава подклас на CustomPainter и имплементира два задължителни метода: paint(Canvas, Size) и shouldRepaint(covariant CustomPainter oldDelegate). Според Flutter API Documentation (2026), CustomPainter се използва заедно с widget-а CustomPaint, който делегира изобразяването на подадения обект CustomPainter на всеки кадър от анимация или промяна на състояние.
Основни моменти
CustomPainter — е основният клас на Flutter за имплементиране на произволна 2D графика. Той заменя подхода с презаписване на onDraw в Android, предоставяйки по-гъвкав и декларативен модел на изобразяване. Разработчикът създава подклас на CustomPainter, презаписва метода paint, където получава Canvas и Size, и изпълнява всички операции по рисуване върху Canvas. Widget-ът CustomPaint управлява извикването на paint и интегрира резултата в дървото от widget-и.
Ключовата разлика между CustomPainter и директното рисуване чрез Canvas — разделянето на логиката на изобразяване (paint) и тригера за прерисуване (shouldRepaint). Системата на Flutter извиква shouldRepaint всеки път, когато родителският widget се промени. Ако shouldRepaint върне false, paint не се извиква, което спестява ресурси. Този механизъм е аналогичен на shouldRebuild в RenderObject и е част от архитектурата на Flutter с нейните три дървета (Widget, Element, RenderObject).
CustomPaint — е widget, който приема параметрите painter (фонов слой) и foregroundPainter (преден слой). И двата параметъра приемат обекти CustomPainter. Фоновият painter рисува преди детето на CustomPaint, а foregroundPainter — след него. Това позволява използването на CustomPaint като контейнер с декоративен фон или наслагване, без да се губи възможността за вграждане на обикновени widget-и вътре.
Canvas — е централният обект за рисуване във Flutter, който капсулира графичната повърхност. Той предоставя около 40 метода за рисуване: drawLine, drawCircle, drawRect, drawPath, drawArc, drawImage, drawPicture, drawPoints и други. Canvas във Flutter съответства на аналогичния клас в Android Canvas, но е адаптиран към архитектурата на Dart и Flutter Engine. Canvas работи в координатна система, където началото (0, 0) е горният ляв ъгъл.
Paint — да не се бърка с метода paint() — е клас, който определя стила на рисуване: цвят (color), дебелина на линията (strokeWidth), стил (style: fill или stroke), прозрачност, режим на смесване (blendMode) и маска за изглаждане. Paint се конфигурира преди рисуване и се предава на методите на Canvas. Важно: Paint е променяем обект, но се препоръчва да се създаде веднъж и да се използва многократно, като се променят само необходимите параметри.
| Метод на Canvas | Предназначение | Параметри |
|---|---|---|
| drawCircle | Рисуване на окръжност | Offset center, double radius, Paint paint |
| drawLine | Рисуване на отсечка | Offset p1, Offset p2, Paint paint |
| drawRect | Рисуване на правоъгълник | Rect rect, Paint paint |
| drawPath | Рисуване на произволен контур | Path path, Paint paint |
| drawArc | Рисуване на дъга или сектор | Rect rect, double startAngle, double sweepAngle, bool useCenter, Paint paint |
| drawRRect | Правоъгълник със заоблени ъгли | RRect rrect, Paint paint |
| drawOval | Рисуване на овал | Rect rect, Paint paint |
| drawImage | Рисуване на изображение от Image | Image image, Offset p, Paint paint |
Canvas поддържа трансформации на координатната система: translate(double dx, double dy) премества началото на координатите; rotate(double radians) завърта системата; scale(double sx, double sy) мащабира; skew(double sx, double sy) накланя. Преди трансформация се препоръчва запазване на състоянието на Canvas чрез save(), а след завършване — възстановяване чрез restore(). Това предотвратява натрупването на трансформации в последващи извиквания на paint.
paint(Canvas canvas, Size size) — задължителният метод, в който се имплементира цялата логика на рисуване. Параметърът canvas — графичната повърхност за рисуване. Параметърът size — размерът на областта, определена за рисуване (ширина и височина в логически пиксели). Canvas е предварително конфигуриран така, че началото на координатите (0,0) съответства на горния ляв ъгъл на областта за рисуване. Всички координати се задават спрямо това начало.
shouldRepaint(covariant CustomPainter oldDelegate) — задължителният метод, който оптимизира честотата на прерисуване. Flutter извиква shouldRepaint при всяка актуализация на дървото от widget-и, която засяга CustomPainter. Ако методът върне false, вече нарисуваното изображение се счита за валидно и paint не се извиква. Типична имплементация: return oldDelegate.someField != someField — сравнение на полетата на стария и новия painter за определяне на необходимостта от прерисуване.
За анимирани или постоянно променящи се изображения (часовници, индикатори за зареждане, графики) shouldRepaint трябва да връща true при всяка промяна на състояние. Ако обектът е статичен (лого, икона, фон), shouldRepaint връща false след първото изобразяване. Flutter автоматично кешира резултата от paint и прерисува само при промяна на размер, прозрачност или извикване на shouldRepaint.
Нека създадем GaugePainter — персонализиран кръгов индикатор с анимирано запълване. CustomPainter приема текущата стойност (progress от 0.0 до 1.0) и рисува дъга с градиент. В метода shouldRepaint се сравнява стойността на progress за оптимизация. CustomPaint се поставя в дървото от widget-и с параметър size за ограничаване на областта за рисуване.
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;
}
}
Примерът рисува вълнова графика с използване на Path. Path позволява изграждане на произволни контури от линии, криви на Безие и дъги. След изграждането контурът се запълва с цвят или се очертава с линия. В примера графиката се изгражда на базата на масив от точки, имитиращи вълнова форма.
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 (фон) рисува преди детето на CustomPaint. Ако CustomPaint има дете (параметър child), фоновият painter се показва под него. Това е подходящо за декорация: ленти за напредък, фонове шарки, водни знаци. foregroundPainter рисува след детето на CustomPaint, т.е. върху него. Използва се за наслагвания: отблясъци, маски, измервателни линийки, анотации.
Разликата между painter и foregroundPainter е критична за Z-order. Ако трябва да нарисувате нещо зад съдържанието — използвайте painter. Ако върху съдържанието — foregroundPainter. В някои случаи можете да използвате двата painter едновременно: фон чрез painter, етикет-наслагване чрез foregroundPainter. Това позволява разделяне на логиката на различни графични слоеве в два отделни класа CustomPainter.
Параметърът size в метода paint е еднакъв и за двата painter и се определя от размера на областта, определена за CustomPaint. Размерът може да бъде зададен изрично чрез параметъра size в CustomPaint или неявно чрез родителския контейнер. Ако size е равен на Size.zero, paint не се извиква — Flutter пропуска изобразяването за нулева област. Това поведение трябва да се вземе предвид при анимирано появяване на елемент.
Основното правило за производителността на CustomPainter — да не се създават обекти вътре в метода paint. Canvas, Paint, Path, Rect, Offset и други обекти за рисуване трябва да се създават в конструктора на CustomPainter или да се кешират. Създаването на обекти вътре в paint води до заделяне на памет на всеки кадър, което при 60 кадъра в секунда причинява често събиране на отпадъци и потрепване на анимацията (jank).
Кеширането на Path значително ускорява изобразяването на повтарящи се сложни контури. Ако контурът не се променя между кадрите (напр. фонова мрежа), създайте Path веднъж и го използвайте многократно. RepaintBoundary — widget-обвивка, който предотвратява прерисуването на CustomPainter при промяна на горното дърво от widget-и. Ако CustomPainter е статичен, увийте го в RepaintBoundary за изолация.
За анимирани CustomPainter с често прерисуване използвайте Ticker или AnimationController с shouldRepaint, връщащ true при промяна на анимираната стойност. Оптимално е да сравнявате анимираното поле в shouldRepaint, а не да прерисувате на всеки кадър без необходимост. Rect кеширане за сложни градиенти: ако градиентът не променя цветовете, може да кеширате Shader чрез Paint.shader.
Често задавани въпроси
CustomPainter — е клас-договор, който определя какво и как да се рисува върху Canvas. Canvas — самият обект за рисуване, който предоставя методи (drawCircle, drawLine и т.н.). CustomPainter получава Canvas чрез метода paint и управлява използването му. Един Canvas може да се използва от различни CustomPainter по различно време.
Не, CustomPainter няма смисъл без CustomPaint. CustomPaint е widget-ът, който извиква метода paint на CustomPainter и показва резултата на екрана. Без CustomPaint Canvas няма да бъде създаден и paint няма да се изпълни. Може обаче да имплементирате собствен RenderObject с Canvas, но това е подход от по-ниско ниво.
Използвайте AnimationController с Ticker, като предавате анимираната стойност на CustomPainter чрез параметрите на конструктора. При промяна на стойността извикайте setState() в widget-а, съдържащ CustomPaint, което задейства shouldRepaint. Ако shouldRepaint върне true, системата извиква paint за новия кадър на анимацията.
Причината почти винаги е shouldRepaint, който връща false. Проверете дали полетата на oldDelegate се сравняват правилно с новите стойности. Ако се използва променяем обект, shouldRepaint може да не забележи промените, тъй като референцията към обекта остава същата. Използвайте == или неизменяеми обекти за правилно сравнение.
Да, CustomPainter се използва за рисуване на персонализирани декорации в сложни списъци. Имайте предвид обаче, че при пренареждане на елементи RepaintBoundary вътре в ListView кешира изобразяването на всеки елемент. CustomPainter трябва да бъде лек и да не съдържа тежки операции в paint, за да не забавя анимацията на влачене.
Обобщение
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също