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 се използва заедно с widget-а CustomPaint, който делегира изобразяването на подадения обект CustomPainter на всеки кадър от анимация или промяна на състояние.

Основни моменти

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

Какво е 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 и 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 при всяка актуализация на дървото от widget-и, която засяга 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 се поставя в дървото от widget-и с параметър 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 рисува след детето на CustomPaint, т.е. върху него. Използва се за наслагвания: отблясъци, маски, измервателни линийки, анотации.

Разликата между 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 — widget-обвивка, който предотвратява прерисуването на CustomPainter при промяна на горното дърво от widget-и. Ако CustomPainter е статичен, увийте го в RepaintBoundary за изолация.

За анимирани CustomPainter с често прерисуване използвайте Ticker или AnimationController с shouldRepaint, връщащ true при промяна на анимираната стойност. Оптимално е да сравнявате анимираното поле в shouldRepaint, а не да прерисувате на всеки кадър без необходимост. Rect кеширане за сложни градиенти: ако градиентът не променя цветовете, може да кеширате Shader чрез Paint.shader.

Често задавани въпроси

Каква е разликата между CustomPainter и Canvas във Flutter?

CustomPainter — е клас-договор, който определя какво и как да се рисува върху Canvas. Canvas — самият обект за рисуване, който предоставя методи (drawCircle, drawLine и т.н.). CustomPainter получава Canvas чрез метода paint и управлява използването му. Един Canvas може да се използва от различни CustomPainter по различно време.

Може ли да се използва CustomPainter без CustomPaint?

Не, CustomPainter няма смисъл без CustomPaint. CustomPaint е widget-ът, който извиква метода paint на CustomPainter и показва резултата на екрана. Без CustomPaint Canvas няма да бъде създаден и paint няма да се изпълни. Може обаче да имплементирате собствен RenderObject с Canvas, но това е подход от по-ниско ниво.

Как да анимирам CustomPainter?

Използвайте AnimationController с Ticker, като предавате анимираната стойност на CustomPainter чрез параметрите на конструктора. При промяна на стойността извикайте setState() в widget-а, съдържащ CustomPaint, което задейства shouldRepaint. Ако shouldRepaint върне true, системата извиква paint за новия кадър на анимацията.

Защо CustomPainter не се актуализира при промяна на данни?

Причината почти винаги е shouldRepaint, който връща false. Проверете дали полетата на oldDelegate се сравняват правилно с новите стойности. Ако се използва променяем обект, shouldRepaint може да не забележи промените, тъй като референцията към обекта остава същата. Използвайте == или неизменяеми обекти за правилно сравнение.

Може ли да се използва CustomPainter за анимация в ReorderableListView?

Да, CustomPainter се използва за рисуване на персонализирани декорации в сложни списъци. Имайте предвид обаче, че при пренареждане на елементи RepaintBoundary вътре в ListView кешира изобразяването на всеки елемент. CustomPainter трябва да бъде лек и да не съдържа тежки операции в paint, за да не забавя анимацията на влачене.

Обобщение

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

Ще разработим мобилно приложение под ключ

IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.

Обсъдете проекта

Прочетете също