Flutter 学习 布局类Widget
Posted RikkaTheWorld
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Flutter 学习 布局类Widget相关的知识,希望对你有一定的参考价值。
文章目录
1. 布局类组件介绍
布局类组件可以容纳一个或多个子组件,Widget根据容纳子树,大致分为三种,如下:
Widget | 说明 | 用途 |
---|---|---|
LeafRenderObjectWidget | 非容器类组件基类 | Widget树的叶子结点,用于没有子节点的Widget |
SingleChildRenderObjectWidget | 单子组件基类 | 只能包含一个子Widget,例如 ConstrainedBox 等 |
MultiChildRenderObjectWidget | 多子组件基类 | 包含多个子Widget,一般有一个children参数,接收一个Widget数组,例如 Row、Column、Stack等 |
它们都继承自 RenderObjectWidget
布局类组件就是直接或间接的继承 SingleChildRenderObjectWidget
或者 MultiChildRenderObjectWidget
的 Widget,一般它们用 child 或者 children 来接收子Widget。
2. 布局原理与约束布局
Flutter 有两种布局模型:
- 基于 RenderBox的盒模型布局
- 基于 Sliver(RenderSliver)按需加载列表布局
两者的实现上有差异,但是大体流程相同:
- 上层组件向下层组件传递约束(constraints)条件
- 下层组件确定自己大小,然后通知上层组件
- 上层组件确定下层组件相对自身的偏移和确定自身的大小(大多数情况下会根据子组件的大小来确定自身的大小)
本节主要来学习 盒模型布局,在之后的可滚动组件里学习 Sliver 模型, 盒模型布局组件有两个特点:
- 组件对应的渲染对象都继承
RenderBox
,如果某个组件是 RenderBox,则指它是盒模型布局 - 在布局过程中父级传递给子级的约束信息由
BoxConstraints
描述
2.1 BoxConstraints
约束信息, 包含参数如下:
class BoxConstraints extends Constraints
const BoxConstraints(
this.minWidth = 0.0, // 最小宽度
this.maxWidth = double.infinity, // 最大宽度,默认为无限大
this.minHeight = 0.0, // 最小高度
this.maxHeight = double.infinity // 最大高度,默认为无限大
)
...
它包含了四个属性,除此之外,还定义了便捷的构造函数,例如
BoxContraints.tight(Size size)
用于生成固定宽高限制BoxConstraints.expand()
可以生成一个尽可能大的填充另一个容器的 BoxConstranits
如果 BoxConstraints 不指定,就可以认为父组件不约束其子组件的宽高。
2.2 ConstrainedBox
ConstrainedBox 用于对子组件添加额外的约束。例如,如果你想让子组件的最小高度是50,可以使用 BoxConstraintes(minHeight: 50) 做为子组件的约束。
例子:我们定义一个 blueBox, 作为背景为红色的盒子,并且不指定它的宽高:
Widget rexBox = const DecoratedBox(decoration: BoxDecoration(color: Colors.blue));
接下来实现一个最小高度为50,宽度尽可能大的容器来包裹上面这个盒子:
ConstrainedBox(
constraints:
BoxConstraints(minWidth: double.infinity, minHeight: 50),
child: Container(
height: 5.0,
child: blueBox,
),
)
效果:
可以看到, 虽然设置了 Container 的高度为5像素,但是最终却是 50像素,这正是 ConstrainedBox 的最小高度限制生效了。
如果将 Container 的高度设置为 80像素,那么最终蓝色区域的高度也会为80像素,因为在此示例中, ConstrainedBox 只限制了最小高度,并未限制最大高度
2.3 SizedBox
用于子元素指定固定的宽高,如:
SizedBox(
width: 50,
height: 50,
child: yellowBox,
)
效果:
2.4 多重限制
如果某一个子组件有多个父级 ConstrainedBox
限制,那么最终会是哪个生效呢?
例如:
ConstrainedBox(
constraints: BoxConstraints(minWidth: 60.0, minHeight: 60.0), //父
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: 90.0, minHeight: 20.0),//子
child: redBox,
),
)
的效果和下面的效果:
ConstrainedBox(
constraints: BoxConstraints(minWidth: 90.0, minHeight: 20.0),
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: 60.0, minHeight: 60.0),
child: redBox,
)
)
是相同的,也就是说:
当一个子 Widget 有多重限制的时候, 对于 minWidth
和 minHeight
来说,是取多重限制中较大的的那个。
2.5 UnconstrainedBox
假如 A 组件的子组件是 B, B 的子组件是 C,那么 A可以约束 B组件,B组件可以约束 C组件, 但是 A组件不会直接约束到C组件,除非 B 将 A 对它的约束透传给了 C。 那么根据该原理,可以实现一个 B组件:
- B组件在布局 C时 不约束 C
- C根据自身的空间占用来确定自身大小
- B在遵守A 的约束前提小结合子组件大小来确定自身大小
这个 B组件就是 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 的框,但是加了这个后,最终会生成 90 * 20 的框。
但是这里请注意, 这个 90*20 并非是真正的大小, 它上面还有 80 像素的空白空间,所以并不是真正的屏蔽了父组件的限制呢。
3. 线性布局
线性布局就是水平或垂直方向排列的子组件。 Flutter 中使用 Row
和 Column
来实现线性布局,它们都继承自 Flex
, Flex
是弹性布局。
3.1 主轴和纵轴
- 如果当前布局是沿水平方向,那么主轴就是水平方向, 而纵轴就是垂直方向
- 如果当前布局是沿垂直方向,那么主轴就是垂直方向,纵轴就是水平方向
在线性布局中,有两个定义对齐方式的枚举类 MainAxisAlignment
和 CrossAxisAlignment
,分别代表主轴对齐和纵轴对齐
3.2 Row
Row可以沿着水平方向排列子 Widget,构造函数如下:
Row(
...
MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
MainAxisSize mainAxisSize = MainAxisSize.max,
CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
TextDirection? textDirection,
VerticalDirection verticalDirection = VerticalDirection.down,
TextBaseline? textBaseline,
List<Widget> children = const <Widget>[],
)
textDirection
表示水平方向子组件的布局顺序是从左往右还是右往左, 默认是系统Locale中的方向mainAxisSize
表示Row
在主轴(就是水平)方向占用的控件,默认是MainAxisSize.max
,表示尽可能多的占用水平方向的空间,此时无论子 widgets 实际占用多少水平空间,Row
的宽度始终等于水平方向的最大宽度; 而MainAxisSize.min
表示尽可能少的占用水平空间,当子组件没有占满水平剩余空间,则Row
的实际宽度等于所有子组件占用的水平空间mainAxisAlignment
表示子组件主轴的对齐方式, 当mainAxisSzie == MainAxisSize.min
时是无意义的,因为子组件宽度就是父组件宽度。 只有等于MainAxisSize.max
时才有意义,MainAxisAligment.start
指从textDirection设置的值开始布局。 也就是说 textDirection 是参考系,这里就不再赘述verticalDirection
表示纵轴的对齐方向。 默认VerticalDirection.down
是从上到下crossAxisAlignment
和mainAxisAlignment
一样,表示纵轴的对齐方式children
定义子组件数组
示例代码:
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("hello "),
Text("i am Rikka"),
],
)
3.3 Column
垂直线性布局。 参数和 Row
是一样的,就是主轴和纵轴和 Row 反着来。 所以可以触类旁通,这里就不再赘述
3.4 特殊情况
如果 Row
里嵌套 Row
, 或者 Column
里嵌套 Column
,那么只有最外面的 Row
或 Column
会占用尽可能大的空间,里面 Row 或 Column
所占用的空间为实际大小,下面以 Column
为例说明:
Container(
color: Colors.yellow,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max, // 有效,外层 Column 主轴占满整个屏幕
children: [
Container(
color: Colors.red,
child: Column(
mainAxisSize: MainAxisSize.max, // 无效,内层 Column 高度为实际占用高度
children: [
Text("hello "),
Text("I am Rikka")
],
),
)
],
),
),
),
效果如下:
如果要让里面的 Column
占满外部的 Column, 可以使用 Expanded
组件:
Expanded(
child: Container(
color: Colors.red,
child: Column(
mainAxisSize: MainAxisSize.max, // 无效,内层 Column 高度为实际占用高度
children: [Text("hello "), Text("I am Rikka")],
),
))
4. 弹性布局
弹性布局允许组件按一定比例来分配父容器空间, Flutter中主要通过 Flex
和 Expanded
来实现
4.1 Flex
Flex
可以沿着水平或者垂直方向排列子组件,如果你知道主轴和纵轴的方向,那么直接使用 Row
或者 Column
会更好,因为它们两个就是继承自 Flex
, 参数也基本相同,所以可以被这两个来替代。
Flex 主要是和 Expanded
配合实现弹性布局, 下面是Flex的构造函数, 和 Row、Column 相同的都用省略号概括了:
Flex(
...
required this.direction, //弹性布局的方向, Row默认为水平方向,Column默认为垂直方向
List<Widget> children = const <Widget>[],
)
4.2 Expanded
Expanded
只能作为 Flex
的孩子(否则会报错),它可以按比例扩展 Flex 子组件所占用的空间。
class Expanded extends Flexible
const Expanded(
Key? key,
int flex = 1,
required Widget child,
)...
flex
弹性系数, 如果为0 或者 null,则 child 是没有弹性的,即不会扩展占用空间。如果大于0,所有的 Expand 按照其 flex 的比例来分割主轴的全部空闲位置。 这玩意就和 android 原生控件 LinearLayout 中的子控件的weight
一样的。这里就不贴代码
这里再多介绍一个 Spacer
, 它是 Expanded
的一个包装类,也可以按比例占用空间, 源码如下,可以用来做分割线什么的:
class Spacer extends StatelessWidget
const Spacer(Key? key, this.flex = 1)
: assert(flex != null),
assert(flex > 0),
super(key: key);
final int flex;
@override
Widget build(BuildContext context)
return Expanded(
flex: flex,
child: const SizedBox.shrink(),
);
5. 流式布局
在使用 Row
或者 Column
时,如果子 Widget 超出屏幕,就会有溢出的错误,如:
Row(
children: <Widget>[
Text("xxx"*100)
],
);
这是因为 Row 默认只有一行,如果超出屏幕不会折行,我们把超出屏幕显示范围会自动折行的布局称为流式布局。 Flutter中通过 Wrap
和 Flow
来支持流式布局,将上例中的 Row 换成 Wrap 将会自动折行。
5.1 Wrap
来看看 Wrap 的参数:
Wrap(
...
this.direction = Axis.horizontal,
this.alignment = WrapAlignment.start,
this.spacing = 0.0,
this.runAlignment = WrapAlignment.start,
this.runSpacing = 0.0,
this.crossAxisAlignment = WrapCrossAlignment.start,
this.textDirection,
this.verticalDirection = VerticalDirection.down,
List<Widget> children = const <Widget>[],
我们可以看到 Wrap 的很多属性在 Flex
中也有,这些属性的意义都是相同的。
额外需要了解的属性:
spacing
主轴方向子 Widget 的间距runSpacing
纵轴方向子 Widget 的间距runAlignment
纵轴方向的对齐方式
5.2 Flow
因为 Flow 较为复杂,一般会很少的场景使用 Flow,需要实现子 Widget 的位置转换,所以它可以做一些动画。它有一下的优点:
- 性能好
Flow 是一个对子组件尺寸及位置调整非常高效的控件。Flow
用转化矩阵在对子组件进行位置调整的时候进行了优化:在Flow
定位过后,如果子组件的尺寸或者位置发生了变化,在FlowDelegate
中的paintChildren()
中进行重绘,重绘时使用了转化矩阵,并没有实际调整组件位置 - 灵活
由于我们需要自己实现FlowDelegate.paintChildren()
,所以我们需要自己计算每一个组件的位置,因此可以自定义布局策略
缺点:
- 使用比较复杂
- Flow 不能自适应子组件大小,必须要通过指定父容器大小或实现
TestFlowDelegate.getSize
返回固定大小
示例:
我们对六个色块进行自定义流式布局:
Flow(
delegate: TestFlowDelegate(margin: EdgeInsets.all(10.0)),
children: <Widget>[
Container(width: 80.0, height:80.0, color: Colors.green,),
Container(width: 80.0, height:80.0, color: Colors.red,),
Container(width: 80.0, height:80.0, color: Colors.yellow,),
Container(width: 80.0, height:80.0, color: Colors.blue,),
Container(width: 80.0, height:80.0, color: Colors.brown,),
Container(width: 80.0, height:80.0, color: Colors.purple,),
],
)
同时继承 FlowDelegate 实现一个 TestFlowDelegate
:
class TestFlowDelegate extends FlowDelegate
EdgeInsets margin;
TestFlowDelegate(this.margin = EdgeInsets.zero);
double width = 0;
double height = 0;
@override
void paintChildren(FlowPaintingContext context)
var x = margin.left;
var y = margin.top;
//计算每一个子widget的位置
for (int i = 0; i < context.childCount; i++)
var w = context.getChildSize(i)!.width + x + margin.right;
if (w < context.size.width)
context.paintChild(i, transform: Matrix4.translationValues(x, y, 0.0));
x = w + margin.left;
else
x = margin.left;
y += context.getChildSize(i)!.height + margin.top + margin.bottom;
//绘制子widget(有优化)
context.paintChild(i, transform: Matrix4.translationValues(x, y, 0.0));
x += context.getChildSize(i)!.width + margin.left + margin.right;
@override
Size getSize(BoxConstraints constraints)
// 指定Flow的大小,简单起见我们让宽度竟可能大,但高度指定为200,
// 实际开发中我们需要根据子元素所占用的具体宽高来设置Flow大小
return Size(double.infinity, 200.0);
@override
bool shouldRepaint(FlowDelegate oldDelegate)
return oldDelegate != this;
效果如下:
6. 层叠布局
层叠布局和 Android 中的 FrameLayout
类似,子组件可以根据父容器的四个角的位置来确定自身位置,且该布局允许子组件按照声明顺序堆叠起来。
Flutter 中使用 Stack
和 Positioned
这两个组件来配合实现绝对定位。Stack
允许子组件堆叠, Positioned
用于根据 Stack
的四个角来确定子组件位置。
6.1 Stack
来看看 Stack 的构造函数:
Stack(
Key? key,
this.alignment = AlignmentDirectional.topStart,
this.textDirection,
this.fit = StackFit.loose,
@Deprecated(
'Use clipBehavior instead. See the migration guide in flutter.dev/go/clip-behavior. '
'This feature was deprecated after v1.22.0-12.0.pre.',
)
this.overflow = Overflow.clip,
this.clipBehavior = Clip.hardEdge,
List<Widget> children = const <Widget>[],
)
alignment
对齐方式。 left、right 为横轴, top、bottom为纵轴textDirection
和 Row、Column 的 textDirection 功能一样fit
此参数用于确定没有定位的子组件如何去适应Stack
的大小。StackFit.loose
表示使用子组件的大小,StackFit.expand
表示拉伸到 Stack 的大小overflow
此属性决定如何显示超出 Stack 显示空间的子组件,Overflow.clip
时,超出部分会被剪裁,该属性被废弃,使用 clipBehavior,两者作用是一样的
6.2 Positiened
来看下构造函数:
const Positioned(
Key? key,
this.left,
this.top,
this.right,
this.bottom,
this.width,
this.height,
required Widget child,
)
left
、top
、right
、bottom
分别代表离 Stack 左上右下的距离width
、height
宽高。 但是水平方向上 left、right、width 只能三选二,因为任意两点就能确定水平的位置, 垂直方向同理
6.3 官方示例
我么能通过对几个 Text
的定位来演示 Stack
和 Positioned
的特性:
ConstrainedBox(
constraints: const BoxConstraints.expand(),
child: Stack(
alignment: Alignment.center,
children: [
Container(
child: const Text(
"hello rikka",
style: TextStyle(color: Colors.white),
),
color: Colors.red,
),
const Positioned(left: 18.0, child: Text("I am Rikka")),
const Positioned(
top: 18.0, child: Text("Your friendly neighborhood"))
],
),
)
效果如下:
7. 对齐与相对定位
如果我们只想简单调整一个子元素在父元素中的位置的话, 可以使用 Align 会更加简单一点
7.1 Align
const Align(
Key? key,
this.alignment = Alignment.center,
this.widthFactor,
this.heightFactor,
Widget? child,
)
alignment
表示子组件在父组件中的其实位置,接收一个AliginmentGemotry
,有两个常用子类:Alignment
和FractionalOffset
widthFactor
、heightFactor
用于确定Align
组件本身宽高属性,实际上会用这两个因子分别乘以 宽、高得出最终 Align 的宽高,如果为null,则宽高将会占用尽可能多的空间
示例, 定义一个靠父组件右上的子组件:
Container(
heightFlutter 学习 容器类Widget
Flutter学习笔记(22)--单个子元素的布局Widget(ContainerPaddingCenterAlignFittedBoxOffstageLimitedBoxOverflo