CustomPainter — що це, методи paint та shouldRepaint у Flutter

Автор: IT Sectr Опубліковано: 2026-07-22 Час читання: 10 хв

CustomPainter — це абстрактний клас з Flutter SDK, який надає розробникам повний контроль над відмальовуванням кастомної 2D-графіки на Canvas. Розробник створює підклас CustomPainter та реалізує два обов'язкових методи: paint(Canvas, Size) та shouldRepaint(covariant CustomPainter oldDelegate). За даними Flutter API Documentation (2026), CustomPainter використовується разом з віджетом CustomPaint, який делегує відмальовування переданому об'єкту CustomPainter на кожному кадрі анімації або зміни стану.

Головне

  • CustomPainter — абстрактний клас Flutter, що визначає кастомне відмальовування на Canvas через методи paint та shouldRepaint
  • CustomPaint — віджет, що приймає CustomPainter як параметр painter або foregroundPainter та відображає результат на екрані
  • Canvas — об'єкт, що надає методи малювання (drawLine, drawCircle, drawPath, drawRect, drawArc та інші)
  • shouldRepaint — метод оптимізації, що визначає, чи потрібно перемальовувати об'єкт при оновленні
  • size — розмір області малювання, що передається в paint(Canvas canvas, Size size) з батьківського віджета

Що таке 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 та Paint у Flutter

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Малювання зображення з ImageImage image, Offset p, Paint paint

Трансформації Canvas

Canvas підтримує трансформації системи координат: translate(double dx, double dy) зсуває початок координат; rotate(double radians) обертає систему; scale(double sx, double sy) масштабує; skew(double sx, double sy) нахиляє. Перед трансформацією рекомендується зберігати стан Canvas через save(), а після завершення відновлювати через restore(). Це запобігає накопиченню трансформацій на наступних викликах paint.

Методи paint та shouldRepaint

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.

Приклад кастомного малювання з CustomPainter на Dart

Створимо GaugePainter — кастомний круговий датчик з анімованим заповненням. CustomPainter приймає поточне значення (progress від 0.0 до 1.0) та відмальовує дугу з градієнтом. У методі shouldRepaint порівнюється значення progress для оптимізації. CustomPaint розміщується в дереві віджетів з параметром size для обмеження області малювання.

dart
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;
    }
}

CustomPainter з Path для складних фігур

Приклад малює хвильовий графік з використанням Path. Path дозволяє будувати довільні контури з ліній, кривих Безьє та дуг. Після побудови контур заливається кольором або обводиться лінією. У прикладі графік будується на основі масиву точок, що імітують хвильову форму.

dart
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;
    }
}

ForegroundPainter та backgroundPainter: різниця

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

Головне правило продуктивності 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 у Flutter?

CustomPainter — це клас-контракт, що визначає, що і як малювати на Canvas. Canvas — сам об'єкт малювання, який надає методи (drawCircle, drawLine тощо). CustomPainter отримує Canvas через метод paint та керує його використанням. Один Canvas може використовуватися різними CustomPainter в різний час.

Чи можна використовувати CustomPainter без CustomPaint?

Ні, CustomPainter не має сенсу без CustomPaint. CustomPaint — це віджет, який викликає метод paint CustomPainter та відображає результат на екрані. Без CustomPaint Canvas не буде створено, і paint не виконається. Можна, однак, реалізувати власний RenderObject з Canvas, але це більш низькорівневий підхід.

Як анімувати CustomPainter?

Використовуйте AnimationController з Ticker, передаючи анімоване значення в CustomPainter через параметри конструктора. При зміні значення викликайте setState() у віджеті, що містить CustomPaint, що тригерить виклик shouldRepaint. Якщо shouldRepaint повертає true, система викликає paint для нового кадру анімації.

Чому CustomPainter не оновлюється при зміні даних?

Причина майже завжди в shouldRepaint, що повертає false. Перевірте, чи коректно порівнюються поля oldDelegate з новими значеннями. Якщо використовується mutable об'єкт, shouldRepaint може не помітити змін, оскільки посилання на об'єкт залишається тією самою. Використовуйте == або immutable-об'єкти для коректного порівняння.

Чи можна використовувати CustomPainter для анімації в ReorderableListView?

Так, CustomPainter використовується для відмальовування кастомних декорацій у складних списках. Однак врахуйте, що при перевпорядкуванні елементів RepaintBoundary всередині ListView кешує відмальовування кожного елемента. CustomPainter має бути легким і не містити важких операцій у paint, щоб не сповільнювати анімацію перетягування.

Підсумки

  • CustomPainter — абстрактний клас Flutter для кастомного 2D-відмальовування з методами paint та shouldRepaint
  • CustomPaint — віджет, що приймає painter (фон) та foregroundPainter (передній план) для відображення графіки
  • Canvas — об'єкт з методами малювання: drawLine, drawCircle, drawPath, drawRect, drawArc та іншими
  • Paint — клас, що визначає стиль малювання: колір, товщину, стиль (fill/stroke), прозорість та режим змішування
  • shouldRepaint — оптимізаційний метод, що повертає true лише при зміні полів, які впливають на відмальовування
  • Створення об'єктів всередині paint заборонено — всі Paint, Path, Rect мають кешуватися для 60 fps
  • RepaintBoundary — віджет для ізоляції CustomPainter від перемальовувань вищестоящого дерева віджетів

Ми розробимо мобільний застосунок під ключ

IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект

Читайте також