0%

flutter 自定义flex组件

自定义flex组件,获取子组件布局信息

我们都在用ColumnRow,他们都是Flex子类,将children中组件纵向或者横向进行摆放。

现在有一个需求是摆放完成后,需要知道children中各个widget所占的宽或者高,从而知道他们具体的位置锚点,然后可以根据这些锚点进行对children添加精准动画

目标效果

我们先来了解一下需要用到的基础知识点Flex

Flex

根据主轴方向显示一个一维widget数组。

其中ColumnRow就是Flex子类,他们直接把主轴方向写死简单的进行一次封装。

Flex集成自MultiChildRenderObjectWidget,实现两个主要方法createRenderObjectcreateRenderObject,这两个方法分别被Element在挂载(mount)和更新(update)的时候调用。

所以Flex只是MultiChildRenderObjectWidget的一个子类,具体布局还是父类去实现的,Flex通过重写createRenderObject指定返回元素RenderFlex,具体元素的加载是在RenderFlex中执行的,我们想第一时间拿到布局信息,也是在RenderFlexperformLayout中获取

RenderObjectElement 源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
abstract class RenderObjectElement extends Element {
/// Creates an element that uses the given widget as its configuration.
RenderObjectElement(RenderObjectWidget super.widget);
···

RenderObject? _renderObject;

@override
void mount(Element? parent, Object? newSlot) {
super.mount(parent, newSlot);
···
_renderObject = (widget as RenderObjectWidget).createRenderObject(this);
···
super.performRebuild(); // clears the "dirty" flag
}

@override
void update(covariant RenderObjectWidget newWidget) {
super.update(newWidget);
···
_performRebuild(); // calls widget.updateRenderObject()
}

@pragma('vm:prefer-inline')
void _performRebuild() {
···
(widget as RenderObjectWidget).updateRenderObject(this, renderObject);

···
super.performRebuild(); // clears the "dirty" flag
}
}

  • mount: RenderObject 对象的创建,以及与渲染树的插入工作,插入到渲染树后的 Element 就可以显示到屏幕中了。
  • update: 如果 Widget 的配置数据发生了改变,那么持有该 Widget 的 Element 节点也会被标记为 dirty。在下一个周期的绘制时,Flutter 就会触发 Element 树的更新,并使用最新的 Widget 数据更新自身以及关联的 RenderObject 对象,接下来便会进入 Layout 和 Paint 的流程。而真正的绘制和布局过程,则完全交由 RenderObject 完成。

自定义Flex组件

这里我们先自定义flex组件,想要拿到并更新我们的位置信息,我们就得重新两个重要方法:createRenderObjectcreateRenderObject

定义一个LayoutCallback回调类型,在拿到我们想要的布局后通过这个回调去处理,并在需要更新的时候去调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

typedef LayoutCallback = void Function(
List<double> xOffsets, TextDirection textDirection, double width);

class TabLabelBar extends Flex {
TabLabelBar({
Key? key,
List<Widget> children = const <Widget>[],
required this.onPerformLayout,
}) : super(
key: key,
children: children,
direction: Axis.horizontal, // 根据自己需求
);

final LayoutCallback onPerformLayout;

@override
RenderFlex createRenderObject(BuildContext context) {
return TabLabelBarRenderer(
direction: direction,
mainAxisAlignment: mainAxisAlignment,
mainAxisSize: mainAxisSize,
crossAxisAlignment: crossAxisAlignment,
textDirection: getEffectiveTextDirection(context)!,
verticalDirection: verticalDirection,
onPerformLayout: onPerformLayout,
);
}

@override
void updateRenderObject(
BuildContext context, _TabLabelBarRenderer renderObject) {
super.updateRenderObject(context, renderObject);
renderObject.onPerformLayout = onPerformLayout;
}
}
  • createRenderObject Element在挂载(mount)到树上通过调用该方法,去创建widget对应的renderObject。
  • updateRenderObject 在Widget配置数据更新后,修改对应的 Render Object。该方法在首次 build 以及需要更新 Widget 时都会调用

去实现createRenderObject方法

Flex的createRenderObject方法返回的RenderFlex。接下来要做的就是去实现自己的RenderFlex,肯定是去继承RenderFlex,重写相关方法,在重写中拿到我们想要的数据并回调。

  1. 将回调方法传入我们定义RenderFlex子类
  2. 在子类layout完毕的时候拿到布局信息,整合并回调
    1. RenderFlex继承RenderBox,RenderBox混入ContainerRenderObjectMixin 、 RenderBoxContainerDefaultsMixin 和 ContainerBoxParentData,通过 ContainerBoxParentData ,我们可以将 RenderBox 需要的 BoxParentData 和上面的 ContainerParentDataMixin 组合起来,事实上我们得到的 children 双链表就是以 ParentData 的形式呈现出来的。
    2. 要拿到所有同级widget布局,就要去先去拿到父级的renderObject
    3. 要拿到父级renderObject,我们可以通过其中一个子级的parentData
    4. ContainerRenderObjectMixin中的firstChild、lastChild、childCount。RenderFlex中可以直接能达到第一个元素firstChild。
    5. firstChild拿到parentData,然后就准确拿到了同级的RenderBox
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

class TabLabelBarRenderer extends RenderFlex {
TabLabelBarRenderer({
List<RenderBox>? children,
required Axis direction,
required MainAxisSize mainAxisSize,
required MainAxisAlignment mainAxisAlignment,
required CrossAxisAlignment crossAxisAlignment,
required TextDirection textDirection,
required VerticalDirection verticalDirection,
required this.onPerformLayout,
}) : super(
children: children,
direction: direction,
mainAxisSize: mainAxisSize,
mainAxisAlignment: mainAxisAlignment,
crossAxisAlignment: crossAxisAlignment,
textDirection: textDirection,
verticalDirection: verticalDirection,
);

LayoutCallback onPerformLayout;

@override
void performLayout() {
super.performLayout();
// xOffsets will contain childCount+1 values, giving the offsets of the
// leading edge of the first tab as the first value, of the leading edge of
// the each subsequent tab as each subsequent value, and of the trailing
// edge of the last tab as the last value.
RenderBox? child = firstChild;
final List<double> xOffsets = <double>[];
while (child != null) {
final FlexParentData childParentData =
child.parentData! as FlexParentData;
xOffsets.add(childParentData.offset.dx);
assert(child.parentData == childParentData);
//nextSibling: The next sibling in the parent's child list.
child = childParentData.nextSibling;
}
assert(textDirection != null);
switch (textDirection!) {
case TextDirection.rtl:
xOffsets.insert(0, size.width);
break;
case TextDirection.ltr:
xOffsets.add(size.width);
break;
}
onPerformLayout(xOffsets, textDirection!, size.width);
}
}

使用

到此,我们已经封装完毕。

1
2
3
4
TabLabelBar(
onPerformLayout: _saveOffsets,
children: [...widgets],
),