flutter 关于有行数限制的多行标签实现(CustomMultiChildLayout自适应高度)

1,471 阅读4分钟

关于多行标签排列题主了解到有三种实现方式:

  • Wrap: 简单高效,没办法控制显示行数,如果末尾拼接额外视图则会紧跟排列;
  • RichText: 较高的自由度,支持混合任意组件组合(类似html的内联块),支持行数限制,如果末尾拼接额外视图通过行数限制后无法显示;
  • CustomMultiChildLayout:高度自定义,指哪打哪(!!!^ _ ^),缺陷是目前系统提供的协议没办法实现高度自适应,但是可以通过继承修改该类实现。

最后实现的效果:

63855de90ca39eb541e63115691bf61.png

下面依次分析一下各种组件的实现方式。

1、Wrap

          Wrap(
              spacing: 6,
              runSpacing: 6,
              children: [
                ...getTagWidgets(12, true).map((e) => e).toList(),
                const MoreArrow(),
              ],
            )

代码比较简单,也没有什么好阐述的,下面是图例:

4ea4e0f51dc78dc105cda98ef9da817.png

2、RichText

  RichText(
              text: TextSpan(children: [
                ...getTagWidgets(33, true)
                    .map((e) => WidgetSpan(
                            child: Padding(
                          padding: const EdgeInsets.only(left: 6, top: 6),
                          child: e,
                        )))
                    .toList(),
                const WidgetSpan(child: MoreArrow()),
              ]),
              maxLines: 2,
            ),

下面是图例:

450a24fa1cee24aec4e4ff483de9f8d.png

RichText通常使用是对富文本的拼接,但是WidgetSpan的出现让该组件更加丰富,而不仅仅只是文字的组合排列。WidgetSpan可以使任意组件加入到富文本的组合排列之中,包括不限于聊天内容中的表情+文字混排、URL/Email/Phone高亮等等。

但是在这里却没办法实现实现拼接额外视图的操作,如果仅需要限制显示行数RichText应该是最优解。

3、CustomMultiChildLayout

有关CustomMultiChildLayout的使用请自行查找资料。

正常的CustomMultiChildLayout使用,并不能满足题主需求。究其原因是系统的使用没办法让多行标签自适应内容高度,所以在这里主要说明如何实现CustomMultiChildLayout的内容高度自适应。

CustomMultiChildLayout的实现是通过自定义布局协议MultiChildLayoutDelegate来实现的。 而我们每个子集的具体布局在协议方法void performLayout() {}中,我们所有的计算操作都是在这里。

  /// 重写这个方法来布局和定位所有的子元素组件的大小。
  ///
  ///该方法必须为每个子节点调用[layoutChild]。它还应该指定使用[positionChild]获取每个子节点的最终位置。
  void performLayout(Size size);

通过计算我们可以在该方法中获取CustomMultiChildLayout整个布局的内容高度,即视图需要显示的高度。

!!!思考:高度我们可以通过计算拿到,但是我们如果将高度告诉我们的渲染器呢?

查询API我们可以看到协议中有方法:

  /// 该方法可以控制我们 CustomMultiChildLayout 的约束,也可以直接给定一个size
  Size getSize(BoxConstraints constraints) => constraints.biggest;

但是该方法的执行顺序在方法 void performLayout(Size size);之前,所以没办法拿到计算结果之后在渲染视图大小,此路不通。

继续查看源码:

78acc48292984d9928e817fa8fef06d.png 我们可以看到CustomMultiChildLayout是通过createRenderObject(BuildContext context)实现的渲染,并返回了一个RenderCustomMultiChildLayoutBox对象,

84e22afd655f05748be3e637db9c2e9.png 而查看RenderCustomMultiChildLayoutBox实现,我们可以看到在void performLayout()中有设置大小的方法和调用delegate视图布局的方法。

94f96d9be9e4ebf1464993945fbfbee.png

之前我们通过自定义delegate的布局可以计算出具体内容的高度,在这里我们又找到可以设置视图大小的地方,所以想实现高度自定义只需要把二者关联起来即可。

1、首先完成 CustomMultiChildLayout 的重写

// 重写 CustomMultiChildLayout 只是为了调用 RenderCustomSizedMultiChildLayoutBox 
// 高度有赋值的地方
class CustomSizedMultiChildLayout extends CustomMultiChildLayout {
  CustomSizedMultiChildLayout({
    super.key,
    required SizedMultiChildLayoutDelegate delegate,
    List<Widget> children = const <Widget>[],
  }) : super(children: children, delegate: delegate);

