目录
1. Container容器
2. DecoratedBox装饰容器
3. Padding内边距留白
4. 尺寸限制类容器
ConstrainedBox 给子组件设置最大最小宽高约束
SizedBox 给子组件设置固定的宽高
UnconstrainedBox
AspectRatio 指定子组件的长宽比
LimitedBox 指定最大宽高
FractionallySizedBox 根据父容器宽高的百分比来设置子组件宽高等
5. FittedBox(自定义适配规则)
Flutter官方并没有对Widget进行官方分类,对其分类主要是为了对Widget进行功能区分。
容器类和布局类Widget的区别
相同点:
1. 都作用于其子Widget
不同点:
1. 布局类Widget直接或间接继承MultiChildRenderObjectWidget(拥有一个children属性用于接收多个子Widget)。容器类Widget直接或间接继承自(或包含)SingleChildRenderObjectWidget(拥有一个child属性)。
2. 布局类Widget是按照一定的布局方式来对其子Widget进行布局。容器类Widget是对其子Widget进行了包装修饰(补白、旋转或剪裁、大小限制)。
1. Container容器
对子组件进行装饰、变换、大小限制。
类似于html的div。
// Container本身不对应具体的RenderObject,是由DecoratedBox、ConstrainedBox、Transform、Padding、Align等组件组合起来的。
Container({
this.alignment, // Alignment.left
this.padding, // 内边距,属于decoration的装饰范围
Color color, // 背景色
Decoration decoration, // 背景装饰
Decoration foregroundDecoration, // 前景装饰
double width, // 宽,设置为double.infinity则等于父容器宽
double height, // 高
BoxConstraints constraints, // 容器大小的限制条件
this.margin,// 外边距,不属于decoration的装饰范围
this.transform, // 变换
this.child,
})
注意:
1. 容器的大小可以通过width、height属性来指定,也可以通过constraints来指定;如果它们同时存在时,width、height优先。本质上Container内部会根据width、height来生成一个constraints。
2. color和decoration是互斥的,如果同时设置它们则会报错。本质上,当指定color时,Container内会自动创建一个decoration。
示例1
Container(
margin: EdgeInsets.only(top: 50.0, left: 120.0), // 外边距
constraints: BoxConstraints.tightFor(width: 200.0, height: 150.0), // 宽高
decoration: BoxDecoration( // 背景装饰
gradient: RadialGradient( // 渐变
colors: [Colors.red, Colors.orange],
center: Alignment.topLeft,
radius: .98
),
boxShadow: [ // 阴影
BoxShadow(
color: Colors.black54,
offset: Offset(2.0, 2.0),
blurRadius: 4.0
)
]
),
transform: Matrix4.rotationZ(.2), // 形变(旋转)
alignment: Alignment.center, // 居中
child: Text( // 文本
"5.20", style: TextStyle(color: Colors.white, fontSize: 40.0),
),
);
示例2(Padding和Margin)
Container(
margin: EdgeInsets.all(20.0),
color: Colors.orange,
child: Text("Hello world!"),
),
Container(
padding: EdgeInsets.all(20.0),
color: Colors.orange,
child: Text("Hello world!"),
),
====================
Container内margin和padding都是通过Padding 组件来实现的,上面的示例代码实际上等价于:
Padding(
padding: EdgeInsets.all(20.0),
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.orange),
child: Text("Hello world!"),
),
),
DecoratedBox(
decoration: BoxDecoration(color: Colors.orange),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Text("Hello world!"),
),
),
变换(Transform)
Matrix4是一个4D矩阵,通过它可以实现各种矩阵操作.
Transform的变换是应用在绘制阶段,而并不是应用在布局阶段,所以无论对子组件应用何种变化,其占用空间的大小和在屏幕上的位置都是固定不变的,因为这些是在布局阶段就确定的。
由于矩阵变化只会作用在绘制阶段,所以在某些场景下,在UI需要变化时,可以直接通过矩阵变化来达到视觉上的UI改变,而不需要去重新触发build流程,这样会节省layout的开销,所以性能会比较好。如Flow组件,它内部就是用矩阵变换来更新UI,除此之外,Flutter的动画组件中也大量使用了Transform以提高性能。
例
Container(
color: Colors.black,
child: new Transform(
alignment: Alignment.topRight, // 相对于坐标系原点的对齐方式
transform: new Matrix4.skewY(0.3), // 沿Y轴倾斜0.3弧度
child: new Container(
padding: const EdgeInsets.all(8.0),
color: Colors.deepOrange,
child: const Text('Apartment for rent!'),
),
),
);
例
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
DecoratedBox(
decoration:BoxDecoration(color: Colors.red),
child: Transform.scale(scale: 1.5,
child: Text("Hello world") // 缩放
)
),
Text("你好", style: TextStyle(color: Colors.green, fontSize: 18.0),)
],
)
由于第一个Text应用变换(放大)后,其在绘制时会放大,但其占用的空间依然为红色部分,所以第二个Text会紧挨着红色部分,最终就会出现文字重合
- 平移
Transform.translate接收一个offset参数,可以在绘制时沿x、y轴对子组件平移指定的距离。
例
DecoratedBox(
decoration:BoxDecoration(color: Colors.red),
// 默认原点为左上角,左移20像素,向上平移5像素
child: Transform.translate(
offset: Offset(-20.0, -5.0),
child: Text("Hello world"),
),
)
- 旋转
Transform.rotate可以对子组件进行旋转变换
要使用math.pi需先进行导包:import 'dart:math' as math;
例
DecoratedBox(
decoration:BoxDecoration(color: Colors.red),
child: Transform.rotate(
//旋转90度
angle:math.pi/2 ,
child: Text("Hello world"),
),
);
- 缩放
Transform.scale可以对子组件进行缩小或放大
例
DecoratedBox(
decoration:BoxDecoration(color: Colors.red),
child: Transform.scale(
scale: 1.5, //放大到1.5倍
child: Text("Hello world")
)
);
RotatedBox
RotatedBox和Transform.rotate功能相似,它们都可以对子组件进行旋转变换,但是有一点不同:RotatedBox的变换是在layout阶段,会影响在子组件的位置和大小。
例
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
DecoratedBox(
decoration: BoxDecoration(color: Colors.red),
//将Transform.rotate换成RotatedBox
child: RotatedBox(
quarterTurns: 1, //旋转90度(1/4圈)
child: Text("Hello world"),
),
),
Text("你好", style: TextStyle(color: Colors.green, fontSize: 18.0),)
],
),
由于RotatedBox是作用于layout阶段,所以子组件会旋转90度(而不只是绘制的内容),decoration会作用到子组件所占用的实际空间上。
2. DecoratedBox装饰容器
修饰子组件(背景、边框、渐变等)
DecoratedBox({
// BoxDecoration类(继承自Decoration装饰抽象类)的主要职责是通过实现createBoxPainter方法来创建一个画笔,该画笔用于绘制装饰。
Decoration decoration,
/*
在哪绘制Decoration(DecorationPosition枚举类型)
1. background:在子组件之后绘制,即背景装饰。
2. foreground:在子组件之上绘制,即前景装饰。
*/
DecorationPosition position = DecorationPosition.background,
Widget child
})
BoxDecoration({
Color color, // 背景色
DecorationImage image, // 背景图片,DecorationImage(image: fit:)
BoxBorder border, // 边框,BoxBorder.all(color: width:)
BorderRadiusGeometry borderRadius, // 圆角,BorderRadius.circular(宽/2)切圆,BorderRadius.all(Radius.circular(宽/2))切圆
List<BoxShadow> boxShadow, // 阴影,可以指定多个
Gradient gradient, // 渐变,LinearGradient类用于定义线性渐变的类,还有其它渐变配置类:RadialGradient、SweepGradient
BlendMode backgroundBlendMode, // 背景混合模式
BoxShape shape = BoxShape.rectangle, // 背景形状,默认是矩形。
})
/*
例(线性渐变)
LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xFF56AF6D),
Color(0xFF56AA6D),
]
),
*/
/*
DecorationImage({
required this.image,
this.onError,
this.colorFilter,
this.fit,
this.alignment = Alignment.center,
this.centerSlice,
this.repeat = ImageRepeat.noRepeat,
this.matchTextDirection = false,
this.scale = 1.0
})
Border({ // 继承自BoxBorder
this.top = BorderSide.none,
this.right = BorderSide.none,
this.bottom = BorderSide.none,
this.left = BorderSide.none,
})
BorderSide({
this.color = const Color(0xFF000000),
this.width = 1.0,
this.style = BorderStyle.solid,
})
RadialGradient({ // 继承自Gradient
this.center = Alignment.center,
this.radius = 0.5,
required List<Color> colors,
List<double>? stops,
this.tileMode = TileMode.clamp,
this.focal,
this.focalRadius = 0.0,
GradientTransform? transform,
})
LinearGradient({ // 继承自Gradient。
this.begin = Alignment.centerLeft,
this.end = Alignment.centerRight,
required List<Color> colors,
List<double>? stops,
this.tileMode = TileMode.clamp,
GradientTransform? transform,
})
*/
示例
DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(colors:[Colors.red,Colors.orange[700]]), // 背景渐变。渐变配置类:LinearGradient、RadialGradient、SweepGradient
borderRadius: BorderRadius.circular(3.0), // 3像素圆角
boxShadow: [ // 阴影
BoxShadow(
color:Colors.black54, // 阴影色
offset: Offset(2.0,2.0), // x轴偏移,y轴偏移
blurRadius: 4.0 // 模糊程度,值越大越明显
)
]
),
child: Padding(padding: EdgeInsets.symmetric(horizontal: 80.0, vertical: 18.0),
child: Text("Login", style: TextStyle(color: Colors.white),),
)
)
3. Padding 内边距留白
内边距留白。
Padding({
EdgeInsetsGeometry padding,
Widget child,
})
说明:
1. EdgeInsets类(继承自EdgeInsetsGeometry抽象类)的便捷方法:
1. fromLTRB(double left, double top, double right, double bottom):分别指定四个方向的填充。
2. all(double value) : 所有方向均使用相同数值的填充。
3. only({left, top, right ,bottom }):可以设置具体某个方向的填充(可以同时指定多个方向)。
4. symmetric({ vertical, horizontal }):用于设置对称方向的填充,vertical指top和bottom,horizontal指left和right。
示例
class PaddingTestRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
//上下左右各添加16像素补白
padding: EdgeInsets.all(16.0),
child: Column(
// 显式指定对齐方式为左对齐,排除对齐干扰
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
// 左边添加8像素补白
padding: const EdgeInsets.only(left: 8.0),
child: Text("Hello world"),
),
Padding(
// 上下各添加8像素补白
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Text("I am Jack"),
),
Padding(
// 分别指定四个方向的补白
padding: const EdgeInsets.fromLTRB(20.0,.0,20.0,20.0),
child: Text("Your friend"),
)
],
),
);
}
}
4. 尺寸限制类容器
用于限制容器大小
1. ConstrainedBox
给子组件设置最大最小宽高约束
2. SizedBox
给子组件设置固定的宽高
3. UnconstrainedBox
4. AspectRatio
指定子组件的长宽比
5. LimitedBox
指定最大宽高
6. FractionallySizedBox
根据父容器的宽高百分比来设置子组件宽高
Flutter中有两种布局模型:
1. 基于RenderBox的盒模型布局。
1. 组件对应的渲染对象都继承自 RenderBox 类。
2. 在布局过程中父级传递给子级的约束信息由 BoxConstraints 描述。
2. 基于RenderSliver(Sliver)按需加载列表布局。
两种布局方式在细节上略有差异,但大体流程相同,布局流程如下:
1. 上层组件向下层组件传递约束条件。
2. 下层组件确定自己的大小,然后告诉上层组件。注意下层组件的大小必须符合父组件的约束。
3. 上层组件确定下层组件相对于自身的偏移和确定自身的大小(大多数情况下会根据子组件的大小来确定自身的大小)。
例:
父组件传递给子组件的约束是“最大宽高不能超过100,最小宽高为0”,如果给子组件设置宽高都为200,则子组件最终的大小是100*100。
因为任何时候子组件都必须先遵守父组件的约束,在此基础上再应用子组件约束(相当于父组件的约束和自身的大小求一个交集)。
- ConstrainedBox
给子组件设置最大最小宽高约束(BoxConstraints约束)。
BoxConstraints({
this.minWidth = 0.0, // 最小宽度
this.maxWidth = double.infinity, // 最大宽度
this.minHeight = 0.0, // 最小高度
this.maxHeight = double.infinity // 最大高度
})
盒模型布局过程中父渲染对象传递给子渲染对象的约束信息(包含最大最小宽高),子组件大小需要在约束的范围内。
BoxConstraints的便捷构造函数:
1. BoxConstraints.tight(Size size) 生成给定大小的BoxConstraints。
2. BoxConstraints.expand() 生成尽可能占用另一个容器的BoxConstraints。
示例1(让子组件的最小高度是80像素)
使用BoxConstraints(minHeight: 80.0)作为子组件的约束。
示例2
定义一个redBox,它是一个背景颜色为红色的盒子,不指定它的宽度和高度
Widget redBox=DecoratedBox(
decoration: BoxDecoration(color: Colors.red),
);
实现一个最小高度为50,宽度尽可能大的红色容器
ConstrainedBox(
constraints: BoxConstraints(
minWidth: double.infinity, // 宽度尽可能大
minHeight: 50.0 // 最小高度为50像素
),
child: Container(
height: 5.0,
child: redBox
),
)
虽然将Container的高度设置为5像素,但是最终却是50像素,这正是ConstrainedBox的最小高度限制生效了。
- SizedBox
给子组件设置固定的宽高
ConstrainedBox和SizedBox的createRenderObject方法都返回一个用来渲染的RenderConstrainedBox对象:
@override
RenderConstrainedBox createRenderObject(BuildContext context) {
return new RenderConstrainedBox(
additionalConstraints: ...,
);
}
示例
SizedBox(
width: 80.0,
height: 80.0,
child: redBox
)
实际上SizedBox只是ConstrainedBox的一个定制,上面代码等价于:
ConstrainedBox(
constraints: BoxConstraints.tightFor(width: 80.0,height: 80.0),
child: redBox,
)
而BoxConstraints.tightFor(width: 80.0,height: 80.0)等价于:
BoxConstraints(minHeight: 80.0,maxHeight: 80.0,minWidth: 80.0,maxWidth: 80.0)
多重限制(有多个父级ConstrainedBox限制)
对于minWidth和minHeight,取父子中相应数值较大的。只有这样才能保证父限制与子限制不冲突。
ConstrainedBox(
constraints: BoxConstraints(minWidth: 60.0, minHeight: 60.0), //父
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: 90.0, minHeight: 20.0),//子
child: redBox,
)
)
最终显示效果是宽90,高60,也就是说是子ConstrainedBox的minWidth生效,而minHeight是父ConstrainedBox生效。
将上例中父子限制条件换一下
ConstrainedBox(
constraints: BoxConstraints(minWidth: 90.0, minHeight: 20.0),
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: 60.0, minHeight: 60.0),
child: redBox,
)
)
最终的显示效果仍然是90,高60,效果相同,但意义不同,因为此时minWidth生效的是父ConstrainedBox,而minHeight是子ConstrainedBox生效。
- UnconstrainedBox
UnconstrainedBox不会对子组件产生任何限制,它允许其子组件按照其本身大小绘制。
很少直接使用此组件,经常用于"去除"多重限制。
在实际开发中,当使用SizedBox或ConstrainedBox给子元素指定了宽高,但是仍然没有效果时,几乎可以断定:已经有父元素已经设置了限制。
UnconstrainedBox对父组件限制的“去除”并非真正去除,仍然占有相应的空间。
没有办法可以彻底去除父ConstrainedBox的限制。任何时候子组件都必须遵守其父组件的约束。在定义一个通用的组件时,如果要对子组件指定限制,那么一定要注意,因为一旦指定限制条件,子组件如果要进行相关自定义大小时将可能非常困难,因为子组件在不更改父组件的代码的情况下无法彻底去除其限制条件。
需要注意,UnconstrainedBox虽然在其子组件布局时可以取消约束(子组件可以为无限大),但是 UnconstrainedBox 自身是受其父组件约束的,所以当 UnconstrainedBox 随着其子组件变大后,如果UnconstrainedBox 的大小超过它父组件约束时,也会导致溢出报错。
示例
ConstrainedBox(
constraints: BoxConstraints(minWidth: 60.0, minHeight: 100.0), // 父
child: UnconstrainedBox( // “去除”父级限制
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: 90.0, minHeight: 20.0),// 子
child: redBox,
),
)
)
上面代码中,如果没有中间的UnconstrainedBox,那么根据上面所述的多重限制规则,那么最终将显示一个90×100的红色框。但是由于UnconstrainedBox “去除”了父ConstrainedBox的限制,则最终会按照子ConstrainedBox的限制来绘制redBox,即90×20。
UnconstrainedBox对父组件限制的“去除”并非是真正的去除:上面例子中虽然红色区域大小是90×20,但上方仍然有80的空白空间。也就是说父限制的minHeight(100.0)仍然是生效的,只不过它不影响最终子元素redBox的大小,但仍然还是占有相应的空间,可以认为此时的父ConstrainedBox是作用于子UnconstrainedBox上,而redBox只受子ConstrainedBox限制。
/*
Column(
children: <Widget>[
UnconstrainedBox(
alignment: Alignment.topLeft,
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(children: [Text('xx' * 30)]),
),
),
]
),
*/
示例
Material组件库中的AppBar(导航栏)的右侧菜单中,使用SizedBox指定了loading按钮的大小,代码如下:
AppBar(
title: Text(title),
actions: <Widget>[
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation(Colors.white70),
),
)
],
)
右侧loading按钮大小并没有发生变化!这正是因为AppBar中已经指定了actions按钮的限制条件,所以要自定义loading按钮大小,就必须通过UnconstrainedBox来“去除”父元素的限制,代码如下:
AppBar(
title: Text(title),
actions: <Widget>[
UnconstrainedBox(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation(Colors.white70),
),
),
)
],
)
AspectRatio 指定子组件的长宽比
LimitedBox 指定最大宽高
FractionallySizedBox 根据父容器的宽高百分比来设置子组件宽高
5. FittedBox(自定义适配规则)
子组件大小超出父组件大小时,会显示溢出警告并在控制台打印错误日志。
示例(溢出)
Padding(
padding: const EdgeInsets.symmetric(vertical: 30.0),
child: Row(children: [Text('xx'*30)]), //文本长度超出 Row 的最大宽度会溢出
)
根据Flutter布局协议,适配算法在容器或布局组件的layout中实现。
FittedBox组件(快速自定义适配规则)。
FittedBox({
Key? key,
this.fit = BoxFit.contain, // 适配方式
this.alignment = Alignment.center, // 对齐方式
this.clipBehavior = Clip.none, // 是否剪裁
Widget? child,
})
适配原理
1. FittedBox在布局子组件时会忽略其父组件传递的约束,可以允许子组件无限大。
FittedBox传递给子组件的约束为(0<=width<=double.infinity, 0<= height <=double.infinity)。
2. FittedBox对子组件布局结束后就可以获得子组件真实的大小。
3. FittedBox知道子组件的真实大小也知道他父组件的约束,那么FittedBox 就可以通过指定的适配方式(BoxFit枚举中指定),让其子组件在FittedBox父组件的约束范围内按照指定的方式显示。
示例(BoxFit.none、BoxFit.contain)
Widget build(BuildContext context) {
return Center(
child: Column(
children: [
wContainer(BoxFit.none),
Text('Wendux'),
wContainer(BoxFit.contain),
Text('Flutter中国'),
],
),
);
}
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),
),
);
}
因为父Container要比子Container 小,所以当没有指定任何适配方式时,子组件会按照其真实大小进行绘制,所以第一个蓝色区域会超出父组件的空间,因而看不到红色区域。第二个指定了适配方式为 BoxFit.contain,含义是按照子组件的比例缩放,尽可能多的占据父组件空间,因为子组件的长宽并不相同,所以按照比例缩放适配父组件后,父组件能显示一部分。
要注意一点,在未指定适配方式时,虽然 FittedBox 子组件的大小超过了 FittedBox 父 Container 的空间,但FittedBox 自身还是要遵守其父组件传递的约束,所以最终 FittedBox 的本身的大小是 50×50,这也是为什么蓝色会和下面文本重叠的原因,因为在布局空间内,父Container只占50×50的大小,接下来文本会紧挨着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),
),
),
);
示例2(单行缩放布局)
有三个数据指标,需要在一行显示,因为换行的话就会将页面布局打乱,所以换行是不能接受的。因为不同设备的屏幕宽度不同,且不同人的数据也不同,所以就会出现数据太长或屏幕太窄时三个数据无法在一行显示,因此,希望当无法在一行显示时能够对组件进行适当的缩放以确保一行能够显示的下:
@override
Widget build(BuildContext context) {
return Center(
child: Column(
children: [
wRow(' 90000000000000000 '), // 溢出
FittedBox(child: wRow(' 90000000000000000 ')), // 按比例缩放
wRow(' 800 '), // BoxConstraints(0.0<=w<=396.0, 0.0<=h<=Infinity)
FittedBox(child: wRow(' 800 ')), // 会挤在一起,FittedBox传给Row的约束的 maxWidth为无限大(double.infinity)BoxConstraints(unconstrained)
]
.map((e) => Padding(
padding: EdgeInsets.symmetric(vertical: 20),
child: e,
))
.toList();,
),
);
}
Widget wRow(String text) {
Widget child = Text(text);
child = Row(
// spaceEvenly:Row在进行布局时会拿到父组件的约束,如果约束的 maxWidth 不是无限大,则 Row 会根据子组件的数量和它们的大小在主轴方向来根据 spaceEvenly 填充算法来分割水平方向的长度,最终Row 的宽度为 maxWidth;但如果 maxWidth 为无限大时,就无法在进行分割了,会将子组件的宽度之和作为自己的宽度。
mainAxisAlignment: MainAxisAlignment.spaceEvenly, // 均分
children: [child, child, child],
);
return child;
}
封装一个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: constraints.maxWidth
minWidth: constraints.maxWidth, // 不会出现缩在一起的情况
maxWidth: double.infinity, // 无限大,处理超出屏幕的情况
),
child: child,
),
);
},
);
}
}
将上例中FittedBox改为SingleLineFittedBox。