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

2022-05-02  本文已影响0人  maskerII

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 不裁剪

适配原理

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

上一篇 下一篇

猜你喜欢

热点阅读