Flutter-Compositing
先回顾一下Flutter的渲染管线:
void drawFrame(){
pipelineOwner.flushLayout();
pipelineOwner.flushCompositingBits();
pipelineOwner.flushPaint();
renderView.compositeFrame()
...//省略
}
CustomRotatedBox
实现一个CustomRotatedBox,功能是将子元素放倒(顺时针旋转90度),要实现这个效果可以直接使用canvas变换功能,下面是核心代码:
class CustomRotatedBox extends SingleChildRenderObjectWidget {
CustomRotatedBox({Key? key, Widget? child}) : super(key: key, child: child);
@override
RenderObject createRenderObject(BuildContext context) {
return CustomRenderRotatedBox();
}
}
class CustomRenderRotatedBox extends RenderBox
with RenderObjectWithChildMixin<RenderBox> {
@override
void performLayout() {
_paintTransform = null;
if (child != null) {
child!.layout(constraints, parentUsesSize: true);
size = child!.size;
//根据子组件大小计算出旋转矩阵
_paintTransform = Matrix4.identity()
..translate(size.width / 2.0, size.height / 2.0)
..rotateZ(math.pi / 2) // 旋转90度
..translate(-child!.size.width / 2.0, -child!.size.height / 2.0);
} else {
size = constraints.smallest;
}
}
@override
void paint(PaintingContext context, Offset offset) {
if(child!=null){
// 根据偏移,需要调整一下旋转矩阵
final Matrix4 transform =
Matrix4.translationValues(offset.dx, offset.dy, 0.0)
..multiply(_paintTransform!)
..translate(-offset.dx, -offset.dy);
_paint(context, offset, transform);
} else {
//...
}
}
void _paint(PaintingContext context,Offset offset,Matrix4 transform ){
// 为了不干扰其他和自己在同一个layer上绘制的节点,所以需要先调用save然后在子元素绘制完后
// 再调用restore显示,关于save/restore有兴趣可以查看Canvas API doc
context.canvas
..save()
..transform(transform.storage);
context.paintChild(child!, offset);
context.canvas.restore();
}
... //省略无关代码
}
测试demo
class CustomRotatedBoxTest extends StatelessWidget {
const CustomRotatedBoxTest({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: CustomRotatedBox(
child: Text(
"A",
textScaleFactor: 5,
),
),
);
}
}
文字倒下,达到预期效果。 给CustomRotatedBox添加一个RepaintBoundary试试
@override
Widget build(BuildContext context) {
return Center(
child: CustomRotatedBox(
child: RepaintBoundary( // 添加一个 RepaintBoundary
child: Text(
"A",
textScaleFactor: 5,
),
),
),
);
}
添加RepaintBoundary后,CustomRotatedBox中的持有的还是OffsetLayer1:
void _paint(PaintingContext context,Offset offset,Matrix4 transform ){
context.canvas // 该 canvas 对应的是 PictureLayer1
..save()
..transform(transform.storage);
// 子节点是绘制边界节点,会在新的 OffsetLayer2中的 PictureLayer2 上绘制
context.paintChild(child!, offset);
context.canvas.restore();
}
... //省略无关代码
}
很显然,CustomRotatedBox中进行旋转变换的canvas对应的是PictureLayer1,而Text("A")的绘制是使用PictureLayer2对应的Canvas,它们不属于同一个Layer。所以CustomRotatedBox也就不会对Text("A")起作用。
之前有介绍很多容器类组件都附带变换效果,拥有旋转变化的容器Layer是TransformLayer,那么可以在CustomRotatedBox中绘制子节点之前:
- 创建一个TransformLayer(记为TransformLayer1)添加到Layer树中,接着创建一个新的PaintingContext和TransformLayer1绑定。
- 子节点通过这个新的PaintingContext去绘制 完成上面步骤后,后代节点绘制所在的PictureLayer都会是TransformLayer的子节点,因此可以通过TransformLayer对所有子节点整体做变换,下面是添加TransformLayer1前、后的Layer树结构。
这其实就是一个重新Layer合成(Layer compositing)的过程:创建一个新的ContainerLayer,然后将该ContainerLayer传递给子节点,这样后代节点的Layer必然属于ContainerLayer,那么给这个ContainerLayer做变换就会对其前部的子孙节点生效。
Layer合成在不同的语境中会有不同的指代,比如Skia最终渲染时也是将一个个layer渲染出来,这个过程也可以认为是多个layer上的绘制信息合成最终的位图信息;另外canvas中也有layer的概念(canvas.save方法生成新的layer),对应的将所有layer绘制结果最后叠加在一起的过程也可以称为layer合成。
由于layer的组合是一个标准的过程(唯一的不同是使用那种ContainerLayer来作为父容器),PaintingContext中提供了一个pushLayer方法来执行组合过程:
void pushLayer(ContainerLayer childLayer, PaintingContextCallback painter, Offset offset, { Rect? childPaintBounds }) {
if (childLayer.hasChildren) {
childLayer.removeAllChildren();
}
//下面两行是向Layer树中添加新Layer的标准操作,在之前小节中详细介绍过,忘记的话可以去查阅。
stopRecordingIfNeeded();
appendLayer(childLayer);
//通过新layer创建一个新的childContext对象
final PaintingContext childContext =
createChildContext(childLayer, childPaintBounds ?? estimatedBounds);
//painter是绘制子节点的回调,我们需要将新的childContext对象传给它
painter(childContext, offset);
//子节点绘制完成后获取绘制产物,将其保存到PictureLayer.picture中
childContext.stopRecordingIfNeeded();
}
那么,只需创建一个TransformLayer然后指定需要的旋转变换,然后直接调用pushLayer:
// 创建一个持有 TransformLayer 的 handle.
final LayerHandle<TransformLayer> _transformLayer = LayerHandle<TransformLayer>();
void _paintWithNewLayer(PaintingContext context, Offset offset, Matrix4 transform) {
//创建一个 TransformLayer,保存在handle中
_transformLayer.layer = _transformLayer.layer ?? TransformLayer();
_transformLayer.layer!.transform = transform;
context.pushLayer(
_transformLayer.layer!,
_paintChild, // 子节点绘制回调;添加完layer后,子节点会在新的layer上绘制
offset,
childPaintBounds: MatrixUtils.inverseTransformRect(
transform,
offset & size,
),
);
}
// 子节点绘制回调
void _paintChild(PaintingContext context, Offset offset) {
context.paintChild(child!, offset);
}
然后需要在paint方法中判断一下子节点是否是绘制边界节点,如果是则需要走layer组合,如果不是则需要走layer合成。
@override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
final Matrix4 transform =
Matrix4.translationValues(offset.dx, offset.dy, 0.0)
..multiply(_paintTransform!)
..translate(-offset.dx, -offset.dy);
if (child!.isRepaintBoundary) { // 添加判断
_paintWithNewLayer(context, offset, transform);
} else {
_paint(context, offset, transform);
}
} else {
_transformLayer.layer = null;
}
}
为了让代码看起来更清晰,将child不为空时的逻辑绘制逻辑封装一个pushTransform函数里:
TransformLayer? pushTransform(
PaintingContext context,
bool needsCompositing,
Offset offset,
Matrix4 transform,
PaintingContextCallback painter, {
TransformLayer? oldLayer,
}) {
final Matrix4 effectiveTransform =
Matrix4.translationValues(offset.dx, offset.dy, 0.0)
..multiply(transform)
..translate(-offset.dx, -offset.dy);
if (needsCompositing) {
final TransformLayer layer = oldLayer ?? TransformLayer();
layer.transform = effectiveTransform;
context.pushLayer(
layer,
painter,
offset,
childPaintBounds: MatrixUtils.inverseTransformRect(
effectiveTransform,
context.estimatedBounds,
),
);
return layer;
} else {
context.canvas
..save()
..transform(effectiveTransform.storage);
painter(context, offset);
context.canvas.restore();
return null;
}
}
然后修改paint实现,直接调用pushTransform方法即可:
@override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
pushTransform(
context,
child!.isRepaintBoundary,
offset,
_paintTransform!,
_paintChild,
oldLayer: _transformLayer.layer,
);
} else {
_transformLayer.layer = null;
}
}
现在重新运行实例,达到预期效果。 需要说明的是,其实PaintingContext已经帮我们封装好了pushTransform方法,可以直接使用:
@override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
context.pushTransform(
child!.isRepaintBoundary,
offset,
_paintTransform!,
_paintChild,
oldLayer: _transformLayer.layer,
);
} else {
_transformLayer.layer = null;
}
}
实际上,PaintingContext针对常见的拥有变换功能的容器类Layer的组合都封装好了对应的方法,同时Flutter中已经预定了拥有相应变换功能的组件: | Layer的名称 | PaintingContext对应的方法 | Widget | | ClipPathLayer | pushClipPath | ClipPath | | OpacityLayer | pushOpacity | Opacity | | ClipRRectLayer | pushClipRRect | ClipRRect | | ClipRectLayer | pushClipRect | ClipRect | | TransformLayer | pushTransform | RotatedBox、Transform |
什么时候需要合成Layer
- 合成Layer的原则 通过上面的例子知道CustonRotatedBox的直接子节点是绘制边界节点时CustomRotatedBox中就需要合成Layer。实际上这是一种特例,还有一些其他情况也是需要CustomRotatedBox进行Layer合成,什么时候需要Layer合成?有没有一个普适性的原则?思考CustomRotatedBox中需要Layer合成的根本原因是什么?如果CustomRotatedBox的所有厚道节点都共享的是同一个PictureLayer,但是一旦后代节点创建了新的PictureLayer,则绘制会脱离之前的PictureLayer,因为不同的PictureLayer上的绘制是相互隔离的,所以为了使变换对所有后台节点对应的PictureLayer都生效,则需要将所有后代节点添加到同一个ContainerLayer中,也就是需要在CuntomRotatedBox中先进行Layer合成。 综上总结:当后代节点会向layer树中添加新的绘制layer时,则父级的变换类组件中就需要合成layer。
下面来验证:修改上面的实例,给RepaintBoundary添加一个Center父组件:
@override
Widget build(BuildContext context) {
return Center(
child: CustomRotatedBox(
child: Center( // 新添加
child: RepaintBoundary(
child: Text(
"A",
textScaleFactor: 5,
),
),
),
),
);
}
因为CustomRotatedBox中只判断了其子节点的child!isRepaintBoundary为true时,才会进行layer合成,而现在它的直接子节点是Center,所以该判断会是false,则不会进行layer合成。但是根据上面的出的结论,RepaintBoundary作为CustomRotatedBox的后代节点且会向layer树中添加新layer时就需要进行layer合成,而本例中应该是合成layer但实际上却没有合成,所以预期是不能将A放倒的。运行后和预期相同,A没有倒下。解决这个问题,在判断是否需要进行Layer合成时,要去遍历整个子树,看看是否存在绘制边界节点,如果是则合成,反之则否。为此,新定义一个在子树上查找是否存在绘制边界节点的needCompositing()方法:
//子树中递归查找是否存在绘制边界
needCompositing() {
bool result = false;
_visit(RenderObject child) {
if (child.isRepaintBoundary) {
result = true;
return ;
} else {
//递归查找
child.visitChildren(_visit);
}
}
//遍历子节点
visitChildren(_visit);
return result;
}
修改paint实现:
@override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
context.pushTransform(
needCompositing(), //子树是否存在绘制边界节点
offset,
_paintTransform!,
_paintChild,
oldLayer: _transformLayer.layer,
);
} else {
_transformLayer.layer = null;
}
}
运行后A预期的放倒。
alwaysNeedsCompositing
考虑一种情况:如果CustomRotatedBox 的后代节点中没有绘制边界节点,但是有后代节点向layer树中添加了新的layer。这种情况下,按照之前得出的结论CustomRotatedBox中也是需要进行Layer合成的,但是CustomRotated实际上并没有。 原因是:在CustomRotatedBox中遍历后代节点时,无法知道非绘制边界节点是否往layer树中添加了新的layer。 Flutter是通过约定来解决这个问题:
- RenderObject中定义了一个布尔类型alwaysNeedsCompositing属性。
- 约定:自定义组件中,如果组件isRepaintBoundary为false时,在绘制时要向layer树中添加新的layer的话,要将alwaysNeedsCompositing置为true。
开发者在自定义组件时,应遵守这个规范。根据此规范,CustomRotatedBox中在子树中递归查找时的判断条件修改为:
child.isRepaintBoundary || child.alwaysNeedsCompositing
最终needCompositing实现如下:
//子树中递归查找是否存在绘制边界
needCompositing() {
bool result = false;
_visit(RenderObject child) {
// 修改判断条件改为
if (child.isRepaintBoundary || child.alwaysNeedsCompositing) {
result = true;
return ;
} else {
child.visitChildren(_visit);
}
}
visitChildren(_visit);
return result;
}
注意:这要求非绘制节点组件在向layer树中添加layer时,必须让自身alwaysNeedsCompositing值为true。
Opacity解析
Opacity可以对子树进行透明度控制,这个效果通过Canvaas是很难实现,所以flutter中直接使用了OffsetLayer合成的方式:
class RenderOpacity extends RenderProxyBox {
// 本组件是非绘制边界节点,但会在部分透明的情况下向layer树中添加新的Layer,所以部分透明时要返回 true
@override
bool get alwaysNeedsCompositing => child != null && (_alpha != 0 && _alpha != 255);
@override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
if (_alpha == 0) {
// 完全透明,则没必要再绘制子节点了
layer = null;
return;
}
if (_alpha == 255) {
// 完全不透明,则不需要变换处理,直接绘制子节点即可
layer = null;
context.paintChild(child!, offset);
return;
}
// 部分透明,需要通过OffsetLayer来处理,会向layer树中添加新 layer
layer = context.pushOpacity(offset, _alpha, super.paint, oldLayer: layer as OpacityLayer?);
}
}
}
优化
上面通过CustomRotatedBox延时了变换类组件的核心原理,不过还有一些优化的地方,比如:
- 变换类组件中,遍历子树以确定是否需要layer合成是变换类组件的通用逻辑,不需要在每个组件里都实现一遍。
- 不是每一次重绘都需要遍历子树,比如可以在初始化时遍历一次,然后将结果缓存,如果后续有变化,再重新遍历即可,此时直接使用缓存的结果。
flushCompositingBits
每一个节点(RenderObject中)都有一个_needsCompositing字段,该字段用于缓存当前节点在绘制子节点时是否需要合成layer。flushCompositingBits的功能就是在节点树初始化和子树中合成信息发生变化时来重新遍历节点树,更新每一个节点的_needsCompositing值。可以发现:
- 递归遍历子树的逻辑抽到了flushCompositingBits中,不需要组件单独实现。
- 不需要每一次重绘都遍历子树了,只需要在初始化和发生变化时重新遍历。
void flushCompositingBits() {
// 对需要更新合成信息的节点按照节点在节点树中的深度排序
_nodesNeedingCompositingBitsUpdate.sort((a,b) => a.depth - b.depth);
for (final RenderObject node in _nodesNeedingCompositingBitsUpdate) {
if (node._needsCompositingBitsUpdate && node.owner == this)
node._updateCompositingBits(); //更新合成信息
}
_nodesNeedingCompositingBitsUpdate.clear();
}
RenderObject的_updateCompositingBits方法的功能就是递归遍历子树确定如果每一个节点的_needsCompositing值:
void _updateCompositingBits() {
if (!_needsCompositingBitsUpdate)
return;
final bool oldNeedsCompositing = _needsCompositing;
_needsCompositing = false;
// 递归遍历查找子树, 如果有孩子节点 needsCompositing 为true,则更新 _needsCompositing 值
visitChildren((RenderObject child) {
child._updateCompositingBits(); //递归执行
if (child.needsCompositing)
_needsCompositing = true;
});
// 这行我们上面讲过
if (isRepaintBoundary || alwaysNeedsCompositing)
_needsCompositing = true;
if (oldNeedsCompositing != _needsCompositing)
markNeedsPaint();
_needsCompositingBitsUpdate = false;
}
执行完毕后,每一个节点的_needsCompositing就确定了,在绘制时只需要判断一下当前的needsCompositing(一个 getter,会直接返回_needsCompositing),就能知道子树是否存在剥离layer了。这样的话,可以再优化一下CustomRenderRotatedBox的实现,最终实现如下:
class CustomRenderRotatedBox extends RenderBox
with RenderObjectWithChildMixin<RenderBox> {
Matrix4? _paintTransform;
@override
void performLayout() {
_paintTransform = null;
if (child != null) {
child!.layout(constraints, parentUsesSize: true);
size = child!.size;
//根据子组件大小计算出旋转矩阵
_paintTransform = Matrix4.identity()
..translate(size.width / 2.0, size.height / 2.0)
..rotateZ(math.pi / 2)
..translate(-child!.size.width / 2.0, -child!.size.height / 2.0);
} else {
size = constraints.smallest;
}
}
final LayerHandle<TransformLayer> _transformLayer =
LayerHandle<TransformLayer>();
void _paintChild(PaintingContext context, Offset offset) {
print("paint child");
context.paintChild(child!, offset);
}
@override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
_transformLayer.layer = context.pushTransform(
needsCompositing, // pipelineOwner.flushCompositingBits(); 执行后这个值就能确定
offset,
_paintTransform!,
_paintChild,
oldLayer: _transformLayer.layer,
);
} else {
_transformLayer.layer = null;
}
}
@override
void dispose() {
_transformLayer.layer = null;
super.dispose();
}
@override
void applyPaintTransform(RenderBox child, Matrix4 transform) {
if (_paintTransform != null) transform.multiply(_paintTransform!);
super.applyPaintTransform(child, transform);
}
}
再此思考flushCompositingBits 思考引入flushCompositingBits的根本原因: 假如在变化类容器中始终采用合成layer的方式对子树应用变换效果,也就是不再使用canvas进行变换,这样的话flushCompositingBits就没必要存在了。 需要flushCompositingBits的根本原因是:如果在变换类组件中一刀切的使用合成layer方式的话,每遇到一个变换类组件则至少会再创建一个layer,这样的话,最终layer树上的layer数量会变多。之前说过,对子树应用变换效果既能通过Canvas实现,也能通过容器类Layer实现时,建议使用Canvas。这是因为每新建一个layer都会有额外的开销,所以只应该在无法通过Canvas来实现子树变化效果时,再通过layer合成的方式来实现。
综上:引入flushCompositingBits的根本原因是为了减少layer的数量。另外,flushCompositingBits的执行过程中只是做标记,并没有进行层合成,真正的合成是在绘制时(组件的paint方法中)。
总结
- 只有组件树中有变换类容器时,才有可能需要重新合成layer;如果没有变换类组件,则不需要。
- 当变换类容器的后代节点会向layer树中添加新的绘制layer时,则变换类组件中就需要合成layer。
- 引入flushCompositingBits的根本原因是为了减少layer的数量。
转载自:https://juejin.cn/post/7231116444548579388