В предыдущем параграфе мы рассмотрели нюансы RenderObject
, а ещё ранее — анимирование. В этом и двух последующих параграфах мы объединим эти концепции: посмотрим, как можно частично обновлять интерфейс с помощью слоёв, доступных в RenderObject
через объект PaintingContext
и познакомимся с принципами построения многослойного изображения.
После вы сможете:
- Оптимизировать производительность анимаций, чтобы интерфейс оставался плавным даже при сложных переходах и движениях.
- Глубоко понимать, как именно Flutter рендерит анимации покадрово, что позволит точно настраивать поведение и избегать распространённых ошибок.
- Эффективно управлять кэшированием слоёв, снижая нагрузку на GPU и предотвращая лишние перерисовки, которые могут привести к «фризам».
Приступим!
Что такое PaintingContext
Начнём чуть издалека.
В приложениях на Flutter можно создавать плавный интерфейс произвольной сложности с поддержкой анимации. Часто при анимации изменяется только часть экрана, а изменения сводятся к движению уже готовых элементов интерфейса.
Чтобы анимация оставалась плавной даже в сложных приложениях, необходимо понимать, как Flutter управляет обновлением отдельных частей экрана и рендерит слои изображений.
В этом случае можно сберечь ресурсы графического процессора и использовать заранее построенное растровое изображение виджета. Эта возможность реализуется во Flutter с помощью слоёв. Слои поддерживают оба движка — и первоначальный, но устаревающий движок Skia, и новый движок Impeller.
Слои создаются с помощью класса PaintingContext
.
PaintingContext
— это объект, предоставляющий методы для рисования (paint
) и управления потомками в дереве рендеринга. Он используется в методеpaint
классов, наследующихся отRenderObject
.
PaintingContext
абстрагирует низкоуровневую отрисовку и упрощает реализацию paint()
в кастомных RenderObject
.
Но прежде чем поговорить о слоях, давайте сперва вспомним, как устроен процесс отрисовки приложения на экране:
- Последовательно вызываются методы
build
, и из Stateless/Stateful-виджетов создаётся полное дерево виджетов, состоящих из наследников классовProxyWidget
иRenderObjectWidget
. - Для
RenderObjectWidget
создаются объекты классов-наследниковRenderObject
, которые содержат код для определения собственных размеров, позиционирования дочернихRenderObject
и отрисовки визуального представленияRenderObject
на экране. - В
RenderObject
отрисовка происходит наCanvas
, при этом каждыйRenderObject
использует относительные координаты и размещается относительно родительскогоRenderObject
или всей поверхности для рисования для устройства, если родителем является корневойRenderView
. - Полученная последовательность операций отрисовки отправляется на GPU и с использованием фрагментных шейдеров, отвечающих за заливку и градиенты, преобразуется в пиксели на экране.
Давайте разберём каждый из этапов подробнее. Для удобства, первые два мы объединили вместе.
Этап №1: обработка дерева виджетов и создание RenderObject
Сделаем очень простое приложение с единственным корневым виджетом ColoredBox
.
1import 'package:flutter/material.dart';
2
3void main() {
4 runApp(RootWidget());
5}
6
7class RootWidget extends StatelessWidget {
8 @override
9 Widget build(BuildContext context) => const ColoredBox(color: Colors.green);
10}
В DevTools можно будет увидеть, что в финальном дереве виджетов во вкладке Widget Details Tree будут представлены и RootWidget
, который не имеет визуального представления, и ColoredBox
, который является наследником RenderObjectWidget
и связан с RenderObject
:

