[Flutter小技巧] Row中widget高度自适应的几种方法

我们Flutter开发经常会碰到的一个问题:如何自适应Row中widget的高度?例如我们需要找出这些children里面最高的那个,其他child的高度都设置成这个最高child高度相同或者某个百分比。 本文将会列出几种常见的方法,并分析他们的优缺点。

比较常用的方法有:

方法1:将Row中的Item设置成固定高度

比如下方代码:

dart 复制代码
Row(
children: [
    SizedBox(
        height: 100,
            child: Center(
            child: Text("data1"),
        ),
    ),
    SizedBox(
        height: 100,
            child: Center(
            child: Text("data2"),
        ),
        )
    ],
);

这种方法一般用于我们知道Row中item的高度是多少,例如上面的例子,我们将item的高度都设置成100。

但大部分情况下,我们是不知道Row中item的高度的,希望它能够自适应。于是就有了

方法2:通过IntrinsicHeight包裹Row的方式,计算出Row的最小高度。

比如下方代码:

dart 复制代码
IntrinsicHeight(
    child:Row(
    children: [
        SizedBox(
                child: Center(
                child: Text("data1"),
            ),
        ),
        SizedBox(
            height: 100,
                child: Center(
                child: Text("data2"),
            ),
            )
        ],
    )
);

第一个SizedBox没有设置它的高度,但因为是通过IntrinsicHeight包裹的,第一个SizedBox的高度会强行变成100.

貌似"方法2"就能解决我们大部分问题了,而且通过搜索引擎我们得到的大部分解决方法也都是这个。但IntrinsicHeight并不适用于所有的场景,特别是Row中的children通过LayoutBuilder包裹的情况。

我们再看看下面的代码:

dart 复制代码
IntrinsicHeight(
    child:Row(
    children: [
        LayoutBuilder(builder:(context,cons){
            return  SizedBox(
                    child: Center(
                    child: Text("data1"),
                ),
            );
        }),
        SizedBox(
            height: 100,
                child: Center(
                child: Text("data2"),
            ),
            )
        ],
    )
);

通过测试,你会发现,上面的代码会抱错: "LayoutBuilder does not support returning intrinsic dimensions."

在研究 IntrinsicHeight源码后,会发现IntrinsicHeight之所以能够实现它child以最小高度展示,是因为IntrinsicHeight 通过重写 computeMinIntrinsicHeight和 computeMaxIntrinsicHeight方法获取child的最小高度。 而这两个方法是通过轮询自己的子child 的 computeMinIntrinsicHeight和computeMaxIntrinsicHeight进行计算的,而我们再看看LayoutBuilder的相关代码:

dart 复制代码
class _RenderLayoutBuilder extends RenderBox 
 with RenderObjectWithChildMixin<RenderBox>, 
 RenderConstrainedLayoutBuilder<BoxConstraints, RenderBox> {
    ....
    @override
    double computeMinIntrinsicWidth(double height) {
        assert(_debugThrowIfNotCheckingIntrinsics());
        return 0.0;
    }

    @override
    double computeMaxIntrinsicWidth(double height) {
        assert(_debugThrowIfNotCheckingIntrinsics());
        return 0.0;
    }

    @override
    double computeMinIntrinsicHeight(double width) {
        assert(_debugThrowIfNotCheckingIntrinsics());
        return 0.0;
    }

    @override
    double computeMaxIntrinsicHeight(double width) {
        assert(_debugThrowIfNotCheckingIntrinsics());
        return 0.0;
    }
    ....
}

bool _debugThrowIfNotCheckingIntrinsics() {
    assert(() {
            if (!RenderObject.debugCheckingIntrinsics) {
            throw FlutterError(
            'LayoutBuilder does not support returning intrinsic dimensions.\n'
            'Calculating the intrinsic dimensions would require running the layout '
            'callback speculatively, which might mutate the live render object tree.',
            );
        }
        return true;
    }());
    return true;
}

从上方代码可以看出,LayoutBuilder对compute这一系列的方法都做了限制,这些方法都不允许在LayoutBuilder存在的Widget树中使用。也就是说通过IntrinsicHeight包裹的widget都不允许存在LayoutBuilder,不管它是在wiget树中的第几层。

在日常开中LayoutBuilder是经常用到的组件,它让我们可以通过上级传递的constraints进行动态布局。

那么如果我们在Row中用到了LayoutBuilder就没有办法实现"Row中的Item高度一致"吗? 现在可以想到的方式就是通过重写Row组件,在计算出Row中所有Item的高度,取其中最高的,再进行一次布局。

方法3:重写Row组件,实现统一高度

dart 复制代码
typedef _NextChild = RenderBox? Function(RenderBox child);

 
class MRow extends Flex {

    const MRow({
        super.key,
        super.mainAxisAlignment,
        super.mainAxisSize,
        super.textDirection,
        super.verticalDirection,
        super.textBaseline,
        super.clipBehavior,
        super.children,
    }) : super(
        direction: Axis.horizontal,
        crossAxisAlignment: CrossAxisAlignment.start);

 
    @override
    RenderFlex createRenderObject(BuildContext context) {
        return MRowFlex(
            direction: direction,
            mainAxisAlignment: mainAxisAlignment,
            mainAxisSize: mainAxisSize,
            crossAxisAlignment: crossAxisAlignment,
            textDirection: getEffectiveTextDirection(context),
            verticalDirection: verticalDirection,
            textBaseline: textBaseline,
            clipBehavior: clipBehavior,
            );
   }
}

  


