跟着 Flutter 3.7 的更新, dart:ui
下多了 Picture.toImageSync
和 Scene.toImageSync
这两个办法,和Picture.toImage
以及 Scene.toImage
不同的是 ,toImageSync
是一个同步履行办法,所以它不需求 await
等待,而调用 toImageSync
会直接返回一个 Image 的句柄,并在 Engine 后台会异步对这个 Image 进行光栅化处理。
前言
那 toImageSync
有什么用?不是有个 toImage
办法了,为什么要多一个 Sync 这样的同步办法?
-
现在
toImageSync
最大的特色便是图画会在 GPU 中常驻 ,所以比照toImage
生成的图画,它的制作速度会更快,并且能够重复运用,进步功率。toImage
生成的图画也能够完结 GPU 常驻,但现在没有未完结罢了。 -
toImageSync
是一个同步办法,在某些场景上弥补了toImage
有必要是异步的缺乏。
而 toImageSync
的运用场景上,官方也列举了一些用途,例如:
- 快速捕捉一张贵重的栅格化图片,用户支撑跨多帧重复运用
- 应用在图片的多路过滤器上
- 应用在自界说着色器上
具体在 Flutter Framework 里,现在 toImageSync
最直观的完结,便是被运用在 Android 默许的页面切换动画 ZoomPageTransitionsBuilder
上,满意于 toImageSync
的特性,Android 上的页面切换动画的功用,简直减少了帧光栅化一半的时刻,然后减少了掉帧和进步了刷新率。
当然,这是经过牺牲了一些其他特性来完结,后边咱们会讲到。
SnapshotWidget
前面说了 toImageSync
让 Android 的默许页面切换动画功用得到了大幅进步,那究竟是怎么完结的呢?这就要聊到 Flutter 3.7 里新增加的 SnapshotWidget
。
其实一开始 SnapshotWidget
是被界说为 RasterWidget
,从初始界说上看它的 Target 更大,可是终究在落地的时候,被简化处理为了 SnapshotWidget
,而从运用上看确实 Snapshot 更契合它的设定。
概念
SnapshotWidget
的作用是能够将 Child 变成的快照(ui.Image
)然后替换它们进行显现,简而言之便是把子控件都变成一个快照图片,而 SnapshotWidget
得到快照的办法便是 Scene.toImageSync
。
那么到这里,你应该知道为什么
toImageSync
能够进步 Android 上的页面切换动画的功用了吧?由于SnapshotWidget
会在页面跳转时把 Child 变成的快照,而toImageSync
栅格化的图片还能够跨多帧重复运用。
那么问题来了,SnapshotWidget
既然是经过 toImageSync
将 Child 变成的快照(ui.Image
)来进步功用,那么带来的副作用是什么?
答案是动画作用,由于子控件都变成了快照,所以假如 Child 控件带有动画作用,会出现“冻住”状况,更形象的比照如下图所示:
FadeUpwardsPageTransitionsBuilder | ZoomPageTransitionsBuilder |
---|---|
默许情况下 Flutter 在 Android 上的页面切换作用运用的是 ZoomPageTransitionsBuilder
,而 ZoomPageTransitionsBuilder
里在页面切换时会开启 SnapshotWidget
的截图能力,所以能够看到,它在页面跳转时,比照 FadeUpwardsPageTransitionsBuilder
动图, ZoomPageTransitionsBuilder
的赤色方块和动画会停止。
由于动画很短,所以能够在代码里设置
timeDilation = 40.0;
和SchedulerBinding.resetEpoch
来大局减慢动画履行的速度,别的能够装备MaterialApp
的ThemeData
下对应的pageTransitionsTheme
来切换页面跳转作用。
所以在官方的界说中,SnapshotWidget
是用来协助履行一些简短的动画作用,比如一些 scale 、 skew 或许 blurs 动画在一些复杂的 child 构建上开销会很大,而运用 toImageSync
完结的 SnapshotWidget
能够依靠光栅缓存:
对于一些简短的动画,例如
ZoomPageTransitionsBuilder
的页面跳转,SnapshotWidget
会将页面内的 children 都转化为快照(ui.Image
),虽然页面切换时会导致 child 动画“冻住”,可是实际页面切换时长很短,所以看不出什么反常,而带来的切换动画流畅度是清晰可见的。
再举个更直观的例子,如下代码所示,运行后咱们能够看到一个旋转的 logo 在屏幕上随机翻滚,这里分别运用了 AnimatedSlide
和 AnimatedRotation
履行移动和旋滚动画。
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,
),
),
)
假如这时候在 AnimatedRotation
上层加多一个 SnapshotWidget
,并且打开 allowSnapshotting
,能够看到此时 logo 不再滚动,由于整个 child 已经被转化为快照(ui.Image
)。
所以
SnapshotWidget
不适用于子控件还需求继续动画或有交互呼应的当地,例如轮播图。
运用
如之前的代码所示,运用 SnapshotWidget
也相对简略,你只需求装备 SnapshotController
,然后经过 allowSnapshotting
控制子控件是否烘托为快照即可。
controller.allowSnapshotting = true;
SnapshotWidget
在捕获快照时,会生成一个全新的 OffsetLayer
和 PaintingContext
,然后经过 super.paint
完结内容捕获(这也是为什么不支撑 PlatformView 的原因之一),之后经过 toImageSync
得到完整的快照(ui.Image
)数据,并交给 SnapshotPainter
进行制作。
所以 SnapshotWidget
完结图片制作会需求一个 SnapshotPainter
,默许它是经过内置的 _DefaultSnapshotPainter
完结,当然咱们也能够自界说完结 SnapshotPainter
来完结自界说逻辑。
从完结上看,
SnapshotPainter
用来制作子控件快照的接口,正如上面代码所示,会根据 child 是否支撑捕获(_childRaster == null
),然后挑选调用paint
或paintSnapshot
来完结制作。
别的,现在受制于 toImageSync
的底层完结, SnapshotWidget
无法捕获 PlatformView 子控件,假如遇到 PlatformView,SnapshotWidget
会根据 SnapshotMode
来决议它的行为:
normal | 默许行为,假如遇到无法捕获快照的子控件,直接 thrown |
---|---|
permissive | 宽松行为,遇到无法捕获快照的子控件,运用未快照的子目标烘托 |
forced | 强制行为,遇到无法捕获快照的子控件直接忽略 |
别的 SnapshotPainter
能够经过调用 notifyListeners
触发 SnapshotWidget
运用相同的光栅进行重绘,简略来说便是:
你能够在不需求从头生成新快照的情况下,对当然快照进行一些缩放、含糊、旋转等作用,这对功用会有很大进步。
所以在 SnapshotPainter
里主要需求完结的是 paint
和 paintSnapshot
两个办法:
-
paintSnapshot 是制作 child 快照时会被调用
-
paint 办法里主要是经过
painter
(对应super.paint
)这个 Callback 制作 child ,当快照被禁用或许permissive
形式下遭受 PlatformView 时会调用此办法
举个例子,如下代码所示,在 paintSnapshot
办法里,经过调整 Paint ..color
,能够在前面的小 Logo 快照上增加透明度作用:
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
里,然后经过对 animation 的状况进行管理,然后经过 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();
除此之外,dart:ui
里的 Scene
和 _Image
目标其实都是 NativeFieldWrapperClass1
,曾经咱们解释过:NativeFieldWrapperClass1
便是它的逻辑是由不同渠道的 Engine 区分完结 。
所以假如你直接在
flutter/bin/cache/pkg/sky_engine/lib/ui/compositing.dart
下去断点toImageSync
是无法成功履行到断点位置的,由于它的实在完结在对应渠道的 Engine 完结。
别的,前面咱们一直说 toImageSync
比照 toImage
是 GPU 常驻,那它们的差异在哪里?从上图咱们就能够看出:
-
toImageSync
履行了Scene:RasterizeToImage
并返回Dart_Null
句柄 -
toImage
履行了Picture:RasterizeLayerTreeToImage
并直接返回
简略打开来说,便是:
-
toImageSync
终究是经过SkImage::MakeFromTexture
经过纹路得到一个 GPUSkImage
图片 -
toImage
是经过makeImageSnapshot
和makeRasterImage
生成SkImage
,makeRasterImage
是一个仿制图画到 CPU 内存的操作。
其实一开始 toImageSync
是被指令为 toGpuImage
,可是为了更形象通用,最终才修改为 toImageSync
。
而 toImageSync
等相关功用的落地能够说相同历经了漫长的评论,关于是否供给这样一个 API 到终究落地,其履行难度一点点不比 background isolate 简略,比如:是否界说反常场景,遇到错误是否需求在Framwork 层消化,是否真的需求这样的接口来进步功用等等。
而 toImageSync
等相关功用终究能落地,其间最重要的一点我认为是:
toGoulmage
gives the framework the ability to take performance into their own hands, which is important given that our priorities don’t always line up.
最终
toImageSync
仅仅一个简略的 API ,可是它的背后阅历了许多故事,一起 toImageSync
和它对应的封装 SnapshotWidget
,终究的目的便是进步 Flutter 运行的功用。
或许现在对于你来说 toImageSync
并不是有必要的,乃至 SnapshotWidget
看起来也很鸡肋,可是一旦你需求处理复杂的制作场景时, toImageSync
便是你必不可少的菜刀。