先回顾一下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,
),
),
),
);
}
发现文字并没有倒下。 结合图剖析,依据上节常识Layer实例,画出增加RepaintBoundary前和后的Layer的树结构。
增加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的数量。