flutter 自定义flex组件

216 阅读4分钟

自定义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 源码

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回调类型,在拿到我们想要的布局后通过这个回调去处理,并在需要更新的时候去调用。


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
    6. ...

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);
  }
}

使用

到此,已经封装完毕。

 TabLabelBar(
    onPerformLayout: _saveOffsets,
    children: [...widgets],
  ),