关于多行标签排列题主了解到有三种实现方式:
Wrap: 简单高效,没办法控制显示行数,如果末尾拼接额外视图则会紧跟排列;RichText: 较高的自由度,支持混合任意组件组合(类似html的内联块),支持行数限制,如果末尾拼接额外视图通过行数限制后无法显示;CustomMultiChildLayout:高度自定义,指哪打哪(!!!^ _ ^),缺陷是目前系统提供的协议没办法实现高度自适应,但是可以通过继承修改该类实现。
最后实现的效果:
下面依次分析一下各种组件的实现方式。
1、Wrap
Wrap(
spacing: 6,
runSpacing: 6,
children: [
...getTagWidgets(12, true).map((e) => e).toList(),
const MoreArrow(),
],
)
代码比较简单,也没有什么好阐述的,下面是图例:
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,
),
下面是图例:
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);之前,所以没办法拿到计算结果之后在渲染视图大小,此路不通。
继续查看源码:
我们可以看到
CustomMultiChildLayout是通过createRenderObject(BuildContext context)实现的渲染,并返回了一个RenderCustomMultiChildLayoutBox对象,
而查看
RenderCustomMultiChildLayoutBox实现,我们可以看到在void performLayout()中有设置大小的方法和调用delegate视图布局的方法。
之前我们通过自定义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_…