Compositing
如何呈现已合成的视觉效果?
Scene是合成 UI 的非透明、不可变的表示形式。每一帧,渲染管线都会生成一个 scene 来提交给 engine 做光栅化(通过Window.render方法)。Scene 也可以被直接光栅化为一个Image(通过Scene.toImage方法)。Scene 里封装了一系列渲染操作(例如,clips,transforms,effects)和图形(例如,pictures,textures,platform views)。SceneBuilder是 framework 中创建一个 scene 的最底层机制。SceneBuilder管理着一个存储着渲染操作的栈(通过SceneBuilder.pushClipRect,SceneBuilder.pushOpacity,SceneBuilder.pop进行操作)及其相关的图形(通过SceneBuilder.addPicture,SceneBuilder.addTexture,SceneBuilder.addRetained等等)。当渲染操作应用后,SceneBuilder生成、追踪、返回一个EngineLayer实例。它可能会被 framework 缓存起来以优化后续帧的绘制(比如,更新一个旧的 layer 而不是重新创建它,或者重用一个之前已经光栅化好的 layer)。完成后,通过SceneBuilder.build方法得到最终的Scene。- 绘制操作累积在一个
Picture(通过PictureRecorder)中,该 picture 通过SceneBuilder.addPicture添加到 scene 中。 - 外部纹理(由使用平台纹理存储库建立的纹理 ID 表示)通过
SceneBuilder.addTexture添加到 scene 中。它支持“冻结”参数,以便在暂停动画时可以暂停纹理。 - 先前经过光栅化处理的
EngineLayers可以通过SceneBuilder.addRetained缓存起来。
- 绘制操作累积在一个
Picture是一系列绘制操作的不透明、不可变的表示。 这些操作可以直接添加到“场景”中(通过SceneBuilder.addPicture),也可以组合到另一张图片中(通过Canvas.drawPicture)。Pictures 也可以光栅化(通过Picture.toImage),并提供其内存占用量的估计值。PictureRecorder是一种不透明的机制,通过该机制可以对绘制操作进行排序并将其转换为不可变的Picture。Picture recorders 通常通过Canvas进行管理。录制完成后(通过PictureRecorder.endRecording),将返回最终Picture,随后PictureRecorder(和关联的Canvas)就会失效。
How are layers represented?
Layer代表已合成的 scene 中的一个片断。Layers 按层次结构排列,每个节点都有可能影响其下面的节点(通过应用渲染操作或绘制图形)。Layers 是可变的,可以在树上自由移动,但只能使用一次。使用SceneBuilder将每一个 layer 合成为一个 scene(通过Layer.addToScene);添加 rootLayer 相当于合成整个树(例如,Layer 添加其子节点)。注意,整个 layer 树有变化时必须重新合成,这里没有脏状态的概念。- 当添加一个 layer 时,
SceneBuilder提供一个EngineLayer实例。它可以被存储在Layer.engineLayer中,以后可以用来优化合成。例如,Layer.addToScene可能会传递 engine layer (通过Layer._addToSceneWithRetainedRendering)而不是重新渲染来重用上一帧的光栅化纹理。另外,在执行某些渲染操作时可以指定 engine layer(即"oldLayer"),以允许将这些操作作为内联变换来实现。 - Layers 支持渲染保留(通过
Layer.markNeedsAddToScene)。这个标志位代表着前一个 engine layer 能不能在合成过程中重用(即,对比上一帧 layer 没有任何变化)。如果是这样,保留的渲染允许整个子树替换为上一个光栅化的位图(通过SceneBuilder.addRetained)。否则,必须重新对 layer 进行合成。一旦有 layer 被加入到树中,这个标志位就会重置(除非这个 layer 指定它自身必须一直参与合成)。- 首先,必须将所有 layer 添加到 scene。添加后,缓存的 engine layer 可能会用于渲染保留。其他 layer 则完全禁用渲染保留(通过
Layer.alwaysNeedsAddToScene)。通常,当某个 layer 发生变化(即通过修改属性或添加子节点)时,必须将其重新添加到 scene 中。 - 如果一个 layer 必须被添加到 scene,则必须添加所有祖先 layer。保留的渲染允许将子树替换为缓存的位图。如果子孙节点已更改,则该位图将变为无效,因此还必须将祖先 layer 添加到 scene 中(通过
Layer.updateSubtreeNeedsAddToScene)。- 请注意,绘制时不会强制使用此属性;仅当 scene 已合成(通过
ContainerLayer.buildScene), layer 树才会保持连通。
- 请注意,绘制时不会强制使用此属性;仅当 scene 已合成(通过
- 一个 layer 的父节点接收一个新的 engine layer 时,父节点必须被重新添加到树中。
- 通常来说,只有 container layer 使用渲染保留。
- 首先,必须将所有 layer 添加到 scene。添加后,缓存的 engine layer 可能会用于渲染保留。其他 layer 则完全禁用渲染保留(通过
- 元数据可以被嵌入 layer 中以便后面检索(通过
Layer.find和Layer.findAll)。
- 当添加一个 layer 时,
ContainerLayer管理着一个子节点列表,并把它们按顺序插入已合成的 scene。Root layer (TransformLayer)是ContainerLayer的子类。因此,ContainerLayer负责组合整个 scene(通过ContainerLayer.buildScene)。这涉及使保留的渲染状态保持一致(通过ContainerLayer.updateSubtreeNeedsAddToScene),将所有子节点添加到 scene(通过ContainerLayer.addChildrenToScene),以及将 container 标记为不再需要添加到 scene(通过Layer._needsAddToScene)。ContainerLayer实现了许多对子节点的操作(例如,ContainerLayer.append,ContainerLayer.removeAllChildren),并且大多数这些操作会对子节点进行遍历(例如,ContainerLayer.find;注意,这是倒叙遍历,即 lastLayer 优先)。ContainerLayer将为其子代使用保留的渲染,只要它们没有偏移(即位于 layer 的原点)即可。这是 layer 树中大多数保留的渲染的入口。ContainerLayer.applyTransform转换给定的矩阵以反映在合成过程如何变换给定的子节点。此方法假定所有子节点都位于原点;因此,必须删除所有 layer 偏移并将其转换为一个 transformation(通过SceneBuilder.pushTransform或SceneBuilder.pushOffset)。否则,所得矩阵将不准确。
OffsetLayer是ContainerLayer的子类,它支持高效的重绘(即 repaint boundaries)。被定义作 repaint boundaries 的 Render objects 对应于 Layer 树中的 OffsetLayers。如果 render object 不需要重绘或者只是做了简单的 transform,则可以重用现有的 offset layer。这避免整颗子树被重绘。OffsetLayer将任何偏移量作为顶级的 transform,这样的后代节点可以在原点进行合成,且可以进行渲染保留。OffsetLayer.toImage通过Scene.toImage将光栅化以此 layer 为根的子树。得到的 image,由原始像素数据组成,且以指定像素比率(忽略设备的屏幕像素密度)渲染为矩形。
TransformLayer是一个ContainerLayer的子类,可应用任意 transform;它也恰好(通常来说)是 RenderView 对应的 root layer。任何 layer 的偏移(通过Layer.addToScene)或显示偏移(通过OffsetLayer.offset)会被删除并转换成一个 transformation 来确保与TransformLayer.applyTransform的表现一致。PhysicalModelLayer是将物理光影效果集成到已合成 scene 中的核心。这个类将 elevation,clip region,background color 和 shadow color 组合起来,在它的子节点背面投射一个 shadow(通过SceneBuilder.pushPhysicalShape)。AnnotatedRegionLayer将元数据合并到一颗给定固定边界的 layer 树中。一个确定了矩形左上角的偏移值(默认到原点)和一个 size 表示它的尺寸。Hit testing 由AnnotatedRegionLayer.find和AnnotatedREgionLayer.findAll来执行。在树中较深或在子列表中后入的节点优先级更高。FollowerLayer和LeaderLayer支持高效的转换连接(transform linking)(例如,一个 layer 看上去可以与另一 layer 一起滚动)。这些 layers 通过LayerLink通讯,将 leader 的 transform (包括 layerOffset)传递给 follwer。在 follower 被渲染的地方,会使用这个 transform 来呈现。CompositedTransformFollower和CompositedTransformTargetwidgets 创建和管理这些 layers。- 有各种各样的叶子类、内部类组成了 layer 树。
- 叶节点通常直接继承
Layer。PictureLayer描述了一张会被加入到 scene 的Picture。类似的,TextureLayer描述了一个纹理(通过 ID)和一个边界矩形。PlatformViewLayer几乎相同,它只是应用视图而不是纹理。 - 非叶子节点通常继承
ContainerLayer或它的子类。OffsetLayer是实现高效重绘的关键。ClipPathLayer,ClipRectLayer,ColorFilterLayer,OpacityLayer等通过推入 scene builder 状态,添加所有子节点(通过ContainerLayer.addChildrenToScene,这可能会利用保留的渲染)来应用相应的效果,然后弹出该状态。
- 叶节点通常直接继承
EngineLayer及其各种特殊实现类 (例如,OpacityEngineLayer,OffsetEngineLayer) 代表着后端 layer 的不透明句柄。SceneBuilder为其支持的每个操作生成适当的句柄。然后,这些可用于启用渲染保留和内联更新。
Compositing 有哪些组成部分?
-
Canvas提供了在单个PictureRecorder上下文中执行绘图操作的接口。也就是说,所有由Canvas执行的绘图都被限制为一个单独的Picture(和PictureLayer)。PaintingContext支持跨越多个 layer 的渲染和绘图操作,PaintingContext根据渲染树的合成需求管理Canvas生命周期。 -
PaintingContext.repaintCompositedChild是一个将 render object 绘制到自己的 layer 中的静态方法。 一旦为了绘制而把节点标记为脏,则会在渲染管道的绘制阶段对其进行处理(通过PipelineOwner.flushPaint)。这将对所有脏 render object 执行深度优先遍历。请注意,实际上只有 repaint boundaries 被标记为脏;所有其他节点会便利其祖先节点找到最近的 repaint boundary,然后将其标记为脏。检索或创建一个OffsetLayer来作为子树的容器 layer。创建一个新的PaintingContext来进行实际的绘制(通过PaintingContext._paintWithContext)。 -
Layers 可以从渲染管道上挂载或分离。如果在带有已分离 layer 中的 render object 被发现是脏的(通过
PipelineOwner.flushPaint),则遍历祖先节点来找到最近的在一个已挂载(或未创建的)layer 中的 repaint boundary。这个节点会被标记为脏,以便于在重挂载的时候按预期进行绘制(viaRenderObject._skippedPaintingOnLayer)。 -
PaintingContext在 paint 方法和底层 canvas 之间提供了一个中间层,让 render object 适应不断变化的合成需求。也就是说,render object 在某些情况下可能会引入新的 layer,而在其他情况下可能会重用现有的 layer。当这种变化发生时,任何祖先节点都需要适配子孙节点引入新 layer 的可能情况(通过RenderObject.needsCompositing); 特别是,这需要引入他们自己的 layer来实现某些特殊操作(例如,clip)。PaintingContext管理该过程,并提供一些封装好的方法(例如,PaintingContext.pushClipPath)。PaintingContext还可以确保引入 repaint boundary 的子节点被组合到新的 layer 中(通过PaintingContext.paintChild)。最后,PaintingContext管理着提供给Canvas的PictureRecorder, 每当 recording 完成时添加 picture layers(即PictureLayer)。-
使用
ContainerLayer子类初始化PaintingContext,以作为通过该 context 生存的 layer 树的根节点。绘制永远不会在此 layer 中进行,而是由新的PictureLayer实例捕获,该实例随绘制进行而添加。 -
PaintingContext可能会因为合成而管理多个 canvas。每个Canvas在其生命周期中都与一个PictureRecorder和一个PictureLayer(即当前 layer)相关联。除非必须添加新 layer(例如,为了实现合成效果或遇到 repaint boundary),所有的 render object 会使用相同的canvas(和PictureRecorder)。- Recording 在第一次访问
Canvas(通过PaintingContext.canvas)时开始。 这将生成一个新的 picture layer, picture recorder, 和 canvas 实例;一旦创建,picture layer 就会被添加到 container layer。 - 当直接或间接引入新的 layer 时,Recording 结束;此时,picture 会被存储在当前的 layer 中(通过
PictureRecorder.endRecording),并且清除 canvas 和当前 layer。下次给子节点绘制时,将创建一个新的 canvas,picture recorder 和 picture layer。 - 使用新的由相应的
OffsetLayer初始化的PaintingContext来绘制已合成的子节点。如果一个已合成的子节点实际上并不需要重绘制(例如,它仅仅是应用了 transform), 那OffsetLayer可以以一个新的偏移量来重用。
- Recording 在第一次访问
-
PaintingContext提供了一个简单的接口。如果需要合成,则新的 layer 会被自动推入以实现所需的效果(通过PaintingContext.pushLayer)。否则,可以直接使用Canvas来实现效果(例如,通过PictureRecorder)。- 推入 layer 将结束当前的 recording,添加新的 layer 到 conatiner layer。为这个新的 layer(如果存在,则该 layer 的子节点可能会因为过期而被删除)创建一个
PaintingContext。如果创建了,则使用新的 conetxt 调用绘制方法;任何绘制都会包含在新的 layer 中。随后使用原始PaintingContext进行的操作,都将导致新的 canvas,layer 和 picture recorder 被创建。
- 推入 layer 将结束当前的 recording,添加新的 layer 到 conatiner layer。为这个新的 layer(如果存在,则该 layer 的子节点可能会因为过期而被删除)创建一个
-
-
ClipContext是PaintingContext的基类。它的主要用途是在不引入新 layer 的情况下为剪裁提供支持(例如,适用于不需要合成的 render object 的方法)。
"needs compositing" 标志位做了什么事情?
- 合成描述了在光栅化过程中引擎将 layer 组合在一起的过程。在 framework 的上下文中,合成通常指的是分配与绘制相关的 render object 到 layer。需要合成并不意味着 render object 将被分配到它自己的 layer;相反,它表明某些效果必须通过引入新的 layer 来实现,而不是修改当前的 layer(例如,应用不透明度或者裁剪)。例如,如果父节点先建立一个 clip 然后绘制其子节点,则必须决定如何实现这个 clip: (1)作为当前 layer 的一部分(即
Canvas.clipPath),或者 (2) 通过推入一个ContainerLayer(即ClipPathLayer))。"needs compositing" 位管理着这些信息。- 通常来说,这是因为这个节点可能最终绘制了一个推入了新 layer 的子节点(例如,因为它是一个 repaint boundary)。因此,任何可能产生非局部后果的绘制都必须以跨 layer 工作的方式来实现(即通过合成)。
- 请注意,这不同于将 render object 标记为 repaint boundary。这样做会在绘制子节点(通过
PaintingContext.paintChild)时引入一个新的 layer。这使 repaint boundary 的所有祖先节点的 "needs compositing" 位失效。这可能会导致某些祖先节点在绘制时引入新的 layer,但前提是他们使用了任何非局部操作。
"needs compositing" 标志位如何更新?
-
RenderObject.markNeedsCompositingBitsUpdate把 render object 标记为需要更新 "needs compositing" 位(通过PipelineOwner._nodesNeedingCompositingBitsUpdate)。如果节点被标记为脏节点,则其所有祖先也被标记为脏节点。作为一种优化,如果当前节点或当前节点的父级是 repaint boundary(或父节点已被标记为脏),则可以中断此遍历。- 如果节点的合成标志位需要更新,则可能会引入一个新 layer(即,需要合成)。如果是这样,所有祖先节点也需要合成,因为他们可能会绘制引入了新 layer 的后代。如上所述,某些非局部效果将需要通过合成来实现。
- 由于所有祖先节点都必须先标记为需要合成,所以遍历可能在 repaint boundaries 处停止。
- 添加或删除子节点(通过
RenderObject.adoptChild和RenderObject.dropChild)可能会更改 render 树的合成要求。同样地,更改RenderObject.alwaysNeedsCompositing位将更新 "needs composting“ 位。
-
在渲染过程中,
PipelineOwner.flushCompositingBits更新所有脏的合成位(通过RenderObject._updateCompositingBits)。给定节点是否需要合成,取决于(1)它是 repaint boundary(通过RenderObject.isRepaintBoundary)。(2)它被标记为始终需要合成(通过RenderObject.alwaysNeedsCompositing)。(3)它的任何后代节点需要合成。- 这个过程将遍历所有被标记为脏的后代节点,根据上述策略更新他们的 "needs compositing" 标志位。注意,这通常只会遍历将合成标识位设为脏时标记的路径。
- 如果更改了节点的合成标记位,它会被标记为脏以进行绘制(因为绘制现在可能需要额外的合成)。
-
如果 render object 始终需要引入一个 layer,它应该切换
alwaysNeedsCompositing为 true。如果这个改变发生了(除了更改子节点时,这会自动调用),markNeedsCompositingBitsUpdate将被调用。
什么是 repaint boundaries?
- 某些 render object 在 render 树里引入了 repaint boundaries (通过
RenderObject.isRepaintBoundary)。这些 render object 总是被绘制到一个新的 layer 中,从而使它们与其父节点分开绘制。这有效的解耦以 repaint boundaries 为根的子树与先前绘制的父节点。- 当部分 UI 大部分时间保持静态并且部分 UI 频繁更新时,这会很有用。Repaint boundaries 有助于避免重绘 UI 的静态部分。
- 作为 repaint boundary 的 render object 与
OffsetLayer相关联,OffsetLayer是一种特殊类型的 layer,可以更新其位置而无需重新渲染。
- 作为 repaint boundaries 的 Render objects 在绘制过程中的处理方式有所不同 (通过
PaintingContext.paintChild)。- 如果子节点不需要绘制,则跳过整个子树。对应的 layer 将会被更新以适配新的偏移量(通过
PaintContext.paintChild),并将其添加到父 layer。 - 如果子节点需要绘制,则当前的 offset layer 会被清理或创建(通过
PaintingContext._repaintCompositedChild),然后将其用于绘制(通过RenderObject._paintWithContext)。 - 否则,绘制将通过
RenderObject._paintWithContext进行。
- 如果子节点不需要绘制,则跳过整个子树。对应的 layer 将会被更新以适配新的偏移量(通过
- 由于 repaint boundaries 失踪推入新的 layer,因此必须将所有祖先节点标记为需要合成。任何需要非局部绘制效果的节点也需要引入新的 layer。
可以推入新 layer 的子节点是如何绘制的?
PaintingContext.paintChild通过以下方式管理此过程:(1)结束所有正在进行的 recording。(2)为子节点创建一个新的 layer。(3)使用新的 painting context 在新的 layer 中绘制该子节点。(4)将该 layer 添加到当前的 layer 树。(5)为将来会使用原始 painting context 的绘制创建新的 canvas 和 layer。PaintingContext中的方法会询问合成标志位,以决定是通过插入 layer 还是更改 canvas (例如,clipping)来实现行为。
Layer 如何使绘制更高效?
- 所有 render object 都可能与
ContainerLayer相关联(通过RenderObject.layer)。如果已定义了此 layer,则这是用于绘制 render object 的最后一个 layer(如果使用了多个 layer,则这是最底层的)。- Repaint boundaries 会被自动分配一个
OffsetLayer(通过PaintingContext._repaintCompositedChild)。绘制时,所有其他 layer 都必须专门设置此字段。 - 不直接推入 layer 的 render object 必须将其设置为 null。
- Repaint boundaries 会被自动分配一个
ContainerLayer跟踪任何关联的EngineLayer(通过ContainerLayer.engineLayer)。某些操作可以使用此信息来更新 layer 而不是重新创建。- 某些方法接收
oldLayer参数以启用此优化。 - 已缓存的 engine layers 可以直接被重用(通过
SceneBuilder.addRetained));这是渲染保留。
- 某些方法接收
Widget 树如何手动光栅化?
- 可以通过
OffsetLayer.toImage把一个 layer 光栅为一个图像。一种获取整个 UI 图像的便捷方法时在RenderView自己的 Layer 上使用此方法。要获得 UI 的局部图像,可以插入适当的 repaint boundary,并对 layer 进行同样的操作。
如何管理 Clips, transforms 和其他效果?
- 这里有两种路径。不需要合成的 layer 可通过
Canvas在自己的 rendering context 上处理 transforms 和 clips(因此,效果仅限于该单个 layer)。那些需要合成的 layer 会通过PaintingContext将这些效果作为特殊的ContainerLayers引入。- 包括各种有用的
ContaienrLayers:AnnotatedRegionLayer(用于在 layer 的各部分存储元数据),BackDropFilterLayer(用于背景模糊或滤镜),OpacityLayer(允许改变不透明度)和OffsetLayer(repaint boundary 优化机制的关键)。
- 包括各种有用的
Canvas.saveLayer/Canvas.restore允许对绘制命令进行分组,以便一次将诸如 混合(blending)、颜色过滤(color filtering) 和抗锯齿(anti-aliasing)之类的效果应用于该组,而不是进行增量的堆叠(这可能导致无效的渲染)。
外接纹理如何合成?
- 纹理完全由 engine 管理,并由简单的纹理 ID 引用。 描述纹理的 layer 与此 ID 关联,并在合成后与后端纹理集成。As such, these are painted out-of-band by the engine.
- 纹理 layers 通过
TextureBox与 render 树集成在一起。 这是一个跟踪纹理 ID 并绘制相应TextureLayer的 render box。 在布局期间,此 box 将展开以满足传入的约束。