Чтобы эффективно управлять отрисовкой в Flutter и минимизировать избыточные пересчёты, важно понимать, как именно устроен механизм взаимодействия между RenderObject и слоями.
В этом параграфе мы подробнее рассмотрим PaintingContext — ключевой компонент, участвующий в построении дерева слоёв и связывающий логику RenderObject с низкоуровневым отображением на экране. Разберёмся, какие операции доступны внутри PaintingContext, как устроены различные типы слоёв, а также как управлять их созданием, вложенностью и ограничениями области отрисовки.
PaintingContext и ClipContext
PaintingContext — это одна из реализаций ClipContext. ClipContext предоставляет методы для обрезки произвольного слоя:
clipPathAndPaint— отрисовка слоя и обрезка по произвольному пути.clipRRectAndPaint— отрисовка и обрезка по границам скруглённого прямоугольника.clipRectAndPaint— отрисовка и обрезка по прямоугольнику.
PaintingContext инициализируется с использованием двух параметров:
- Контейнерного слоя —
ContainerLayer, в который будет добавлено содержимое, созданное в процессе отрисовки. - Ограничивающей области (bounds) — прямоугольника в логических пикселях, определяющего размер и позицию области рисования внутри контейнерного слоя.
Для приложения в целом в качестве контейнерного слоя, на основе которого создаётся PaintingContext, используется TransformLayer, созданный в RenderView.
Для визуализации RenderObject используется метод paintChild, который выполняет отрисовку дочернего RenderObject с указанным смещением. Каждый создаваемый RenderObject получает собственный PaintingContext на основе родительского слоя.
В зависимости от флага isRepaintBoundary либо формируется новый слой содержания (если флаг true), либо передаётся текущий paintingContext и RenderObject создаётся непосредственно в текущем слое. Изображение создаётся на PictureLayer, который встраивается внутрь контейнерного слоя OffsetLayer на основе Canvas + PictureRecorder. Использование canvas не отличается от рассмотренного ранее в параграфе про CustomPaint.
Добавление слоёв через методы PaintingContext
Для RenderObject можно создать иерархию слоёв любой сложности с использованием методов PaintingContext для добавления слоёв (push), callback-метод которого получает новый контекст, привязанный к собственному слою. Кроме специализированных методов (pushOpacity, pushClipRect, ...) существует обобщённый метод pushLayer, который может использоваться для добавления слоёв:
|
Тип слоя |
Назначение |
Примечание |
|
|
Изображение слоя, используется совместно с |
Используется как поверхность для рисования и на его основе, через создание |
|
|
Слой для отображения текстуры, полученной от нативной платформы |
Определяется по |
|
|
Слой для отображения платформенного компонента |
Определяется по |
|
|
Слой для графика времени отрисовки кадра (UI / Raster Task Runner) |
Используется при добавлении флага |
|
|
Определяет сдвиг отображаемого на слое содержимого на указанное смещение (этот и последующие слои являются контейнерами |
Дополнительно добавляет возможность создания снимка изображения слоя в список байтов |
|
|
Применение матричного преобразования для содержимого |
Использует |
|
|
Обрезка по границе прямоугольника |
Также может использоваться |
|
|
Обрезка по границе скругленного прямоугольника |
Также |
|
|
Применение цветового фильтра к растровому изображению слоя |
Также |
|
|
Применение фильтра к изображению, размещенному через RawImage или полученному через растеризацию |
Также |
|
|
Использование шейдера и смешивания цветов над растровым изображением слоя |
Также |
|
|
Добавление полупрозрачности к растровому изображению слоя |
Также |
|
|
Управляющий и управляемый слой, первый накапливает примененные трансформации в матричное преобразование, второй связывается с первым, отслеживает изменение преобразований и применяет его ко всем размещенным на нем объектам |
Используется в составных виджетах, расположение которых зависит от положения другого виджета (например, |
|
|
Добавление слоя с произвольными данными |
Данные могут быть извлечены из любого слоя выше в иерархии через |
Слои могут содержать метаданные, которые связаны с определённой областью на экране, — например, можно создать слой для маркировки области уведомлений на экране телефона.
Для хранения метаданных используется слой AnnotatedRegionLayer, который также создаётся через виджет AnnotatedRegion с сохранением типизированных данных, которые могут быть извлечены из слоёв, находящихся выше по иерархии, через вызов findAnnotations<T>() (заполняет список) и find<T> (возвращает найденный объект с данными указанного типа или null). Аннотация относится к набору пикселей в границах слоя.
Использование метода addLayer и копирование слоёв
Существующий слой можно вручную добавить через PaintingContext.addLayer(). Однако важно помнить:
-
Один и тот же слой нельзя использовать повторно в нескольких местах.
-
Если требуется копия, необходимо:
- Выполнить растеризацию существующего слоя (
toImage()илиtoImageSync()). - Создать новый
PictureLayer. - Добавить его вручную через
addLayer().
- Выполнить растеризацию существующего слоя (
Аналогично можно программно создать слой для рисования через Canvas:
1final _screenshotCurrentLayer = PictureLayer(estimatedBounds);
2final _screenshotRecorder = ui.PictureRecorder();
3final _screenshotCanvas = Canvas(_screenshotRecorder!);
4final rect = Offset.zero & const Size(64, 64);
5final OffsetLayer offsetLayer = OffsetLayer();
6final PaintingContext context = PaintingContext(offsetLayer, rect);
7canvas.drawRect(
8 rect,
9 Paint()
10 ..style = PaintingStyle.fill
11 ..color = Colors.green);
12addLayer(_screenshotCurrentLayer!);
Создание слоёв в памяти
Слои можно создавать вне визуального дерева, полностью в памяти: PaintingContext может быть создан полностью в памяти, для этого в него передаётся корневой контейнерный слой (например, OffsetLayer), который может накапливать другие слои.
1final rect = Offset.zero & const Size(64, 64);
2final OffsetLayer offsetLayer = OffsetLayer();
3final PaintingContext context = PaintingContext(offsetLayer, rect);
4context.canvas.drawRect(
5 rect,
6 Paint()
7 ..style = PaintingStyle.fill
8 ..color = Colors.green);
9final image = offsetLayer.toImageSync(rect);
10print(image.width);
11print(image.height);
Также доступ к PaintingContext есть в любом RenderObject (экземпляр объекта передаётся в метод paint). Важно отметить, что несмотря на указанные границы, слой всегда занимает всю отображаемую поверхность, и в методе paint отрисовка может происходить даже за пределами указанного прямоугольника, в том числе с использованием отрицательных координат.
Чтобы этого избежать, можно использовать дополнительный clip-слой, который применяется к своему поддереву и ограничивает видимые изменения границей зоны обрезки (все остальные пиксели остаются прозрачными).
Также нет никаких ограничений на количество слоёв, добавляемых внутри paint, при этом если среди них есть OffsetLayer, то он будет использоваться для нового PaintingContext при вызове paintChild. В противном случае дочерний RenderObject будет изображаться в пределах того же слоя, что и родительский.
Этот подход активно применяется при генерации изображений, предварительном рендеринге и offscreen-композиции. Чтобы лучше понять, как работает взаимодействие PaintingContext и слоёв в реальных сценариях, перейдём к практическим примерам.
Примеры
Для тестирования мы будем использовать изображение ниже. Чтобы избежать проблемы с CORS в DartPad, мы добавили его в код в виде base64-строки:

Для отладки слоёв можно использовать функцию debugDumpLayerTree() из пакета flutter/rendering.dart. Добавим к первому примеру с простым деревом вывод списка слоёв в консоль при нажатии на кнопку:
Посмотрим на результаты вывода:

Здесь:
TransformLayer(владелец —RenderView) — основной экран с трансформацией масштабирования для преобразования координат в пиксели на экране, например при hidpi-разрешениях.- Внешний
OffsetLayer—RepaintBoundaryизMaterialAppвFocusScope. - Внутренний
OffsetLayer, владелецRepaintBoundary, — слой, соответствующийRepaintBoundary, который Flutter автоматически создаёт вокруг builder-функции, указанной в параметре home у MaterialApp. PictureLayer— изображение нашей сцены, всяColumnвместе с растровым изображением и кнопкой.
Таким образом, при появлении splash-эффекта над кнопкой одновременно с этим перерисовывается и изображение заката над морем. Чтобы этого избежать, добавим RepaintBoundary вокруг кнопки.
В дереве слоёв появится дополнительная ветка, а растровое изображение теперь будет кэшироваться.
Результат

Здесь первый PictureLayer — это фон приложения, второй — изображение заката и третий — изображение кнопки. Теперь при анимации нажатия кнопки фон и закат не будут перерисовываться, а будут переиспользоваться их растровые кэши на этапе композиции слоёв в GPU.
Аналогично при добавлении ряда виджетов будут создаваться промежуточные слои, например можно добавить размытие и полупрозрачность:

Вот как это будет реализовано в DartPad:
Результат

Здесь слои OpacityLayer (полупрозрачность) и ImageFilterLayer (размытия) используются для постобработки растрового изображения заката (PictureLayer)
Для доступа к RenderObject, соответствующему текущему элементу (BuildContext), можно использовать метод findRenderObject (может вернуть null, если к элементу не был присоединён RenderObject) или использовать глобальный ключ:
При этом стоит помнить: слои требуют дополнительной памяти для хранения растеризованных данных, и избыточное использование может привести к повышенной нагрузке на систему.
Понимание того, как работают слои и как передаётся информация в GPU, помогает создавать оптимизированные, отзывчивые и энергоэффективные Flutter-приложения.
Создание собственных типов слоёв
Этот подраздел рассматривает сценарий изменения Flutter Engine, и к нему следует обращаться только в особых случаях. В большинстве случаев можно исключить необходимость создания нового типа слоя через следующие решения:
- Использовать
PictureLayerи заранее растеризовать нужный эффект. - Комбинировать существующие слои (
OpacityLayer+ImageFilterLayer+ShaderMaskLayer). - Применить кастомный
FragmentShaderчерезFragmentProgramи отрисовать наCanvas.
В рамках обычного Flutter-приложения создать новый тип слоя нельзя. Все слои (Layer, ContainerLayer, PictureLayer, OpacityLayer и др.) определены в C++-части движка Flutter и используются через dart:ui в ограниченном виде. Они реализуются внутри Flutter Engine, и Dart-часть просто вызывает готовые нативные функции.
Если вы работаете с собственной сборкой Flutter Engine, тогда да — вы можете добавить новый тип слоя.
Вот общий путь:
- Внести изменения в движок
- Изменить Dart API
Кратко рассмотрим обе.
Изменения в движке
Flutter Engine — это open-source. Чтобы реализовать новый тип слоя, нужно:
- Добавить новый класс, наследующий от
flutter::Layerвengine/flow/layers/Например:class CustomShaderLayer : public Layer. - Переопределить метод
Paint(), который будет вызывать вашу кастомную реализацию с помощью Skia или Impeller. - При необходимости — добавить кэширование через
Preroll()и работу сRasterCache.
Примерный шаблон слоя можно взять из существующих реализаций:
Изменения в Dart API
После создания слоя на C++-стороне нужно:
- добавить поддержку новой push-операции в scene_ builder.cc ,
- добавить поддержку операции в compositing.dart,
- использовать в приложении.
После этого ваш слой можно использовать через кастомные RenderObject или PaintingContext.pushLayer(...), как и встроенные.
Рассмотрим на верхнеуровневом примере.
Пример идеи: GlowLayer
Представим, что вы хотите слой, который добавляет glow-эффект к содержимому. Тогда вам нужно:
- Передать параметры эффекта (радиус, цвет).
- В
paint()реализовать рендеринг glow-ауры через Skia Blur и наложение поверх содержимого. - Использовать
pushGlowLayer()в Dart-коде, передавая child callback.
Но у этой идеи есть подводные камни:
- Это требует форка Flutter Engine и поддержки собственной сборки (неподдерживаемо в обычном pub-пакете).
- Ваш код становится несовместим с официальными обновлениями Flutter без ручной синхронизации.
- Поддержка iOS и Android требует отдельного тестирования на уровне движка.
Вот мы и разобрались с нюансами анимаций во Flutter! Давайте коротко вспомним, о чём шла речь.
Flutter использует систему слоёв для оптимизации отрисовки интерфейса и повышения производительности. Вместо того чтобы перерисовывать весь экран при любом изменении, фреймворк делит визуальное представление на отдельные растровые фрагменты (слои), которые можно повторно использовать. Это позволяет ограничить зону обновления и минимизировать вычислительные затраты при анимации, взаимодействии с пользователем и работе с платформенными компонентами.
Вы узнали, как:
- слои создаются в процессе отрисовки
RenderObject; - работает и связан с системой слоёв
PaintingContext; - разные типы слоёв (например,
OpacityLayer,ClipRectLayer,PictureLayer) помогают реализовывать визуальные эффекты; - использовать
RepaintBoundaryдля ограничения области перерисовки; - интерпретировать дерево слоёв с помощью
debugDumpLayerTreeи инструментов Flutter DevTools; - кэшируются и переиспользуются растровые представления виджетов;
- выполнять offscreen-рендеринг и получать изображение слоя с помощью
toImage.
Важно помнить, что вместе с преимуществами кэширования растровые слои увеличивают использование памяти. Поэтому не стоит чрезмерно оборачивать каждый виджет в RepaintBoundary или создавать новые слои без необходимости — разумное использование слоёв помогает достичь баланса между производительностью и потреблением ресурсов.
А вам интересно углубиться в эту тему, под катом мы собрали ссылки на дополнительные материалы.
Для самостоятельного изучения
Официальная документация и статьи:
-
Flutter rendering: Layers — документация Flutter о процессе отрисовки.
-
RepaintBoundary — описание виджета, ограничивающего область перерисовки.
-
Understanding Composited Layers to improve the performance of Flutter apps — практическое руководство по использованию слоёв для повышения производительности.
-
Код движка Flutter. Flutter Engine реализует работу со слоями и кэшированием в следующих модулях:
-
Анализ производительности:
В DevTools можно отслеживать следующие метрики, связанные с кэшированием:
-
LayerCount— количество слоёв; -
LayerMBytes— объём слоёв в памяти; -
PictureCount— количество кэшированных изображений; -
PictureMBytes— объём кэшированных изображений в памяти.Эти данные отображаются в графиках памяти (Memory View), что помогает анализировать использование ресурсов на устройстве и выявлять узкие места в производительности.
В следующем параграфе мы подробнее разберёмся с семантической разметкой интерфейса (о ней мы уже чуть-чуть поговорили) и научимся создавать приложения, которыми смогут пользоваться люди с ограниченными возможностями здоровья.
