С выходом Flutter 3.7 появились два новых метода в пространстве имён dart:ui — Picture.toImageSync и Scene.toImageSync. В отличие от существующих методов Picture.toImage и Scene.toImage, методы с суффиксом Sync являются синхронными, поэтому они не требуют использования ключевого слова await.
Вызов метода toImageSync сразу возвращает объект типа Image, а процесс растрирования происходит асинхронно в фоновом режиме внутри движка.
Что же такое toImageSync? Почему был создан этот синхронный метод, если уже существует метод toImage?
Основной особенностью метода toImageSync является то, что изображение постоянно хранится в памяти GPU, что позволяет значительно увеличить скорость его отрисовки и повторно использовать его, повышая производительность.
Метод toImageSync является синхронным, что позволяет компенсировать недостаток асинхронного метода toImage в некоторых случаях.
Официальная документация также приводит несколько примеров использования метода toImageSync:
toImageSync
реализовано по умолчанию в Android при использовании анимации перехода страниц ZoomPageTransitionsBuilder
. Благодаря особенностям метода toImageSync
производительность анимации перехода страниц на платформе Android была увеличена почти вдвое, что позволило снизить количество пропущенных кадров и повысить частоту обновления экрана.> Однако стоит отметить, что это достигнуто за счёт отказа от некоторых других возможностей, которые мы рассмотрим далее.Ранее было упомянуто, что метод toImageSync
значительно улучшил производительность анимации перехода страниц по умолчанию на Android. Но как именно это было сделано? Для ответа на этот вопрос следует обратиться к новому виджету Flutter 3.7 — SnapshotWidget.
Начальный вариант виджета назывался RasterWidget, но в конечном итоге он был упрощён до SnapshotWidget, так как последний лучше всего соответствует его целям.
SnapshotWidget
предназначен для преобразования потомков в снимок (ui.Image
) и замены ими, другими словами, все потомки превращаются в одно изображение-снимок. Получение снимка осуществляется методом Scene.toImageSync
.
Теперь вы должны понять, почему
toImageSync
повышает производительность анимации перехода страниц на Android? Это происходит потому чтоSnapshotWidget
преобразует потомков в снимок при переходе между страницами, а полученные изображения могут повторно использоваться во многих кадрах.
Итак, вопрос заключается в том, какие побочные эффекты вызывает использование SnapshotWidget
, который преобразует потомков в снимок (ui.Image
) для повышения производительности?Ответ заключается в том, что это влияет на анимацию. Потому что все потомки становятся снимками; если они имеют анимационные эффекты, то эти эффекты будут "заморожены" , более наглядный пример представлен ниже:| FadeUpwardsPageTransitionsBuilder | ZoomPageTransitionsBuilder |
| ------------------------------------------------------------ | ------------------------------------------------------------ |
| |
|
По умолчанию Flutter использует ZoomPageTransitionsBuilder
для анимации перехода страниц на Android, и этот билдер активирует возможность создания снимков SnapshotWidget
при переходах между страницами. Поэтому можно заметить, что при переходе страниц с помощью ZoomPageTransitionsBuilder
красный квадрат и анимация "копилки" останавливаются, в отличие от FadeUpwardsPageTransitionsBuilder
.
Из-за короткого времени выполнения анимации, её скорость может быть глобально снижена путём установки
timeDilation = 40.0;
и вызоваSchedulerBinding.resetEpoch()
. Также можно настроитьpageTransitionsTheme
в темеThemeData
компонентаMaterialApp
для изменения эффекта перехода страниц.Таким образом, согласно официальной документации,SnapshotWidget
используется для помощи в реализации краткосрочных анимационных эффектов, таких как масштабирование, наклон или размытие. Эти анимации могут быть дорогостоящими при сложной структуре потомков, но использованиеtoImageSync
позволяет использовать буферизированное изображение. Для некоторых коротких анимаций, таких как переходы страниц с помощьюZoomPageTransitionsBuilder
, компонентSnapshotWidget
преобразует всех потомков страницы в снимки (ui.Image
). Хотя это может привести к "заморозке" анимации потомков при смене страниц, время самого перехода между страницами очень мало, поэтому никаких аномалий заметно не будет, в то же время плавность переходной анимации становится очевидной. Для примера ниже показан код, который после запуска отображает вращающийся логотип, случайно перемещающийся по экрану. В этом примере используютсяAnimatedSlide
иAnimatedRotation
, чтобы выполнить анимацию перемещения и вращения соответственно.```dart Timer.periodic(const Duration(seconds: 2), (timer) { final random = Random(); x = random.nextInt(6) - 3; y = random.nextInt(6) - 3; r = random.nextDouble() * 2 * pi; setState(() {}); });
AnimatedSlide( offset: Offset(x.floorToDouble(), y.floorToDouble()), duration: Duration(milliseconds: 1500), curve: Curves.easeInOut, child: AnimatedRotation( turns: r, duration: Duration(milliseconds: 1500), child: Image.asset( 'static/test_logo.png', width: 100, height: 100, ), ), )

Если теперь добавить `SnapshotWidget` поверх `AnimatedRotation` и активировать `allowSnapshotting`, можно заметить, что логотип больше не вращается, так как весь его потомок уже преобразован в снимок (`ui.Image`).
|  |  |
| ----------------------------------------------------------------------------- | ------------------------------------------------------------ |
Поэтому `SnapshotWidget` не подходит для мест, где потомкам требуется продолжение анимации или реакция на взаимодействие пользователя, например, для слайдера.
## Использование
Как видно из предыдущего кода, использование `SnapshotWidget` также довольно просто — вам нужно лишь настроить `SnapshotController` и контролировать через `allowSnapshotting`, будет ли потомок рендериться как снимок.
```dart
controller.allowSnapshotting = true;
При захвате снимка SnapshotWidget
создаёт новый OffsetLayer
и PaintingContext
, затем использует super.paint
для захвата содержимого (это одна из причин, почему он не поддерживает PlatformView
). После этого с помощью toImageSync
получается полный снимок (ui.Image
) данных, которые передаются SnapshotPainter
для рисования.| |
|
| ------------------------------------------------------------------- | ------------------------------------------------------------ |
Таким образом, для рисования снимка SnapshotWidget
требуется SnapshotPainter
. По умолчанию это реализуется с помощью встроенного _DefaultSnapshotPainter
, но вы можете создать свой собственный SnapshotPainter
, чтобы реализовать специфическую логику. SnapshotPainter
используется для отрисовки интерфейса снимков вложенных компонентов. Как показано выше, он выбирает между вызовом методов paint
и paintSnapshot
, в зависимости от того, поддерживает ли дочерний компонент захват (_childRaster == null
).
Кроме того, в настоящее время из-за ограничений в реализации метода toImageSync
, компонент SnapshotWidget
не может захватывать вложенные компоненты типа PlatformView
. В случае встречи с PlatformView
, поведение SnapshotWidget
зависит от значения свойства SnapshotMode
:
режим | описание |
---|---|
normal | По умолчанию выбрасывает ошибку при попытке захвата незахватываемого вложенного компонента |
permissive | При встрече с незахватываемым вложенным компонентом использует его без захвата |
forced | Пропускает незахватываемый вложенный компонент |
Вы можете применять эффекты, такие как масштабирование, размытие, вращение и т.д., к текущему снимку без необходимости создания нового снимка, что значительно повышает производительность.
Поэтому в SnapshotPainter
основное внимание уделяется реализации двух методов — paint
и paintSnapshot
.
Метод paintSnapshot
вызывается при отрисовке снимка вложенного компонента.
Метод paint
использует callback painter
(соответствующий super.paint
) для отрисовки вложенного компонента. Этот метод будет вызван, если снимок недоступен или если в режиме permissive
встретился PlatformView
.
Например, как показано ниже, в методе paintSnapshot
можно добавить прозрачность к маленькому логотипу, используя параметр Paint..color
:
class TestPainter extends SnapshotPainter {
final Animation<double> animation;
TestPainter({required this.animation});
@override
void paint(PaintingContext context, ui.Offset offset, Size size,
PaintingContextCallback painter) {}
@override
void paintSnapshot(PaintingContext context, Offset offset, Size size,
ui.Image image, Size sourceSize, double pixelRatio) {
final Rect src = Rect.fromLTWH(
0, 0, sourceSize.width, sourceSize.height);
final Rect dst = Rect.fromLTWH(
offset.dx, offset.dy, size.width, size.height);
final Paint paint = Paint()
..color = Color.fromRGBO(0, 0, 0, animation.value)
..filterQuality = FilterQuality.low;
context.canvas.drawImageRect(image, src, dst, paint);
}
@override
void dispose() {
super.dispose();
}
}
``` @override
bool shouldRepaint(covariant TestPainter oldDelegate) {
return oldDelegate.animation.value != animation.value;
}
}
На самом деле можно переместить анимацию движения в метод paintSnapshot
, а затем управлять состоянием анимации через вызов notifyListeners
для прямого обновления отрисовки снимка. Это обеспечивает лучшую производительность, как это реализовано в Android в ZoomPageTransitionsBuilder
.
animation.addListener(notifyListeners);
animation.addStatusListener(_onStatusChange);
void _onStatusChange(_) {
notifyListeners();
}
@override
void paintSnapshot(PaintingContext context, Offset offset, Size size, ui.Image image, Size sourceSize, double pixelRatio) {
_drawMove(context, offset, size);
}
@override
void paint(PaintingContext context, ui.Offset offset, Size size, PaintingContextCallback painter) {
switch (animation.status) {
case AnimationStatus.completed:
case AnimationStatus.dismissed:
return painter(context, offset);
case AnimationStatus.forward:
case AnimationStatus.reverse:
}
...
}
Для более подробной информации обратитесь к реализации системы
ZoomPageTransitionsBuilder
.
Кроме SnapshotWidget
, RepaintBoundary
также поддерживает toImageSync
. Поскольку toImageSync
получает постоянные данные из GPU, то теоретически при реализации сценария отрисовки скриншотов и выделения областей, должна быть достигнута лучшая производительность.
final RenderRepaintBoundary boundary = globalKey.currentContext!.findRenderObject()! as RenderRepaintBoundary;
final ui.Image image = boundary.toImageSync();
```Кроме того, объекты `Scene` и `_Image` в модуле `dart:ui` являются `NativeFieldWrapperClass1`. Мы уже объясняли, что **`NativeFieldWrapperClass1` имеет логику, которая различается между платформами благодаря движку**.|  |  |
| --------------------------------------------------------- | --------------------------------------------------------- |
Поэтому если вы установите точку останова в `flutter/bin/cache/pkg/sky_engine/lib/ui/compositing.dart` для метода `toImageSync`, то он может не сработать, так как его реальная реализация находится в платформозависимой части движка.

