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
网友评论