CustomPainter — це абстрактний клас з Flutter SDK, який надає розробникам повний контроль над відмальовуванням кастомної 2D-графіки на Canvas. Розробник створює підклас CustomPainter та реалізує два обов'язкових методи: paint(Canvas, Size) та shouldRepaint(covariant CustomPainter oldDelegate). За даними Flutter API Documentation (2026), CustomPainter використовується разом з віджетом CustomPaint, який делегує відмальовування переданому об'єкту CustomPainter на кожному кадрі анімації або зміни стану.
Головне
CustomPainter — це базовий клас Flutter для реалізації довільної 2D-графіки. Він замінює підхід з перевизначенням onDraw в Android, надаючи більш гнучку та декларативну модель відмальовування. Розробник створює підклас CustomPainter, перевизначає метод paint, де отримує Canvas та Size, та виконує на Canvas всі операції малювання. Віджет CustomPaint керує викликом paint та інтегрує результат в дерево віджетів.
Ключова відмінність CustomPainter від прямого малювання через Canvas — розділення логіки відмальовування (paint) та тригера перемальовування (shouldRepaint). Система Flutter викликає shouldRepaint щоразу, коли батьківський віджет змінюється. Якщо shouldRepaint повертає false, paint не викликається, що економить ресурси. Цей механізм аналогічний shouldRebuild в RenderObject і є частиною архітектури Flutter з її трьома деревами (Widget, Element, RenderObject).
CustomPaint — це віджет, який приймає параметри painter (фоновий шар) та foregroundPainter (передній шар). Обидва параметри приймають об'єкти CustomPainter. Фоновий painter малюється до дочірнього віджета CustomPaint, а foregroundPainter — після. Це дозволяє використовувати CustomPaint як контейнер з декоративним фоном або накладанням, не втрачаючи можливості вбудовувати звичайні віджети всередину.
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 при кожному оновленні дерева віджетів, яке зачіпає 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 розміщується в дереві віджетів з параметром 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 малюється після дочірнього віджета, тобто поверх нього. Використовується для накладань: бликів, масок, вимірювальних лінійок, анотацій.
Різниця між 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 — віджет-обгортка, який запобігає перемальовуванню CustomPainter при зміні вищестоящого дерева віджетів. Якщо CustomPainter статичний, оберніть його в RepaintBoundary для ізоляції.
Для анімованих CustomPainter з частою перемальовкою використовуйте Ticker або AnimationController з shouldRepaint, що повертає true при зміні анімованого значення. Оптимально порівнювати анімоване поле в shouldRepaint, а не перемальовувати на кожному кадрі без потреби. Rect caching для складних градієнтів: якщо градієнт не змінює кольори, можна закешувати Shader через Paint.shader.
Часті запитання
CustomPainter — це клас-контракт, що визначає, що і як малювати на Canvas. Canvas — сам об'єкт малювання, який надає методи (drawCircle, drawLine тощо). CustomPainter отримує Canvas через метод paint та керує його використанням. Один Canvas може використовуватися різними CustomPainter в різний час.
Ні, CustomPainter не має сенсу без CustomPaint. CustomPaint — це віджет, який викликає метод paint CustomPainter та відображає результат на екрані. Без CustomPaint Canvas не буде створено, і paint не виконається. Можна, однак, реалізувати власний RenderObject з Canvas, але це більш низькорівневий підхід.
Використовуйте AnimationController з Ticker, передаючи анімоване значення в CustomPainter через параметри конструктора. При зміні значення викликайте setState() у віджеті, що містить CustomPaint, що тригерить виклик shouldRepaint. Якщо shouldRepaint повертає true, система викликає paint для нового кадру анімації.
Причина майже завжди в shouldRepaint, що повертає false. Перевірте, чи коректно порівнюються поля oldDelegate з новими значеннями. Якщо використовується mutable об'єкт, shouldRepaint може не помітити змін, оскільки посилання на об'єкт залишається тією самою. Використовуйте == або immutable-об'єкти для коректного порівняння.
Так, CustomPainter використовується для відмальовування кастомних декорацій у складних списках. Однак врахуйте, що при перевпорядкуванні елементів RepaintBoundary всередині ListView кешує відмальовування кожного елемента. CustomPainter має бути легким і не містити важких операцій у paint, щоб не сповільнювати анімацію перетягування.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також