Кроме того, мы говорили ранее, что `toImageSync` отличается от `toImage` тем, что он постоянно присутствует в памяти GPU. Но где же заключаются различия между ними? Из приведённой выше диаграммы можно заметить:
- `toImageSync` выполняет `Scene:RasterizeToImage` и возвращает обработчик `Dart_Null`
- `toImage` выполняет `Picture:RasterizeLayerTreeToImage` и сразу возвращает результат
Проще говоря:
- `toImageSync` в конечном итоге использует `SkImage::MakeFromTexture` для получения `GPU SkImage` через текстуру
- `toImage` создаёт `SkImage` с помощью `makeImageSnapshot` и `makeRasterImage`. Последний представляет собой операцию копирования изображения в память процессора.
|  |  |  |  |
| --------------------------------------------------------- | --------------------------------------------------------- | --------------------------------------------------------- | --------------------------------------------------------- |
Изначально метод `toImageSync` был назван `toGpuImage`, но затем название было изменено на более универсальное `toImageSync`.
Разработка функциональности, связанной с `toImageSync`, также прошла долгий период обсуждений. Вопрос о том, следует ли предоставлять такой API и как его внедрять, был очень сложным, не менее трудным, чем [фоновый изолятор](https://juejin.cn/post/7195825738472620087). Например, были рассмотрены вопросы о необходимости определения сценариев ошибок, обработки этих ошибок на уровне фреймворка, а также о целесообразности использования такого API для повышения производительности.
|  |  |  |  |
| --------------------------------------------------------- | --------------------------------------------------------- | --------------------------------------------------------- | --------------------------------------------------------- |
А `toImageSync` и связанные с ним функции были реализованы благодаря одному очень важному аспекту, который, по моему мнению, заключается в следующем:
>`toGoulmage` предоставляет фреймворку возможность самостоятельно контролировать производительность, что важно, учитывая, что наши приоритеты не всегда совпадают.
# В заключение
`toImageSync` — это всего лишь простой API, но за его созданием стоит множество историй. Одновременно `toImageSync` и его обёртка `SnapshotWidget` имеют своей конечной целью повышение производительности работы с Flutter.Может быть, в данный момент `toImageSync` вам не требуется, а `SnapshotWidget` кажется лишним, но как только вы столкнетесь с сложными сценариями отрисовки, `toImageSync` станет вашим неотъемлемым инструментом.
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )