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
和CompositedTransformTarget
widgets 创建和管理这些 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 将展开以满足传入的约束。