Flutter -绘制原理及Layer

377 阅读6分钟

Flutter绘制原理

Flutter中和绘制相关的对象有三个:Canvas、Layer、Scene:

  • Canvas:封装了Flutter Skia各种绘制指令,比如画线、画圆、画矩形等指令
  • Layer:分为容器类和绘制类两种;暂时理解为绘制产物的载体,比如调用Canvas的绘制API后,相应的绘制产物被保存在PictureLayer.picture对象中
  • Scene:屏幕上将要显示的元素。在上屏前需要将Layer中保存的绘制产物关联到Scene上。

Flutter绘制流程:

  1. 构建一个Canvas,用于绘制;同时还需要创建一个绘制指令记录器,因为绘制指令最终是要传递给Skia的,而Canvas可能会连续发起多条绘制指令,指令记录器用于收集Canvas在一段时间内所有的绘制指令,因此Canvas构造函数第一个参数必须传递一个PictureRecorder实例。
  2. Canvas绘制完成后,通过PictureRecorder获取绘制产物,然后将其保存在Layer中。
  3. 构建Scene对象,将Layer的绘制产物和Scene关联起来。
  4. 上屏;调用window.render API将Scene上的绘制产物发送给GPU。

之前绘制棋盘的例子,无论是通过CustomPaint还是自定义RenderObject,都是在Flutter的Widget框架下进行的绘制,实际上,最终到底层Flutter都会按照上述流程去完成绘制。那么也可以直接在main函数中调用这些底层API来完成。

void main() {
  //1.创建绘制记录器和Canvas
  PictureRecorder recorder = PictureRecorder();
  Canvas canvas = Canvas(recorder);
  //2.在指定位置区域绘制。
  var rect = Rect.fromLTWH(30, 200, 300,300 );
  drawChessboard(canvas,rect); //画棋盘
  drawPieces(canvas,rect);//画棋子
  //3.创建layer,将绘制的产物保存在layer中
  var pictureLayer = PictureLayer(rect);
  //recorder.endRecording()获取绘制产物。
  pictureLayer.picture = recorder.endRecording();
  var rootLayer = OffsetLayer();
  rootLayer.append(pictureLayer);
  //4.上屏,将绘制的内容显示在屏幕上。
  final SceneBuilder builder = SceneBuilder();
  final Scene scene = rootLayer.buildScene(builder);
  window.render(scene);
}

Picture

PictureLayer的绘制产物是Picture,Picture有两点需要阐明:

  1. Picture实际上是一系列的图形绘制操作指令,可以参考Picture类源码的注释。
  2. Picture要显示在屏幕上,必然会经过光栅化,随后Flutter会将光栅化后的位图信息缓存起来,也就是说同一个Picture对象,其绘制指令只会执行一次,执行完成后绘制的位图就会被缓存起来。

综上,可以看到Picture Layer的绘制产物一开始是一系列绘图指令,当第一次绘制完成后,位图信息就会被缓存起来,绘制指令也就不会再被执行,所以这时绘制产物就是位图了。

Canvas绘制的位图转图片

既然Picture中保存的是绘制产物,那么它也应该能提供一个方法将绘制产物导出,实际上Picture有一个toImage方法,可以根据指定的大小导出Image。

//将图片导出为Uint8List
final Image image = await pictureLayer.picture.toImage();
final ByteData? byteData = await image.toByteData(format: ImageByteFormat.png);
final Uint8List pngBytes = byteData!.buffer.asUint8List();
print(pngBytes);

Layer

Layer作为绘制产物的持有者有什么作用?

  1. 可以在不同的frame之间复用绘制产物(如没有发生变化)
  2. 划分绘制边界,缩小重绘范围。

Layer类型

  1. OffsetLayer:根Layer,它继承自ContainerLayer,而ContainerLayer继承自Layer类,将直接继承自COntainerLayer类的Layer称为容器类Layer。容器类Layer可以添加任意多个子Layer。
  2. PictureLayer:保存绘制产物的Layer,它直接继承自Layer类。将可以直接承载或关联绘制结果Layer称为绘制类Layer。