1class RootWidget extends StatelessWidget {
2 @override
3 Widget build(BuildContext context) {
4 SchedulerBinding.instance.addPostFrameCallback((_) {
5 debugDumpRenderTree();
6 });
7 return const ColoredBox(color: Colors.green);
8 }
9}
Посмотрим на вывод этого кода (оставлены только наиболее важные строки):
1_ReusableRenderView#771c8
2 │ view size: Size(1080.0, 2337.0) (in physical pixels)
3 │ device pixel ratio: 2.6 (physical pixels per logical pixel)
4 └─child: _RenderColoredBox#613bc
5 creator: ColoredBox ← RootWidget ← _FocusInheritedScope ←
Можно видеть, что RenderView
(класс _ReusableRenderView
) отвечает за преобразование логических пикселей в физические (в этом случае коэффициент равен 2,6), а _RenderColoredBox
— за заполнение экрана цветом.
Этап №2: отрисовка RenderObject на Canvas
Реализация _RenderColoredBox
в методе paint
вызывает единственную операцию для заливки сплошным цветом. Поверх неё выполняется отрисовка дочернего RenderObject
, но пока этот момент здесь пропустим:
1canvas.drawRect(offset & size, Paint()..color = color);
Skia
и Impeller
работают с примитивными операциями для отрисовки геометрических фигур, список которых хорошо известен по интерфейсу Canvas, а также поддерживают матричные преобразования (масштабирование, поворот, сдвиг и другие) и пиксельные фильтры.
Операции на Canvas не отправляются на GPU напрямую. Вместо этого они складываются в специальную последовательность — Display List (о ней расскажем ниже), — которую движок преобразует в команды для GPU.
Этап №3: использование списка операций для растеризации на GPU
Display List внутри графического движка Skia/Impeller преобразуется в набор команд для соответствующей графической библиотеки в зависимости от платформы:
- Android — EGL.
- MacOS, iOS — Metal.
- Linux — OpenGL.
- Windows — Angle.
Для оптимизации производительности операции разделяются на слои, результатом для каждого из них является растровое изображение. Чтобы с этим разобраться, давайте рассмотрим и разберём код, использующий низкоуровневые возможности dart:ui
для заполнения экрана красным цветом.
1import 'dart:ui';
2
3void main() {
4 final view = PlatformDispatcher.instance.implicitView;
5 final builder = SceneBuilder();
6 final pictureRecorder = PictureRecorder();
7 final canvas = Canvas(pictureRecorder);
8 canvas.drawRect(
9 Offset.zero & view!.display.size,
10 Paint()
11 ..color = const Color(0xFFFF0000)
12 ..style = PaintingStyle.fill);
13 builder.addPicture(Offset.zero, pictureRecorder.endRecording());
14 view.render(builder.build());
15}
Результат

