ParentDataWidget 对于布局很重要,但平时用的少,一时间不知道它是做什么的。本文用一个自定义实例让你能够亲自体验 ParentDataWidget 作用和用法。
一句话来说 ParentDataWidget 就是给 RenderObject 的 parentData 提供数据的。但是光凭这一句还是很难理解 ParentDataWidget 是如何工作的。Flutter 有一个案例 Stack,但是代码很多。为了能聚焦,我实现了一个 MyStack,包含自定义的 MyPositioned 组件,就是一个简化版的 Stack,让大家了解 ParentDataWidget 的作用。
我们从需求开始。MyStack 要实现的功能是这样的。
- MyStack 在允许范围内尽量大。
- MyStack 的 children 都是 MyPositioned Widget。
- MyPositioned 可以通过 left 参数 MyStack 指定距 MyStack 左边缘的距离。
为了能使用 ParentDataWidget ,第一步是要先定义一类 extends ParentData 的类。这是 ParentDataWidget 要求的,不能直接用 ParentData 否则会报错。
我们直接从 ContainerBoxParentData 继承,ContainerBoxParentData 是继承自 ParentData 的。用ContainerBoxParentData的好处后,后面可以使用 ContainerRenderObjectMixin 和 RenderBoxContainerDefaultsMixin。里面具体怎么实现不用关注,我们只是为了用最少的代码让示例能跑起来。
我们自定义的 MyStackParentData 很简单,只有一个参数,规定 MyPositioned 距离 MyStack 左边缘的距离。
class MyStackParentData extends ContainerBoxParentData<RenderBox> { MyStackParentData({this.left}); double? left; } 复制代码
然后我们定义 MyStack 和 MyPositioned。代码已经是最简了,刚好实现我们的需求。
class MyRenderStack extends RenderBox with ContainerRenderObjectMixin<RenderBox, MyStackParentData>, RenderBoxContainerDefaultsMixin<RenderBox, MyStackParentData> { @override void setupParentData(RenderBox child) { if (child.parentData is! MyStackParentData) { child.parentData = MyStackParentData(); } } @override bool get sizedByParent => true; @override Size computeDryLayout(BoxConstraints constraints) { return constraints.biggest; } @override void paint(PaintingContext context, Offset offset) { paintStack(context, offset); } @protected void paintStack(PaintingContext context, Offset offset) { defaultPaint(context, offset); } @override void performLayout() { RenderBox? child = firstChild; while (child != null) { final MyStackParentData childParentData = child.parentData! as MyStackParentData; double x = 0, y = 0; if (childParentData.left != null) { x = childParentData.left!; } childParentData.offset = Offset(x, y); //因为只是演示,简化代码,直接固定宽高 100 child.layout(constraints.tighten(width: 100, height: 100), parentUsesSize: false); child = childParentData.nextSibling; } } } class MyPositioned extends ParentDataWidget<MyStackParentData> { const MyPositioned({required this.left, Key? key, required Widget child}) : super(key: key, child: child); final double? left; @override void applyParentData(RenderObject renderObject) { assert(renderObject.parentData is MyStackParentData); final MyStackParentData parentData = renderObject.parentData! as MyStackParentData; bool needsLayout = false; if (parentData.left != left) { parentData.left = left; needsLayout = true; } if (needsLayout) { final AbstractNode? targetParent = renderObject.parent; if (targetParent is RenderObject) { targetParent.markNeedsLayout(); } } } @override Type get debugTypicalAncestorWidgetClass => MyStack; } 复制代码
用的时候和其它 Widget 一样使用,可以尝试修改 left 值,看下效果。
MaterialApp( home: Scaffold( body: MyStack( children: [ MyPositioned( left: 10, child: Container( width: 100, height: 100, color: Colors.green, child: Text('I AM 17'), )) ], ))); 复制代码
MyPositioned 的 applyParentData 在 MyPositioned 生成的 element 执行 attachRenderObject 的时候就会执行。然后 MyStack 在 performLayout 的时候,可以拿到这些经过 MyPositioned 修改的 parentData,MyPositioned 根据这些 parentData 来做出对 child 的偏移。
setupParentData 这一步是必须的,而且是由父级,本例是 MyStack 完成,否则在 MyPositioned 执行 applyParentData 方法的时候拿不到正确类型的 ParentData。
最后附上 完整示例地址 ,copy 到 main.dart 就能执行。