美文网首页
Flutter 之 空间适配 FittedBox (四十六)

Flutter 之 空间适配 FittedBox (四十六)

作者: maskerII | 来源:发表于2022-05-02 22:00 被阅读0次

    1.前言

    子组件大小超出了父组件大小时,如果不经过处理的话 Flutter 中就会显示一个溢出警告并在控制台打印错误日志,比如下面代码会导致溢出:

    Padding(
      padding: const EdgeInsets.all(30.0),
      child: Row(
        children: [
          Text("xx" * 30),
        ],
      ),
    ),
    
    image.png

    控制台日志

    A RenderFlex overflowed by 126 pixels on the right.
    

    可以看到 右边溢出126像素

    理论上我们经常会遇到子元素的大小超过他父容器的大小的情况,比如一张很大图片要在一个较小的空间显示,根据Flutter 的布局协议,父组件会将自身的最大显示空间作为约束传递给子组件,子组件应该遵守父组件的约束,如果子组件原始大小超过了父组件的约束区域,则需要进行一些缩小、裁剪或其它处理,而不同的组件的处理方式是特定的,比如 Text 组件,如果它的父组件宽度固定,高度不限的话,则默认情况下 Text 会在文本到达父组件宽度的时候换行

    那如果我们想让 Text 文本在超过父组件的宽度时不要换行而是字体缩小呢?还有一种情况,比如父组件的宽高固定,而 Text 文本较少,这时候我们想让文本放大以填充整个父组件空间该怎么做呢?

    实际上,上面这两个问题的本质就是:子组件如何适配父组件空间。而根据 Flutter 布局协议适配算法应该在容器或布局组件的 layout 中实现,为了方便开发者自定义适配规则,Flutter 提供了一个 FittedBox 组件

    2. FittedBox

    FittedBox 定义

      const FittedBox({
        Key? key,
        this.fit = BoxFit.contain,
        this.alignment = Alignment.center,
        this.clipBehavior = Clip.none,
        Widget? child,
      })
    

    FittedBox 属性

    属性 介绍
    fit 适配方式 默认 BoxFit.contain
    alignment 对齐方式 默认 Alignment.center
    clipBehavior 裁剪方式 默认 Clip.none 不裁剪

    适配原理

    • FittedBox 在布局子组件时会忽略其父组件传递的约束,可以允许子组件无限大,即FittedBox 传递给子组件的约束为(0<=width<=double.infinity, 0<= height <=double.infinity)。
    • FittedBox 对子组件布局结束后就可以获得子组件真实的大小。
    • FittedBox 知道子组件的真实大小也知道他父组件的约束,那么FittedBox 就可以通过指定的适配方式(BoxFit 枚举中指定),让起子组件在 FittedBox 父组件的约束范围内按照指定的方式显示。

    3. 示例

    示例1

    
    class MSFittedBoxDemo2 extends StatelessWidget {
      const MSFittedBoxDemo2({Key? key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: Text("FittedBoxDemo2")),
          body: FittedBox(
            child: Padding(
              padding: const EdgeInsets.all(30.0),
              child: Row(
                children: [
                  Text("XX" * 30),
                ],
              ),
            ),
          ),
        );
      }
    }
    
    
    image.png

    示例2

    
    class MSFittedBoxDemo3 extends StatelessWidget {
      const MSFittedBoxDemo3({Key? key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: Text("FittedDemo3")),
          body: Center(
            child: Column(
              children: [
                _mContainer(BoxFit.none),
                Text("Hello World 1", textScaleFactor: 1.5),
                _mContainer(BoxFit.contain),
                Text("Hello World 2", textScaleFactor: 1.5),
              ],
            ),
          ),
        );
      }
    
      _mContainer(BoxFit fit) {
        return Container(
          width: 60,
          height: 60,
          color: Colors.red,
          child: FittedBox(
            fit: fit,
            child: Container(
              width: 70,
              height: 80,
              color: Colors.blue,
            ),
          ),
        );
      }
    }
    
    
    image.png

    因为父Container要比子Container 小,所以当没有指定任何适配方式时,子组件会按照其真实大小进行绘制,所以第一个蓝色区域会超出父组件的空间,因而看不到红色区域。第二个我们指定了适配方式为 BoxFit.contain,含义是按照子组件的比例缩放,尽可能多的占据父组件空间,因为子组件的长宽并不相同,所以按照比例缩放适配父组件后,父组件能显示一部分。

    注意
    在未指定适配方式时,虽然 FittedBox 子组件的大小超过了 FittedBox 父 Container 的空间,但FittedBox 自身还是要遵守其父组件传递的约束,所以最终 FittedBox 的本身的大小是 50×50,这也是为什么蓝色会和下面文本重叠的原因,因为在布局空间内,父Container只占50×50的大小,接下来文本会紧挨着Container进行布局,而此时Container 中有子组件的大小超过了自己,所以最终的效果就是绘制范围超出了Container,但布局位置是正常的,所以就重叠了。如果我们不想让蓝色超出父组件布局范围,那么可以可以使用 ClipRect 对超出的部分剪裁掉即可:

    使用ClipRect裁剪

     ClipRect( // 将超出子组件布局范围的绘制内容剪裁掉
      child: Container(
        width: 60,
        height: 60,
        color: Colors.red,
        child: FittedBox(
          fit: boxFit,
          child: Container(width: 70, height: 80, color: Colors.blue),
        ),
      ),
    );
    

    或者 指定FittedBox的clipBehavior的属性为Clip.hardEdge

    Container(
      width: 60,
      height: 60,
      color: Colors.red,
      child: FittedBox(
        fit: fit,
        clipBehavior: Clip.hardEdge,
        child: Container(width: 70, height: 80, color: Colors.blue),
      ),
    );
    

    示例3 单行缩放布局

    
    class MSFittedBoxDemo4 extends StatelessWidget {
      const MSFittedBoxDemo4({Key? key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: Text("FittedDemo4")),
          body: Center(
            child: Column(
              children: [
                _mRow(Text("  90000000000000000  ")),
                FittedBox(
                  child: _mRow(Text("  90000000000000000  ")),
                ),
                _mRow(Text("  800  ")),
                FittedBox(
                  child: _mRow(Text("  800  ")),
                ),
              ].map((e) {
                return Padding(
                  padding: EdgeInsets.all(30),
                  child: e,
                );
              }).toList(),
            ),
          ),
        );
      }
    
      _mRow(Widget child) {
        return Row(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [child, child, child],
        );
      }
    }
    
    
    image.png

    可以看到,当数字为' 90000000000000000 '时,三个数字的长度加起来已经超出了测试设备的屏幕宽度,所以直接使用 Row 会溢出,给 Row 添加上如果加上 FittedBox时,就可以按比例缩放至一行显示,实现了我们预期的效果

    但是当数字没有那么大时,比如下面的 ' 800 ',直接使用 Row 是可以的,但加上 FittedBox 后三个数字虽然也能正常显示,但是它们却挤在了一起,这不符合我们的期望。之所以会这样,原因其实很简单:在指定主轴对齐方式为 spaceEvenly 的情况下,Row 在进行布局时会拿到父组件的约束,如果约束的 maxWidth 不是无限大,则 Row 会根据子组件的数量和它们的大小在主轴方向来根据 spaceEvenly 填充算法来分割水平方向的长度,最终Row 的宽度为 maxWidth;但如果 maxWidth 为无限大时,就无法在进行分割了,所以此时 Row 就会将子组件的宽度之和作为自己的宽度。

    回示例中,当 Row 没有被 FittedBox 包裹时,此时父组件传给 Row 的约束的 maxWidth 为屏幕宽度,此时,Row 的宽度也就是屏幕宽度,而当被FittedBox 包裹时,FittedBox 传给 Row 的约束的 maxWidth 为无限大(double.infinity),因此Row 的最终宽度就是子组件的宽度之和

    示例4 使用自定义MSLayoutLogPrint 打印下约束信息

    
    class MSLayoutLogPrint<T> extends StatelessWidget {
      const MSLayoutLogPrint({Key? key, required this.child, required this.tag})
          : super(key: key);
    
      final Widget child;
      final T tag;
    
      @override
      Widget build(BuildContext context) {
        return LayoutBuilder(builder: (context, constraints) {
          print("$tag -- $constraints");
          return child;
        });
      }
    }
    
    
    
    class MSFittedBoxDemo5 extends StatelessWidget {
      const MSFittedBoxDemo5({Key? key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: Text("FittedDemo5")),
          body: Center(
            child: Column(
              children: [
                MSLayoutLogPrint(
                  tag: 1,
                  child: _wRow(Text("  800  ")),
                ),
                FittedBox(
                  child: MSLayoutLogPrint(
                    tag: 2,
                    child: _wRow(Text("  800  ")),
                  ),
                ),
              ].map((e) {
                return Padding(
                  padding: EdgeInsets.all(30),
                  child: e,
                );
              }).toList(),
            ),
          ),
        );
      }
    
      _wRow(Widget child) {
        return Row(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [child, child, child],
        );
      }
    }
    
    
    image.png

    日志如下:

    flutter: 1 -- BoxConstraints(0.0<=w<=315.0, 0.0<=h<=Infinity)
    flutter: 2 -- BoxConstraints(unconstrained)
    

    也就证明了,当 Row 没有被 FittedBox 包裹时,此时父组件传给 Row 的约束的 maxWidth 为屏幕宽度,此时,Row 的宽度也就是屏幕宽度,而当被FittedBox 包裹时,FittedBox 传给 Row 的约束的 maxWidth 为无限大(double.infinity),因此Row 的最终宽度就是子组件的宽度之和。

    我们只需让FittedBox 子元素接收到的约束的 maxWidth 为屏幕宽度即可达到预期。为此我们封装了一个 SingleLineFittedBox 来替换 FittedBox 以达到我们预期的效果

    示例5 MSSingleLineFittedBox

    
    class MSSingleLineFittedBox extends StatelessWidget {
      const MSSingleLineFittedBox(this.child, {Key? key}) : super(key: key);
      final Widget child;
    
      @override
      Widget build(BuildContext context) {
        return LayoutBuilder(builder: (ctx, constrains) {
          print(constrains);
          return FittedBox(
            child: ConstrainedBox(
              constraints: constrains.copyWith(
                minWidth: constrains.maxWidth, // 最小宽度为父部件的最大宽度 当子部件的真实宽度小于父部件时,不会挤在一起
                maxWidth: double.infinity, // 最大宽度无限大 当子部件的真实宽度大于父部件时,可以按照比例缩放
              ),
              child: child,
            ),
          );
        });
      }
    }
    
    
    
    
    class MSFittedBoxDemo6 extends StatelessWidget {
      const MSFittedBoxDemo6({Key? key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: Text("FittedDemo6")),
          body: Center(
            child: Column(
              children: [
                MSSingleLineFittedBox(_wRow(Text("  9000000000000  "))),
                MSSingleLineFittedBox(_wRow(Text("  800  "))),
              ].map((e) {
                return Padding(
                  padding: EdgeInsets.all(30),
                  child: e,
                );
              }).toList(),
            ),
          ),
        );
      }
    
      _wRow(Widget child) {
        return Row(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [child, child, child],
        );
      }
    }
    
    
    image.png

    参考:https://book.flutterchina.club/chapter5/fittedbox.html#_5-6-1-fittedbox

    相关文章

      网友评论

          本文标题:Flutter 之 空间适配 FittedBox (四十六)

          本文链接:https://www.haomeiwen.com/subject/osbpyrtx.html