容器类Layer

容器类Layer的作用和使用场景:

  1. 将组件树的绘制结构组成一棵树。
    因为Flutter中的Widget是树状结构,那么相应的RenderObject对应的绘制结构也是树状结构,Flutter会根据一些特定的规则为组件树生成一棵Layer树,而容器类Layer就可以组成树状结构(父Layer可以包含任意多个子Layer,子Layer又可以包含任意多个子Layer)。
  2. 可以对多个Layer整体应用一些变换效果。
    容器类Layer可以对其子Layer整体做一些变换效果,比如裁剪效果(ClipRectLayer、ClipRRectLayer、ClipPathLayer)、过滤效果(ColorFilterLayer、ImageFilterLayer)、矩阵变换(TransformLayer)、透明变换(OpacityLayer)等。

虽然ContainerLayer并非抽象类,开发者可以直接创建ContainerLayer类的实例,但实际上很少会这么做,相反,在需要使用ContainerLayer时直接使用其子类即可。如果确实不需要任何变换效果,那么就使用OffsetLayer,不用担心会有额外的性能开销,它的底层(Skia中)实现是非常高效的。

绘制类Layer

PictureLayer是Flutter中国呢最常用的一种绘制类Layer。

最终显示在屏幕上的是位图信息,而位图信息正是由Canvas API绘制的。实际上Canvas的绘制产物是Picture对象表示,而当前版本的Flutter中只有PictureLayer才拥有picture对象,换句话说,Flutter中通过Canvas绘制自身及其子节点的组件的绘制结果最终都会落在PictureLayer中

Flutter 中还要两个Layer类:TextureLayer和PlatformViewLayer。

变换效果实现方式的选择

CantainerLayer可以对其子Layer整体进行一些变换,实际上,在大多数UI系统的Canvas API中都有一些变换相关的API,也就意味着一些变换效果即可以通过ContainerLayer来实现,也可以通过Canvas来实现。比如要实现平移变换,即可以使用OffsetLayer,也可以直接使用Canvas.translate API。来看看Layer实现变换效果的原理。

容器类Layer的变换在底层是通过Skia来实现的,不需要Canvas来处理。具体的原理是,有变换功能的容器类Layer会对应一个Skia引擎中的Layer,为了和Flutter framework中layer区分,Flutter中将Skia的Layer称之为engine layer。而有变换功能的容器类Layer在添加到Scene之前就会构建一个engine layer。比如OffsetLayer的相关实现:

@override
void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
  // 构建 engine layer
  engineLayer = builder.pushOffset(
    layerOffset.dx + offset.dx,
    layerOffset.dy + offset.dy,
    oldLayer: _engineLayer as ui.OffsetEngineLayer?,
  );
  addChildrenToScene(builder);
  builder.pop();
}

OFfsetLayer对其子节点整体做偏移变换的功能是Skia中实现支持的。Skia可以支持多层渲染,但并不是层越多越好,engineLayer是会占用一定的资源,Flutter自带组件库中涉及到变换效果的都是优先使用Canvas来实现,如果Canvas实现其起来非常困难或者实现不了时才会用ContainerLayer来实现。

有哪些场景下变换效果通过Canvas实现起来会非常困难?
比如需要对组件树中的某个子树整体做变换,而子树中有多个PictureLayer时。这是因为一个Canvas往往对应一个PictureLayer,不同的Canvas之间是相互隔离的,只有子树中所有组件都通过同一个Canvas绘制时才能通过Canvas对所有子节点进行整体变换,否则就只能通过ContainerLayer。

注意:Canvas对象中也有名为layer相关的API,如Canvas.saveLayer,它和本文介绍的Layer含义不同。Canvas对象中的layer主要是提供一种在绘制过程中缓存中间绘制结果的手段,为了在绘制复杂对象时方便多个绘制元素之间分离绘制而设计的,可以简单认为,不管Canvas创建多少个layer,这些layer都是在同一个PictureLayer上。