开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 23 天,点击查看活动详情
Clip 裁剪类组件
Flutter中提供了一些裁剪组件,用于对组件进行裁剪,iOS中的裁剪功能比较简单,通常就是圆角的一些简单操作,而Flutter中的裁剪功能比较丰富。
| 裁剪Widget | 默认行为 |
|---|---|
| ClipOval | 子组件为正方形时剪裁成内贴圆形;矩形时剪裁成内贴椭圆 |
| ClipRRect | 将子组件剪裁为圆角矩形 |
| ClipRect | 默认剪裁掉子组件布局空间之外的绘制内容(溢出部分裁剪) |
| ClipPath | 按照自定义的路径剪裁 |
实例:
class ClipTestRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 头像
Widget avatar = Image.asset("imgs/avatar.png", width: 60.0);
return Center(
child: Column(
children: <Widget>[
avatar, //不剪裁
ClipOval(child: avatar), //剪裁为圆形
ClipRRect( //剪裁为圆角矩形
borderRadius: BorderRadius.circular(5.0),
child: avatar,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Align(
alignment: Alignment.topLeft,
widthFactor: .5,//宽度设为原来宽度一半,另一半会溢出
child: avatar,
),
Text("你好世界", style: TextStyle(color: Colors.green),)
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ClipRect(//将溢出部分剪裁
child: Align(
alignment: Alignment.topLeft,
widthFactor: .5,//宽度设为原来宽度一半
child: avatar,
),
),
Text("你好世界",style: TextStyle(color: Colors.green))
],
),
],
),
);
}
}
自定义裁剪(CustomClipper)
//自定义裁剪
class MyClipper extends CustomClipper<Rect> {
//用于获取剪裁区域的接口
@override
Rect getClip(Size size) => Rect.fromLTWH(10.0, 15.0, 40.0, 30.0);
//接口决定是否重新剪裁。如果应用中,剪裁区域始终不会发生变化时应该返回false,这样就不会触发重新剪裁,避免不必要的性能开销。如果剪裁区域发生变化(比如对剪裁区域执行一个动画),那么变化后应该返回true来重新绘制
@override
bool shouldReclip(CustomClipper<Rect> oldClipper) => false;
}
使用实例:
DecoratedBox(
decoration: BoxDecoration(
color: Colors.red
),
child: ClipRect(
clipper: MyClipper(), //使用自定义的clipper
child: avatar
),
)
ClipPath可以按照自定义的路径实现剪裁,它需要自定义一个CustomCliper 类型的Clipper,定义方式和MyClipper类似,只不过getClip需要返回一个Path。
FittedBox
子组件超出了父组件大小时,如果不经过处理的话Flutter中就会显示一个溢出警告,并打印错误日志。
//比如
Padding(
padding: const EdgeInsets.symmetric(vertical: 30.0),
child: Row(children: [Text('xx'*30)]), //文本长度超出 Row 的最大宽度会溢出
)
根据Flutter的布局协议,父组件会将自身的最大显示空间作为约束传递给子组件,子组件应该遵守父组件的约束,如果子组件原始大小超过了父组件的约束区域,则需要进行一些缩小,剪裁或其他处理,而不同的组件的处理方式是特定的,比如Text组件,如果它的父组件宽度固定,高度不限的话,则默认情况下Text会在文本达到父组件宽度的时候进行换行。如果我们想让Text文本在超过父组件宽度时不换行而是字体缩小,或者父组件宽高固定,而Text文本较少,想让文本放大以填充整个父组件的空间。这种情况的本质就是:子组件如何适配父组件空间。根据Flutter布局协议适配算法应该在容器或布局组件的layout中实现,为了方便开发者自定义适配规则,Flutter提供了FittedBox组件。
const FittedBox({
Key? key,
this.fit = BoxFit.contain, // 适配方式
this.alignment = Alignment.center, //对齐方式
this.clipBehavior = Clip.none, //是否剪裁
Widget? child,
})
原理:
- FittedBox在布局子组件时会忽略其父组件传递的约束,可以允许子组件无限大,即FittedBox传递给子组件的约束为(0<=width<=double.infinity,0<=height<=double.infinity)。
- FittedBox对子组件布局结束后就可以获得子组件真实的大小。
- FittedBox知道子组件的真实大小也知道它父组件的约束,那么FittedBox就可以通过指定的适配方式(BoxFit枚举中指定),让其子组件在FittedBox父组件的约束范围内按照指定的方式显示。
举例:
//父Container要比子Container小,所以当没有指定任何适配方式时,子组件会按照其真实大小进行绘制,所以蓝色区域会产出父组件的空间,因而看不到红色区域。
Widget build(BuildContext context) {
return Center(
child: Column(
children: [
wContainer(BoxFit.none),
Text('test sssss 1111'),
wContainer(BoxFit.contain),
Text('Flutter中国'),
],
),
);
}
//指定适配方式BoxFit.contain,含义是按照子组件的比例缩放,尽可能多的占据父组件空间,因为子组件的长宽并不相同,所以按照比例缩放适配父组件后,父组件能显示一部分。
Widget wContainer(BoxFit boxFit) {
return Container(
width: 50,
height: 50,
color: Colors.red,
child: FittedBox(
fit: boxFit,
// 子容器超过父容器大小
child: Container(width: 60, height: 70, color: Colors.blue),
),
);
}
需要注意的是:在未指定适配方式时,虽然FittedBox子组件的大小超过了FittedBox父Container的空间,但FittedBox自身还是需要遵守其父组件传递的约束,所以最终FittedBox的本身的大小是5050,这也是为什么蓝色会和下面文本重叠的原因,因为在布局空间内,父Container只占5050的大小,接下来文本会紧挨着Container进行布局,而此时Container中有子组件的大小超过了自己,所以最终的效果就是绘制范围超出了Container,布局位置是正常的,所以重叠了,如果不想让蓝色超出父组件的布局范围,那么可以使用ClipRect对超出部分进行裁剪。
ClipRect( // 将超出子组件布局范围的绘制内容剪裁掉
child: Container(
width: 50,
height: 50,
color: Colors.red,
child: FittedBox(
fit: boxFit,
child: Container(width: 60, height: 70, color: Colors.blue),
),
),
);
实例
@override
Widget build(BuildContext context) {
return Center(
child: Column(
children: [
wRow(' 90000000000000000 '),
FittedBox(child: wRow(' 90000000000000000 ')),
wRow(' 800 '),
FittedBox(child: wRow(' 800 ')),
]
.map((e) => Padding(
padding: EdgeInsets.symmetric(vertical: 20),
child: e,
))
.toList();,
),
);
}
// 直接使用Row
Widget wRow(String text) {
Widget child = Text(text);
child = Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,//将水平方向剩余显示空间均分成多份穿插在每一个child之间。
children: [child, child, child],
);
return child;
}
可以看到,当数字为' 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 的最终宽度就是子组件的宽度之和。 父组件传递给子组件的约束可以用我们上一章中封装的 LayoutLogPrint 来打印出来:
LayoutLogPrint(tag: 1, child: wRow(' 800 ')),
FittedBox(child: LayoutLogPrint(tag: 2, child: wRow(' 800 '))),
//输出
flutter: 1: BoxConstraints(0.0<=w<=396.0, 0.0<=h<=Infinity)
flutter: 2: BoxConstraints(unconstrained)
问题原因找到了,那解决的思路就很简单了,我们只需要让FittedBox 子元素接收到的约束的 maxWidth 为屏幕宽度即可,为此我们封装了一个 SingleLineFittedBox 来替换 FittedBox 以达到我们预期的效果,实现如下:
class SingleLineFittedBox extends StatelessWidget {
const SingleLineFittedBox({Key? key,this.child}) : super(key: key);
final Widget? child;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (_, constraints) {
return FittedBox(
child: ConstrainedBox(
constraints: constraints.copyWith(
//让 maxWidth 使用屏幕宽度
maxWidth: constraints.maxWidth
),
child: child,
),
);
},
);
}
}
//将上面代码替换为
wRow(' 90000000000000000 '),
SingleLineFittedBox(child: wRow(' 90000000000000000 ')),
wRow(' 800 '),
SingleLineFittedBox(child: wRow(' 800 ')),
返现 800 正常显示了,但用SingleLineFittedBox包裹的 ' 90000000000000000 ' 的那个 Row 却溢出了!溢出的原因其实也很简单,因为我们在 SingleLineFittedBox 中将传给 Row 的 maxWidth 置为屏幕宽度后,效果和不加SingleLineFittedBox 的效果是一样的,Row 收到父组件约束的 maxWidth 都是屏幕的宽度,所以搞了半天实现了个寂寞。但是,不要放弃,其实离胜利只有一步,只要我们稍加修改,就能实现我们的预期,话不多说,直接上代码:
class SingleLineFittedBox extends StatelessWidget {
const SingleLineFittedBox({Key? key,this.child}) : super(key: key);
final Widget? child;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (_, constraints) {
return FittedBox(
child: ConstrainedBox(
constraints: constraints.copyWith(
minWidth: constraints.maxWidth,
maxWidth: double.infinity,
//maxWidth: constraints.maxWidth
),
child: child,
),
);
},
);
}
}
代码很简单,将最小宽度(minWidth)约束指定为屏幕宽度,因为Row必须得遵守父组件的约束,所以 Row 的宽度至少等于屏幕宽度,所以就不会出现缩在一起的情况;同时将 maxWidth 指定为无限大,则就可以处理数字总长度超出屏幕宽度的情况。