参考
核心 Widget 目录 - Flutter 中文文档 - Flutter 中文资源
布局类组件简介
4.1:布局类组件简介 · 《Flutter实战》 (flutterchina.club)
布局类组件都会包含一个或多个子组件,不同的布局类组件对子组件排版(layout)方式不同。
我们在前面说过Element树才是最终的绘制树,Element树是通过Widget树来创建的(通过Widget.createElement()),Widget其实就是Element的配置数据。
在Flutter中,根据Widget是否需要包含子节点将Widget分为了三类,分别对应三种Element,如下表:
Widget |
对应的Element |
用途 |
LeafRenderObjectWidget |
LeafRenderObjectElement |
Widget树的叶子节点,用于没有子节点的widget,通常基础组件都属于这一类,如Image。 |
SingleChildRenderObjectWidget |
SingleChildRenderObjectElement |
包含一个子Widget,如:ConstrainedBox、DecoratedBox等 |
MultiChildRenderObjectWidget |
MultiChildRenderObjectElement |
包含多个子Widget,一般都有一个children参数,接受一个Widget数组。如Row、Column、Stack等 |
注意,Flutter中的很多Widget是直接继承自StatelessWidget或StatefulWidget,然后在build()方法中构建真正的RenderObjectWidget,如Text,它其实是继承自StatelessWidget,然后在build()方法中通过RichText来构建其子树,而RichText才是继承自MultiChildRenderObjectWidget。所以为了方便叙述,我们也可以直接说Text属于MultiChildRenderObjectWidget(其它widget也可以这么描述),这才是本质。
读到这里我们也会发现,其实StatelessWidget和StatefulWidget就是两个用于组合Widget的基类,它们本身并不关联最终的渲染对象(RenderObjectWidget)。
布局类组件 就是指直接 或 间接继承(包含)MultiChildRenderObjectWidget的Widget,它们一般都会有一个children属性用于接收子Widget。
我们看一下继承关系 Widget > RenderObjectWidget > (Leaf/SingleChild/MultiChild)RenderObjectWidget 。
RenderObjectWidget类中定义了创建、更新RenderObject的方法,子类必须实现他们,关于RenderObject我们现在只需要知道它是最终布局、渲染UI界面的对象即可,也就是说,对于布局类组件来说,其布局算法都是通过对应的RenderObject对象来实现的,所以读者如果对接下来介绍的某个布局类组件的原理感兴趣,可以查看其对应的RenderObject的实现,比如Stack(层叠布局)对应的RenderObject对象就是RenderStack,而层叠布局的实现就在RenderStack中。
布局约束Constraints
深入理解 Flutter 布局约束 - Flutter 中文文档 - Flutter 中文资源
flutter中的布局流程,和Android的view的measure、layout很相似:
首先,上层 widget 向下层 widget 传递Constraints约束条件;
然后,下层 widget 向上层 widget 传递Sizes 大小信息。
最后,上层 widget 决定下层 widget 的position位置。
Constraints
其实就类似于Android中父view调用子view.onMeasure()传递的widthMeasureSpec, heightMeasureSpec。
Constraints类是一个abstract类,它的一个子类是主要使用的布局约束,BoxConstraints,
BoxConstraints有4个double的属性,minWidth,maxWidth,minHeight,maxHeight。
const BoxConstraints({ this.minWidth = 0.0, this.maxWidth = double.infinity, this.minHeight = 0.0, this.maxHeight = double.infinity, })
其中用double.infinity表示不受限制。
前边介绍过RenderObject才是真正进行布局、绘制的地方,它的一个子类RenderBox是在 二维笛卡尔坐标系 上有大小的一个矩形RenderObject。
每个RenderBox都有一个自身的坐标系,就和Android的view一样,左上角是(0,0),右下角是(width,height)。
RenderBox在进行布局时会被父RenderBox传递一个BoxConstraints, BoxConstraints为子RenderBox的宽度和高度建立了一个最小值和最大值。在确定子RenderBox大小时,子RenderBox必须尊重父RenderBox给它的限制。
BoxConstraint术语
min和max必须要满足:
- 0.0 <= minWidth <= maxWidth <= double.infinity
- 0.0 <= minHeight <= maxHeight <= double.infinity
术语:
- 当min和max相等时,表示tight,
- 当min是0时,表示loose,如果max也是0,表示既是tight又是loose。
- 当max不是double.infinite,表示bounded。
- 当max是double.infinite,表示unbounded,如果min也是double.infinite,表示expanding。
- 当min是double.infinite,也表示unbounded,因为max必须要>=min。
Flutter 的布局引擎有一些重要限制:
- 一个 widget 仅在其父级给其约束的情况下才能决定自身的大小。这意味着 widget 通常情况下 不能任意获得其想要的大小。
- 一个 widget 无法知道,也不需要决定其在屏幕中的位置。因为它的位置是由其父级决定的。
- 当轮到父级决定其大小和位置的时候,同样的也取决于它自身的父级。所以,在不考虑整棵树的情况下,几乎不可能精确定义任何 widget 的大小和位置。
- 如果子级想要拥有和父级不同的大小,然而父级没有足够的空间对其进行布局的话,子级的设置的大小可能会不生效。 这时请明确指定它的对齐方式。
严格约束(Tight)和 宽松约束(loose)
严格约束
给你了一种获得确切大小。换句话来说就是,它的最大/最小宽度是一致的,高度也一样。
就是Android中的MeasureSpec.EXACTLY。
如果你到 Flutter 的 box.dart 文件中搜索 BoxConstraints 构造器,你会发现以下内容:
BoxConstraints.tight(Size size) : minWidth = size.width, maxWidth = size.width, minHeight = size.height, maxHeight = size.height;
宽松约束
换句话来说就是设置了最大宽度/高度,但是让允许其子 widget 获得比它更小的任意大小。换句话来说,宽松约束的最小宽度/高度为 0。
就是Android中的wrap_content,或MeasureSpec.AT_MOST。
BoxConstraints.loose(Size size) : minWidth = 0.0, maxWidth = size.width, minHeight = 0.0, maxHeight = size.height;
当maxWidth和maxHeight 是double.infinity时,表示不受限制,就是Android中的MeasureSpec.UNSPECIFIED。
根widget的BoxConstraints
根widget的BoxConstraints 就是屏幕大小,所以会让直接子widget的大小强制为屏幕那么大。
可以跟一下源码,
根widget是RenderObjectToWidgetAdapter,它时继承自RenderObjectWidget,
而根widget的renderObject是RenderView,
renderView在创建时会传递一个当前系统的一个属性,其中就有屏幕大小逻辑像素,
RendererBinding:
void initRenderView() { assert(!_debugIsRenderViewInitialized); assert(() { _debugIsRenderViewInitialized = true; return true; }()); renderView = RenderView(configuration: createViewConfiguration(), window: window); renderView.prepareInitialFrame(); } ViewConfiguration createViewConfiguration() { final double devicePixelRatio = window.devicePixelRatio; return ViewConfiguration( size: window.physicalSize / devicePixelRatio, devicePixelRatio: devicePixelRatio, ); }
renderView在进行布局时会把屏幕大小传递进 子renderObject.layout
RenderView.performLayout:
@override void performLayout() { assert(_rootTransform != null); _size = configuration.size; assert(_size.isFinite); if (child != null) child!.layout(BoxConstraints.tight(_size)); }
BoxConstraints.tight
BoxConstraints.tight(Size size) : minWidth = size.width, maxWidth = size.width, minHeight = size.height, maxHeight = size.height;
关于widget布局时的约束规则
在Android中,view的measure、layout是有一个通用规则,
1. 父view在进行measure、layout时,如果子view是match_parent,一般是会把就把子view填充对应方向,
2. 子view是wrap_content,父view只是给一个最大限制,子view来控制自身大小,
3. 子view设置了具体大小,父view如果空间够就给子view。
但父view可以不遵守此规则,所以这些不是通用规则的view,我们只能去读它的文档 或 看源码。
和Android一样,flutter也类似,所以我们学习flutter的widget时,主要学习一些常用的widget,详细了解其布局规则,而对于其他的widget遇到了再看其文档、源码:
1. 找到那个widget,然后看其createRenderObject()方法,
2. 然后看createRenderObject()返回的RenderObject类的performLayout,
3. 看performLayout中是怎么对子renderObject约束的。
ParentDataWidget
其实就是类似于Android中存储在子view上的layoutParams,父view会用子view的layoutParams的数据来去做一些特定布局、绘制。
父布局类的widget 会用 存储在子widget的RenderObject的ParentData来进行一些处理,比如布局、绘制等操作。
ParentDataWidget就是把子widget包装在内,用于配置子widget的RenderObject的ParentData所需要的数据。 例如,Stack使用Positioned(继承了ParentDataWidget)来定位每个子widget。
继承关系:
class ParentDataWidget<T extends ParentData> extends ProxyWidget
其中T表示 子widget的RenderObject的ParentData的类型。
构造函数:
ParentDataWidget({ Key key, Widget child })
子类需要实现的方法:
void applyParentData(RenderObject renderObject);
当ParentDataWidget被重建时会调用此方法,来更新子widget的RenderObject的ParentData数据。
可能ParentDataWidget重建时数据发生了变化,此时存储在子widget的RenderObject上的ParentData 和 新建的ParentDataWidget上的数据不一致,那么就需要更新子widget的RenderObject上的ParentData,并根据需要调用parent的RenderObject.markNeedsLayout或RenderObject.markNeedsPaint。
ParentData
一些RenderObject希望在其子widget上存储数据,例如子类对父类布局算法的输入参数或子类相对于其他子类的位置。
父布局类的widget 会用 存储在子widget的RenderObject的ParentData来进行一些处理,比如布局、绘制等操作。
ParentData和ParentDataWidget的关系
ParentData是一个子RenderObject的直接父RenderObject使用的 存储在child中的数据,
ParentDataWidget是用来配置子RenderObject的ParentData的,
什么意思呢?
比如Row,它的RenderObject是RenderFlex,它可以处理的ParentData是FlexParentData,那么它的所有子RenderObject的ParentData都是FlexParentData,
@override void setupParentData(RenderBox child) { if (child.parentData is! FlexParentData) child.parentData = FlexParentData(); }
FlexParentData有两个属性,
class FlexParentData extends ContainerBoxParentData<RenderBox> { int? flex; FlexFit? fit; }
这两个属性是用来控制子widget在Row中占用的空间,
我们想要配置这两个属性就需要一个中间ParentDataWidget -> Flexible/Expanded:
const Flexible({ Key key, this.flex = 1, this.fit = FlexFit.loose, @required Widget child, })
总而言之,ParentDataWidget并不是任何一个widget都能使用的,必须要是匹配的ParentData才行,比如Row和Flexible匹配。
LinearLayout
在 Android 中,LinearLayout 用于线性布局 widget 的—水平或者垂直。
在 Flutter 中,使用 Row 或者 Column Widget 来实现相同的效果。
主轴和纵轴
对于线性布局,有主轴和纵轴之分,如果布局是沿水平方向,那么主轴就是指水平方向,
而纵轴即垂直方向;如果布局沿垂直方向,那么主轴就是指垂直方向,而纵轴就是水平方向。
在线性布局中,有两个定义对齐方式的枚举类MainAxisAlignment和CrossAxisAlignment,分别代表主轴对齐和纵轴对齐。
Row
class Row extends Flex Row({ ... TextDirection textDirection, MainAxisSize mainAxisSize = MainAxisSize.max, MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start, VerticalDirection verticalDirection = VerticalDirection.down, CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center, List<Widget> children = const <Widget>[], })
- textDirection:表示水平方向子组件的布局顺序(是从左往右还是从右往左),默认为系统当前Locale环境的文本方向(如中文、英语都是从左往右,而阿拉伯语是从右往左)。
- mainAxisSize:
enum MainAxisSize { min, max, }
表示Row在主轴(水平)方向占用的空间,默认是MainAxisSize.max,
-
- MainAxisSize.max表示尽可能多的占用水平方向的空间,此时无论子widgets实际占用多少水平空间,Row的宽度始终等于水平方向的最大宽度,
如果constraints.maxWidth为double.infinite,那么MainAxisSize.max无效,此时等同于MainAxisSize.min。
-
- MainAxisSize.min表示尽可能少的占用水平空间,当子组件没有占满水平剩余空间,则Row的实际宽度等于所有子组件占用的的水平空间;
- mainAxisAlignment:
enum MainAxisAlignment { start, end, center, /// Place the free space evenly between the children. spaceBetween, /// Place the free space evenly between the children as well as half of that /// space before and after the first and last child. spaceAround, /// Place the free space evenly between the children as well as before and /// after the first and last child. spaceEvenly, }
表示子组件在Row所占用的水平空间内对齐方式,
如果mainAxisSize值为MainAxisSize.min,则此属性无意义,因为子组件的宽度等于Row的宽度。只有当mainAxisSize的值为MainAxisSize.max时,此属性才有意义,
-
- MainAxisAlignment.start表示沿textDirection的初始方向对齐,如textDirection取值为TextDirection.ltr时,则MainAxisAlignment.start表示左对齐,textDirection取值为TextDirection.rtl时表示从右对齐。
- 而MainAxisAlignment.end和MainAxisAlignment.start正好相反;
- MainAxisAlignment.center表示居中对齐。
读者可以这么理解:textDirection是mainAxisAlignment的参考系。
- verticalDirection:
和textDirection类似,只不过verticalDirection是表示垂直方向,
决定children垂直摆放的顺序。
以及在垂直方向上,start指的是上边,还是下边。
默认是down。
- crossAxisAlignment:
enum CrossAxisAlignment { start, end, center, /// Require the children to fill the cross axis. /// /// This causes the constraints passed to the children to be tight in the /// cross axis. stretch, /// Place the children along the cross axis such that their baselines match. /// /// Because baselines are always horizontal, this alignment is intended for /// horizontal main axes. If the main axis is vertical, then this value is /// treated like [start]. /// /// For horizontal main axes, if the minimum height constraint passed to the /// flex layout exceeds the intrinsic height of the cross axis, children will /// be aligned as close to the top as they can be while honoring the baseline /// alignment. In other words, the extra space will be below all the children. /// /// Children who report no baseline will be top-aligned. baseline, }
表示子组件在纵轴方向的对齐方式,
Row的高度等于子组件中最高的子元素高度,它的取值和MainAxisAlignment一样(包含start、end、 center三个值),
不同的是crossAxisAlignment的参考系是verticalDirection,
-
- 即verticalDirection值为VerticalDirection.down时crossAxisAlignment.start指顶部对齐,
- verticalDirection值为VerticalDirection.up时,crossAxisAlignment.start指底部对齐;
- 而crossAxisAlignment.end和crossAxisAlignment.start正好相反;
children :子组件数组。
Layout algorithm
Row的布局算法分如下步骤:
1. 布局每个没有个设置flex的child时,会用width不限制(0—double.infinite),height为(0 — 传入Row的constraints.maxHeight) 的BoxConstraints来布局child,
如果Row的CrossAxisAlignment是CrossAxisAlignment.stretch,则会用width不限制,height的min和max都为传入Row的maxHeight(也就是tight)的BoxConstraints来布局child。
2. 接着对于设置了flex的child,把剩余水平空间按比例分配给它们,当然还会受到FlexFit的影响,具体看Flexible。
child的高度和第一步一样。
3. 最后开始计算Row的高度和宽度,
4. 对于Row高度,是所有child中最高的那个高度,
5. 对于Row的宽度,是由mainAxisSize 决定的,
- 如果是MainAxisSize.max,则宽度为传入Row的constraints.maxWidth,
但如果constraints.maxWidth为double.infinite,那么MainAxisSize.max无效,此时等同于MainAxisSize.min。
- 如果是MainAxisSize.min,宽度为所有child的宽度之和(肯定要<=传入Row的constraints.maxWidth)。
6. Determine the position for each child according to the mainAxisAlignment and the crossAxisAlignment. For example, if the mainAxisAlignment is MainAxisAlignment.spaceBetween, any horizontaspace that has not been allocated to children is divided evenly and placed between the children.
Column
class Column extends Flex Column({ Key key, 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>[], })
参数和Row一样,不同的是布局方向为垂直,主轴纵轴正好相反。
Flexible
一般就用此类来控制子widget的大小。
class Flexible extends ParentDataWidget<FlexParentData>
const Flexible({ Key key, this.flex = 1, this.fit = FlexFit.loose, @required Widget child, })
- int flex
其实就是类似于LinearLayout.LayoutParams的weight,
如果是null或0,child就是一个非弹性的,大小由child自己决定。
如果是非0,在其他的非弹性的兄弟widget布局后,再根据所有的 弹性兄弟widget的flex所占比例来分配剩余空间。
- FlexFit fit
描述弹性子widget怎么使用分配的可用空间的,其实就是类似于wrap_content和match_parent,
enum FlexFit { tight, loose, }
tight就相当于Android的match_parent,就会占满根据flex分配的大小。
loose就相当于Android的wrap_content,就会以(0 — 根据flex分配的大小) 来布局,也就是说child的大小是<=分配的大小的。
Expanded
Flexible的一个子类Expanded,用来便捷的创建一个占据所有分配空间的Flexible。
class Expanded extends Flexible { /// Creates a widget that expands a child of a [Row], [Column], or [Flex] /// so that the child fills the available space along the flex widget's /// main axis. const Expanded({ Key key, int flex = 1, @required Widget child, }) : super(key: key, flex: flex, fit: FlexFit.tight, child: child); }