  @override
  RenderCustomSizedMultiChildLayoutBox createRenderObject(
          BuildContext context) =>
      RenderCustomSizedMultiChildLayoutBox(
          delegate: delegate as SizedMultiChildLayoutDelegate);
}

// 重写 RenderCustomSizedMultiChildLayoutBox,在之前重设视图大小的地方改成我们
// 计算出来的大小
class RenderCustomSizedMultiChildLayoutBox
    extends RenderCustomMultiChildLayoutBox {
  RenderCustomSizedMultiChildLayoutBox(
      {List<RenderBox>? children,
      required SizedMultiChildLayoutDelegate delegate})
      : super(children: children, delegate: delegate) {
    addAll(children);
  }

  @override
  SizedMultiChildLayoutDelegate get delegate =>
      super.delegate as SizedMultiChildLayoutDelegate;

  @override
  void performLayout() {
    /// 通过协议拿到我们的内容宽高
    size = delegate._callPerformLayout(constraints.biggest, firstChild);
  }
}

代码中SizedMultiChildLayoutDelegate为我们即将复写的布局协议。

2、其次完成 MultiChildLayoutDelegate 的重写

这个方法我们主要重写两个地方:

  • void _callPerformLayout(Size size, RenderBox? firstChild)
  • void performLayout(Size size)

通过分析只需要将两个方法的返回值改为Size即可(返回我们实际内容大小)。

对于其他方法,由于父类MultiChildLayoutDelegate_idToChild_debugChildrenNeedingLayout属于内部属性,所以涉及布局的方法都需要override一下,否则无法正确实现布局。

自定义类名并继承 MultiChildLayoutDelegate

abstract class SizedMultiChildLayoutDelegate extends MultiChildLayoutDelegate {
    ...
    
    @override
    Size performLayout(Size size);
    
    
  Size _callPerformLayout(Size size, RenderBox? firstChild) {
   
    final Map<Object, RenderBox>? previousIdToChild = _idToChild;

    Size result = Size.zero;           <---------- Add

    Set<RenderBox>? debugPreviousChildrenNeedingLayout;
    assert(() {
      debugPreviousChildrenNeedingLayout = _debugChildrenNeedingLayout;
      _debugChildrenNeedingLayout = <RenderBox>{};
      return true;
    }());

    try {
      _idToChild = <Object, RenderBox>{};
      RenderBox? child = firstChild;
      while (child != null) {
        final MultiChildLayoutParentData childParentData =
            child.parentData! as MultiChildLayoutParentData;
        assert(() {
          if (childParentData.id == null) {
            throw FlutterError.fromParts(<DiagnosticsNode>[
              ErrorSummary(
                  'Every child of a RenderCustomMultiChildLayoutBox must have an ID in its parent data.'),
              child!.describeForError('The following child has no ID'),
            ]);
          }
          return true;
        }());
        _idToChild![childParentData.id!] = child;
        assert(() {
          _debugChildrenNeedingLayout!.add(child!);
          return true;
        }());
        child = childParentData.nextSibling;
      }
      result = performLayout(size);           <---------- Changed
      assert(() {
        if (_debugChildrenNeedingLayout!.isNotEmpty) {
          throw FlutterError.fromParts(<DiagnosticsNode>[
            ErrorSummary('Each child must be laid out exactly once.'),
            DiagnosticsBlock(
              name: 'The $this custom multichild layout delegate forgot '
                  'to lay out the following '
                  '${_debugChildrenNeedingLayout!.length > 1 ? 'children' : 'child'}',
              properties: _debugChildrenNeedingLayout!
                  .map<DiagnosticsNode>(_debugDescribeChild)
                  .toList(),
            ),
          ]);
        }
        return true;
      }());
    } finally {
      _idToChild = previousIdToChild;
      assert(() {
        _debugChildrenNeedingLayout = debugPreviousChildrenNeedingLayout;
        return true;
      }());
    }
    return result;           <---------- Add
  }
    
    ...
}

到这里关于CustomMultiChildLayout自适应高度的改写就完成了!

回归正题,解决了组件高度的问题,对于有有行数限制的多行标签的实现就仅仅是对CustomSizedMultiChildLayout的使用而已,具体代码这就不贴了,看下Git就可以了。

Git地址: github.com/boomcx/max_…

文献参考

CustomSizedMultiChildLayout.. lets a child of CustomMultiChildLayout determine its size. (github.com)