class MRowFlex extends RenderFlex {
    MRowFlex({
        List<RenderBox>? children,
        super.direction,
        super.mainAxisSize,
        super.mainAxisAlignment,
        super.crossAxisAlignment,
        super.textDirection,
        super.verticalDirection,
        super.textBaseline,
        super.clipBehavior,
    });

 
bool get _flipMainAxis =>
        firstChild != null &&
        switch (direction) {
            Axis.horizontal => switch (textDirection) {
                null || TextDirection.ltr => false,
                TextDirection.rtl => true,
            },
            Axis.vertical => switch (verticalDirection) {
                VerticalDirection.down => false,
                VerticalDirection.up => true,
        },
    };

  
    @override
    void performLayout() {
        //先预算一遍所有children的边界值
        super.performLayout();
        //便利所有children,找到高度最高的值
        final (_NextChild nextChild, RenderBox? topLeftChild) =
            _flipMainAxis ? (childBefore, lastChild) : (childAfter, firstChild);

        double maxHeightSize = 0;
        for (RenderBox? child = firstChild;
            child != null;
            child = nextChild(child)) {
            maxHeightSize = max(maxHeightSize, child.size.height);
        }
        //将所有children的高度设置成最高值
        for (RenderBox? child = firstChild;
            child != null;
            child = nextChild(child)) {
            var constraints = child.constraints
                .copyWith(maxHeight: maxHeightSize, minHeight: maxHeightSize);
            child.layout(constraints, parentUsesSize: false);
        }
    }
}

上面代码实际上是对Row进行了重写,通过重写performLayout将所有孩子节点的高度重置成最高的高度。但这种写法有个弊端,就是每个孩子节点都会被layout两次。如果孩子节点里有LayoutBuilder则会发现Row每次build都会触发两次LayoutBuilder的回调函数。而且这两次触发传入的constraints值是不同的,如果用到此属性则可能会对实际业务造成影响,而且在性能上也不是最优。 实际上可以将Row重写的更彻底一点,例如单独设计一个控件放在Row里面,让Row里的其它控件高度都以这个控件高度为准,这样我们只需要找到Row里面的这个特殊控件,也避免了laylout两次的问题。限于篇幅和难度这里就不展开了。也可以看看"方法5"实现原理和这个类似。

方法4:用Table代替Row

less 复制代码
TableRow(children: [
    TableCell(
        //高度灵活适配
        verticalAlignment: TableCellVerticalAlignment.intrinsicHeight,
        child: LayoutBuilder(builder: (context, cons) {
            print("LayoutBuilder layout ${item["title"]} $cons");
            return Container(
                width: 10,
                color: Colors.red,
            );
    })),
    TableCell(
        child: Container(
        margin: const EdgeInsets.all(8),
        padding: const EdgeInsets.all(8),
        decoration: const BoxDecoration(
        color: Colors.grey,
        borderRadius: BorderRadius.all(Radius.circular(8))),
        child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
            Text(item["title"] ?? ""),
            const SizedBox(
                height: 8,
            ),
            Row(
                children: [Expanded(child: Text(item["info"] ?? ""))],
            ),
        ],
    ))),
])

Table虽然在灵活性上没有Row强,性能也没有Row高,代码量也会比Row多不少,但是用来解决高度适配问题却很不错,关键点就在于TableCell里有个参数 verticalAlignment: TableCellVerticalAlignment.intrinsicHeight,通过它我们可以将这个TableCell和同级别的TableCell适配成统一高度。

方法5:第三方插件

这里推荐一个第三方控件boxy,pub.dev/packages/bo...

它不仅能够适配横向的还可以适配纵向的,还有很多其它很实用的功能,具体如何使用这里就不展示了。从原理上看是将Row重写了,可以看到很多Row的老代码。走的也是"方法三"的路子,但可以看到源码里用到了computeMaxIntrinsicHeight去计算控件的高度,大概率子控件里也无法使用LayoutBuilder了。

相关推荐
帅次5 小时前
Objective-C面向对象编程:类、对象、方法详解(保姆级教程)
flutter·macos·ios·objective-c·iphone·swift·safari
小蜜蜂嗡嗡6 小时前
flutter flutter_vlc_player播放视频设置循环播放失效、初始化后获取不到视频宽高
flutter
bawomingtian1238 小时前
FlutterView 源码解析
flutter
Zender Han12 小时前
Flutter 进阶:实现带圆角的 CircularProgressIndicator
flutter
nc_kai15 小时前
Flutter 之 每日翻译 PreferredSizeWidget
java·前端·flutter
littlegnal16 小时前
Flutter Add-to-app profiling
flutter
0wioiw01 天前
Flutter基础(FFI)
flutter
Georgewu10 天前
【HarmonyOS 5】鸿蒙跨平台开发方案详解(一)
flutter·harmonyos