SceneBuilder
и PictureRecorder
могут быть созданы непосредственно как объекты или с использованием методов RendererBinding.instance.createSceneBuilder()
и RendererBinding.instance.createPictureRecorder()
соответственно.
Передача последовательности действий выполняется в объекте view
(экземпляр FlutterView
), который содержит информацию об активном дисплее и поддерживает пересылку сообщений в GPU через вызов view.render
.
Сама последовательность операций представлена в сцене (интерфейс Scene
, нативная реализация во Flutter Engine представлена в виде класса _NativeScene
). Сцена создаётся в дополнительном объекте класса SceneBuilder
(нативная реализация _NativeSceneBuilder
), который поддерживает несколько действий:
Операция | Назначение |
---|---|
pushTransform |
применение матричного преобразования |
pushOffset |
добавление операции смещения на указанное количество логических пикселей |
pushClipRect и множество других Clip-операций |
применение операции обрезки видимой части |
pushOpacity |
добавление полупрозрачности |
pushColorFilter |
применение фильтра смешивания цветов |
pushImageFilter |
применение растровой операции, требует предварительной растеризации |
pushBackdropFilter |
применение операции ко всей вложенной операции (например, размытия), использует предварительную растеризацию |
pushShaderMask |
смешивание растеризованного изображения и результата вычисления фрагментного шейдера |
pop |
завершение применения операции, добавленной через push |
addRetained |
добавление ранее сгенерированного (и кэшированного) изображения, принимает ссылку на объект EngineLayer |
addPerformanceOverlay |
добавление графика времени выполнения: UI - dart-часть кода, Raster: CPU во Flutter Engine до передачи в GPU |
addTexture |
добавляет платформенную текстуру по идентификатору, может быть зарегистрирована и обновлена через embedder или в нативном коде |
addPlatformView |
добавляет обновляемое представление нативного view (View для Android, UIView для iOS) по зарегистрированному идентификатору |
addPicture |
добавляет растеризированное изображение |
Эти действия составляют единую последовательность операций — Display List (мы её упоминали выше).
Все операции push
/pop
, как следует из названия, работают со стеком операций, однако на этом уровне понятие слоя не имеет смысла и сцена создаётся просто как последовательность операций, где операции add
— терминальные. Они добавляют содержание, на которое применяется созданный к этому моменту стек преобразований.
Например, добавим смещение и цветовой фильтр к существующему изображению:
1import 'dart:ui';
2
3void main() {
4 final view = PlatformDispatcher.instance.implicitView;
5 final builder = SceneBuilder();
6 final pictureRecorder = PictureRecorder();
7 final canvas = Canvas(pictureRecorder);
8 canvas.drawRect(
9 Offset.zero & view!.display.size / 2,
10 Paint()
11 ..color = const Color(0xFFFF0000)
12 ..style = PaintingStyle.fill);
13 final picture = pictureRecorder.endRecording();
14 //генерация матрицы 5x4 для преобразования исходного RGBA в инверсный
15 final invert = List.generate(
16 20,
17 (i) => (i % 5 == 4)
18 ? (i == 19)
19 ? 0.0
20 : 255.0 //значение прозрачности
21 : (i == 18)
22 ? 1.0
23 : (i % 5 == i ~/ 5)
24 ? -1.0 //инверсия компонентов RGB
25 : 0.0);
26 builder.pushColorFilter(ColorFilter.matrix(invert));
27 builder.pushOffset(128, 128); //смещение для первого голубого прямоугольника
28 builder.addPicture(Offset.zero, picture); //голубой прямоугольник со смещением
29 builder.pop(); //снимаем со стека смещение
30 builder.addPicture(Offset.zero, picture); //голубой прямоугольник без смещения
31 builder.pop(); //снимаем со стека ColorFilter
32 builder.pushOffset(256, 256); //смещение для красного прямоугольника
33 builder.addPicture(Offset.zero, picture); //красный прямоугольник со смещением
34 builder.pop(); //снимаем смещение Offset(256,256)
35 view.render(builder.build());
36}
Результат будет таким:

Обратите внимание, что для обработки визуального представления из Flutter-виджетов используется только одна операция addPicture
(или операция addRetained
в случае, если переиспользуется ранее построенное изображение слоя).
То есть любые виджеты, включая изображения, CustomPaint
и даже тексты представляются в виде растеризованного изображения, которое может быть кэшировано или использовано в пиксельных преобразованиях, например при применении фильтра размытия.
Однако при любом изменении свойств визуальных элементов — например, при перемещении, изменении размера или цвета — всё содержимое будет заново отрисовано. Это ведёт к лишним вычислениям, снижению производительности, увеличенному потреблению памяти и энергии, особенно на мобильных устройствах.
Чтобы этого избежать, Flutter использует механизм слоёв
— структур, которые позволяют кэшировать и переиспользовать уже растеризованные изображения. Это помогает не только при анимации, но и при взаимодействии с платформенными компонентами, внедрении визуальных эффектов и оптимизации сложных интерфейсов.
В то же время слои занимают дополнительную память для хранения кэшированных изображений, поэтому использовать их стоит осознанно — только там, где это действительно оправданно с точки зрения производительности.
Слои можно создавать как на уровне RenderObject
, так и через PaintingContext
. В следующих параграфах мы последовательно рассмотрим оба подхода: сначала — работу с RenderObject
, затем — работу с PaintingContext
и управление иерархией слоёв вручную.