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)按需加载列表布局

两者的实现上有差异,但是大体流程相同:

  1. 上层组件向下层组件传递约束(constraints)条件
  2. 下层组件确定自己大小,然后通知上层组件
  3. 上层组件确定下层组件相对自身的偏移和确定自身的大小(大多数情况下会根据子组件的大小来确定自身的大小)

本节主要来学习 盒模型布局,在之后的可滚动组件里学习 Sliver 模型, 盒模型布局组件有两个特点:

  1. 组件对应的渲染对象都继承 RenderBox,如果某个组件是 RenderBox,则指它是盒模型布局
  2. 在布局过程中父级传递给子级的约束信息由 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 有多重限制的时候, 对于 minWidthminHeight 来说,是取多重限制中较大的的那个

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 中使用 RowColumn 来实现线性布局,它们都继承自 FlexFlex 是弹性布局。

3.1 主轴和纵轴

  • 如果当前布局是沿水平方向,那么主轴就是水平方向, 而纵轴就是垂直方向
  • 如果当前布局是沿垂直方向,那么主轴就是垂直方向,纵轴就是水平方向

在线性布局中,有两个定义对齐方式的枚举类 MainAxisAlignmentCrossAxisAlignment,分别代表主轴对齐和纵轴对齐

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 ,那么只有最外面的 RowColumn 会占用尽可能大的空间,里面 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中主要通过 FlexExpanded 来实现

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中通过 WrapFlow 来支持流式布局,将上例中的 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 中使用 StackPositioned 这两个组件来配合实现绝对定位。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,
  )
  • lefttoprightbottom
    分别代表离 Stack 左上右下的距离
  • widthheight
    宽高。 但是水平方向上 left、right、width 只能三选二,因为任意两点就能确定水平的位置, 垂直方向同理

6.3 官方示例

我么能通过对几个 Text 的定位来演示 StackPositioned 的特性:

    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,有两个常用子类: AlignmentFractionalOffset
  • widthFactorheightFactor
    用于确定 Align 组件本身宽高属性,实际上会用这两个因子分别乘以 宽、高得出最终 Align 的宽高,如果为null,则宽高将会占用尽可能多的空间

示例, 定义一个靠父组件右上的子组件:

    Container(
        heightFlutter 学习 容器类Widget

Flutter学习-单子布局Widget

Flutter学习笔记(22)--单个子元素的布局Widget(ContainerPaddingCenterAlignFittedBoxOffstageLimitedBoxOverflo

Flutter核心类分析深入理解RenderObject

Flutter核心类分析深入理解RenderObject

Flutter核心类分析深入理解RenderObject