Flutter 学习 容器类Widget
Posted RikkaTheWorld
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Flutter 学习 容器类Widget相关的知识,希望对你有一定的参考价值。
文章目录
1. 概述
容器类和布局类都是接收子Widget展示,他们有很多相同点,而它们的不同点是:
- 布局Widget一般接收一个 Widget 数组,它们直接或间接继承自
MultiChildRenderObjectWidget
,而容器类 Widget 一般只需要接收一个 子Wdiget,它们直接或间接继承SingleChildRenderObjectWidget
- 布局类 Widget 是按照一定的配列方式来对其子 Widget 进行排列,而 容器类Widget 一般只包装其 子Widget,对齐添加一些修饰、变化或限制
2. 填充 Padding
Padding定义:
class Padding extends SingleChildRenderObjectWidget
const Padding(
...
required this.padding,
Widget? child,
)
...
padding 是 EdgeInsetsGeometry
,我们一般使用 EdgeInsets
,它是子类,用于指定留白的大小,定义了一些便捷的方法:
formLTRB(...)
分别指定四个方向的填充all(...)
所有方向均使用相同数组的填充only(...)
仅指定某几个方向的填充symmetric(...)
用于设置对称方向的填充,例如vertical
指上(top)下(bottom)
例子:
Padding(padding: EdgeInsets.all(20.0), // 所有方向留白20像素
child: Column(
children: const [
Padding(padding: EdgeInsets.only(right: 10),child: Text("右边留白"),),
Padding(padding: EdgeInsets.symmetric(vertical: 20),child: Text("上下留白"),),
Padding(padding: EdgeInsets.fromLTRB(5, 20, 20, 8),child: Text("四周留白")),
],
)),
3. 装饰容器 DecoratedBox
DecoratedBox
可以在其子组件绘制前(或后)绘制一些装饰(Decoration),如背景、边框或者渐变,定义如下:
class DecoratedBox extends SingleChildRenderObjectWidget
const DecoratedBox(
required this.decoration,
this.position = DecorationPosition.background,
Widget? child,
)
- decoration
代表要绘制的装饰,类型是Decoration
,它是一个抽象类,定义了一个接口createBoxPainter()
,子类用其实现一个画笔,用于绘制装饰。 position
此属性决定在哪里绘制Decoration
,它接收DecorationPosition
的枚举类型,该枚举类型的值为:
①background
:在子组件之后绘制,用于画背景
②foreground
: 在子组件之上绘制,用于画前景
decoration 一些默认的实现类有:
来学习下 BoxDecoration
3.1 BoxDecration
我们通常会直接使用 BoxDecration
类,它是一个 Decration 的子类,用于实现常用的装饰元素的绘制。
const BoxDecoration(
this.color,
this.image,
this.border, // 边框
this.borderRadius, //圆角
this.boxShadow, // 阴影
this.gradient, // 渐变
this.backgroundBlendMode, // 背景混合模式
this.shape = BoxShape.rectangle, // 形状
)
下面来实现一个带阴影和渐变背景的按钮:
DecoratedBox(
decoration: BoxDecoration(
gradient:const LinearGradient(colors: [Colors.blue, Colors.purple]), // 匀速渐变
borderRadius: BorderRadius.circular(5.0), //圆角
boxShadow: const [
BoxShadow(
color: Colors.black54, //阴影颜色
offset: Offset(2.0, 2.0), // 阴影深度
blurRadius: 6.0 //阴影圆角
)
]
),
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 80, vertical: 18.0),
child: Text("Rikka", style: TextStyle(color: Colors.white))
),
)
效果如下:
4. 变换 Transform
Transform
可以在其子组件绘制时对其应用一些矩阵变换实现动画效果, Matrix4
是一个4D矩阵,通过它们可以实现不同的操作:
Container(
color: Colors.yellow,
child: Transform(
alignment: Alignment.topRight, // 相对坐标系原点的对齐方式
transform: Matrix4.skewY(0.3), // 沿着 Y 轴倾斜 0.3
child: Container(
padding: EdgeInsets.all(8.0),
color: Colors.deepOrange,
child: Text("This is Funker"),
),
),
)
]),
效果为:
4.1 平移
使用 Transform.translate
进行平移,它接收 Offset
参数,用于指定在 x、y轴对子组件的平移距离:
DecoratedBox(
decoration: BoxDecoration(color: Colors.red),
child: Transform.translate(
offset: Offset(20.0, 10.0), child: Text("Hello rikka")))
指定该Text的x轴向右平移20,y轴向下平移10,效果如下:
4.2 旋转
使用 Transform.rotate
进行旋转,使用 angle
来指定旋转角度 :
import 'dart:math' as math;
DecoratedBox(
decoration: BoxDecoration(color: Colors.red),
child: Transform.rotate(
angle: math.pi/3
, child: Text("Hello rikka")))
效果如下:
4.3 缩放
使用 Transform.scale
进行缩放,使用 angle
来指定旋转角度 :
DecoratedBox(
decoration: BoxDecoration(color: Colors.red),
child: Transform.scale(scale: 2.0, child: Text("Hello rikka"))),
4.4 RotatedBox
Transform
的变换阶段是在绘制阶段,这是在布局阶段之后,所以无论对子组件应用何种变化,其占用空间的大小和屏幕上的位置都是固定不变的。
用官方的例子来说,看下面代码:
效果是这样的:
这是因为第一个 Text 实际占据的部分就是红色区域,即使其子组件缩放,也不会改变组件的实际位置,而后面的Text是紧跟红色区域的,就产生了文字重叠。
为了解决这个问题,Flutter 封装了一些可以在布局阶段之后变换的Widget,比如 RotatedBox
,它是用于旋转的,代码示例如下:
Row(mainAxisAlignment: MainAxisAlignment.center, children: const [
DecoratedBox(
decoration: BoxDecoration(color: Colors.red),
// 旋转90度
child: RotatedBox(quarterTurns: 1, child: Text("Hello rikka"))),
Text(
"Hello",
style: TextStyle(color: Colors.blue),
)
])
5. Container容器
Container
本身没有具体的 RenderObject,因为它继承的是 StatelessWidget
,它是用来组合 DecoratedBox、ConstrainedBox、Transform、Padding等组件的一个容器,它定义如下:
class Container extends StatelessWidget
Container(
...
this.alignment,
this.padding,
this.color,
this.decoration,
this.foregroundDecoration,
double? width,
double? height,
BoxConstraints? constraints,
this.margin,
this.transform,
this.transformAlignment,
this.child,
this.clipBehavior = Clip.none,
)
来看看几个重要的
width
、height
可以指定容器的大小,同时constraints
也可以指定,如果同时存在,则优先使用witdh
和height
, 实际上,constraints 也是由 width、height 来生成的color
和decoration
是互斥的,同时使用会报错, 而 decoration 是由 color 创建的
5.1 Padding 和Margin
这两个属性对 android开发来已经是老朋友了, padding 用来留白、 margin 用来补白。 而 Container
中使用 Padding
组件来实现的,例如下面代码:
Container(
margin: EdgeInsets.all(20.0),
color: Colors.blue,
child: Text("Hello Rikka"),
),
Container(
padding: EdgeInsets.all(20.0),
color: Colors.blue,
child: Text("Hello Rikka"),
)
和下面代码等价:
Padding(
padding: EdgeInsets.all(20.0),
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.blue),
child: Text("Hello Rikka"))),
DecoratedBox(
decoration: BoxDecoration(color: Colors.blue),
child: Padding(
padding: EdgeInsets.all(20.0), child: Text("Hello Rikka")))
6. Clip
来看下剪裁相关的 Widget:
ClipOval
子组件为正方形时剪裁成内贴圆形,为矩形时,剪裁成内贴椭圆ClipRRect
将子组件剪裁为圆角矩形ClipRect
默认剪裁子组件布局空间之外的绘制内容ClipPath
按照自定义的路径剪裁
来看下例子:
@override
Widget build(BuildContext context)
Widget avatar = Image.asset("images/bobo.jpg", width: 60.0);
return Scaffold(
appBar: AppBar(
title: const Text("Basics Demo"),
),
body: Center(
child: Column(
children: [
avatar, //不剪裁
ClipOval(child: avatar), //剪成圆形
ClipRRect(
//剪裁为圆角矩形
borderRadius: BorderRadius.circular(5.0),
child: avatar,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Align(
alignment: Alignment.topLeft,
widthFactor: .5, //宽度设为原来宽度的一半,另一半则溢出,
child: avatar,
),
const Text("Hello Rikka",
style: TextStyle(color: Colors.blue))
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ClipRect(
//将溢出部分裁剪
child: Align(
alignment: Alignment.topLeft,
widthFactor: .5, //宽度设置为原来的一半
child: avatar,
),
),
const Text(
"Hello Rikka",
style: TextStyle(color: Colors.blue),
)
],
)
],
),
));
值得一提的是最后的两个 Row, 他们都设置了 widthFactor
为0.5,即将图片设置为原来的一半
- 第一个
Row
图片溢出部分仍然会显示 - 第二个
Row
剪裁掉了溢出的部分
6.1 CustomClipper
如果我们只想剪裁子组件的特定区域,例如图片中间的 60*60 像素范围,可以使用 CustomClipper
来自定义剪裁区域,使用如下:
class CenterClipper extends CustomClipper<Rect>
@override
Rect getClip(Size size) =>const Rect.fromLTWH(15, 15, 30, 30);
@override
bool shouldReclip(covariant CustomClipper<Rect> oldClipper) => false;
getClip
用于获取剪裁区域的接口,由于图片是 1000*1000像素,所以中间区域就是 (250, 250, 500, 500)shouldReclip
决定是否剪裁,如果剪裁区域是不中部发生变化,应该返回false,这样就不会触发重新剪裁,避免不必要的性能开销
接着我们来使用这个 Clip:
DecoratedBox(
decoration: const BoxDecoration(color: Colors.blue),
child: ClipRect(
clipper: CenterClipper(),
child: avatar,
),
)
这里就剪裁成功了,但是图片所占用控件大小依然是60*60,这是因为组件大小是在 layout 阶段确定的,而剪裁是在之后绘制进行的,这和 Transform
的原理类似。
7 FittedBox
我们开发中经常会遇到子元素超过父容器大小的情况。
比如将一张大图片显示在一个较小的区域,父组件会将自身最大的显示空间做为约束传递给子组件,子组件如果超出了这个约束,就要做一些缩小、裁剪。
例如 Text 组件如果其他父组件宽度固定,高度不限的话,则默认情况下 Text 会在文本达到父组件宽度时换行,那如果我们想让 Text 文本在超过父组件的宽度时不要换行而是字体缩小,或者在父组件宽高固定时,而Text文本比较小,此时想让文本放大以填充整个父组件空间该这么做呢?
这个问题的本质是 子组件如何适配父组件空间, Flutter提供了一 FittedBox
组件来解决这个问题,定义如下:
const FittedBox(
Key? key,
this.fit = BoxFit.contain,
this.alignment = Alignment.center,
this.clipBehavior = Clip.none,
Widget? child,
)
FittedBox的原理:
FittedBox
在布局子组件时,会忽略父组件传递的约束,可以允许子组件无限大FittedBox
对子组件布局结束后获得子组件的真实大小FittedBox
知道子组件的真实大小,也知道它父组件的约束,那么 FittedBox 就可以通过指定的适配方式(由 BoxFit 的枚举值),让子组件在 FittedBox 父组件的约束范围内按照指定的方式显示
下面是一个示例:
return Center(
child: Column(
children: [
wContainer(BoxFit.none),
Text("Rikka"),
wContainer(BoxFit.contain),
Text("The World"),
],
),
);
...
Widget wContainer(BoxFit boxFit)
return Container(
width: 50,
height: 50,
color: Colors.red,
child: FittedBox(
fit: boxFit,
// 子容器超过父容器大小
child: Container(width: 80, height: 90, color: Colors.blue),
),
);
BoxFit.container
就是按照子组件的比例进行缩放,尽可能多的占据父组件空间
7.1 示例:单行缩放布局
我们有三个数据,都需要在一行展示,换行是不能接受的。 如果数据过多,就会出现数据太长或屏幕太窄,无法显示在一行的情况,因此,我们希望如果无法一行显示时,要对组件进行适当的缩放,以保证一行能够显示的下。
@override
Widget build(BuildContext context)
return Scaffold(
appBar: AppBar(
title: const Text("Basics Demo"),
),
body: Center(
child: Column(
children: [
_wRow(" 90000000000000000000000000 "),
FittedBox(child: _wRow(" 90000000000000000000000000 ")),
_wRow(" 800 "),
FittedBox(child: _wRow(" 800 ")),
]
.map((e) => Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: e,
))
.toList(),
),
),
);
...
Widget _wRow(String text)
Widget result = Text(text);
result = Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [result, result, result],
);
return result;
我们给 Row 在主轴的对齐方式是 MainAxisAlignment.spaceEvenly
,这会将水平方向的剩余显示空间均分成多份穿插在每一个 child 之间,也就是平等划分区域。
可以看到,当数字为 “90…” 时,三个数字的长度之和已经超出了屏幕的宽度,所以 Row 会有溢出,让给 Row 添加了 FittedBox
时,就可以按比例缩放至一行显示,实现了我们的效果。
但是当数字没有那么大时,如 “800”,直接使用 Row 是符合预期的,但是使用了 FittedBox
却挤在了一起,不符合我们的预期。之所以会这样,是因为指定主轴对齐方式为 sapceEvenly
: Row在布局时会拿到父组件的约束,如果约束的 maxWidth 不是无限大, Row 就会依据子组件的数量和它们的大小在主轴方向来根据 spaceEvenly
填充算法来分割水平的长度,最终 Row 的宽度为 maxWidth,但如果 maxWidth 为无限大,就无法进行分割了,所以此时 Row 就会将子组件宽度之和作为自己的宽度,导致出现这样的结果。
所以此时的解决方法,就是让 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,
));
);
然后使用它:
children: [
_wRow(" 90000000000000000000000000 "),
SingleLineFittedBox(child: _wRow(" 90000000000000000000000000 ")),
_wRow(" 800 "),
SingleLineFittedBox(child: _wRow(" 800 ")),
]
这下下面修复了,但是上面却溢出了, 这是要因为:我们在 SIngleLineFittedBox
中将 Row 的maxWidth 设置为屏幕宽度后,效果和不加 SingleLineFittedBox 的效果是一样的,Row 收到父组件约束的 maxWidth 都是屏幕的宽度,这个是时候需要少加修改就可以实现:
class S以上是关于Flutter 学习 容器类Widget的主要内容,如果未能解决你的问题,请参考以下文章