多子 Widget 布局:Row、Column 与 Expanded

281 阅读6分钟

对于拥有多个子 Widget 的布局类容器而言,其布局行为无非就是两种规则的抽象:水平方向上应该如何布局、垂直方向上应该如何布局。

如同 Android 的 LinearLayout、前端的 Flex 布局一样,Flutter 中也有类似的概念,即将子 Widget 按行水平排列的 Row,按列垂直排列的 Column,以及负责分配这些子 Widget 在布局方向(行 / 列)中剩余空间的 Expanded。

Row 与 Column 的使用方法很简单,我们只需要将各个子 Widget 按序加入到 children 数组即可。在下面的代码中,我们把 4 个分别设置了不同的颜色和宽高的 Container 加到 Row 与 Column 中:

//Row的用法示范
Row(
  children: <Widget>[
    Container(color: Colors.yellow, width: 60, height: 80,),
    Container(color: Colors.red, width: 100, height: 180,),
    Container(color: Colors.black, width: 60, height: 80,),
    Container(color: Colors.green, width: 60, height: 80,),
  ],
);

//Column的用法示范
Column(
  children: <Widget>[
    Container(color: Colors.yellow, width: 60, height: 80,),
    Container(color: Colors.red, width: 100, height: 180,),
    Container(color: Colors.black, width: 60, height: 80,),
    Container(color: Colors.green, width: 60, height: 80,),
  ],
);

Row示例:

row.PNG

可以看到,单纯使用 Row 和 Column 控件,在子 Widget 的尺寸较小时,无法将容器填满,视觉样式比较难看。对于这样的场景,我们可以通过 Expanded 控件,来制定分配规则填满容器的剩余空间。

比如,我们希望 Row 组件(或 Column 组件)中的绿色容器与黄色容器均分剩下的空间,于是就可以设置它们的弹性系数参数 flex 都为 1,这两个 Expanded 会按照其 flex 的比例(即 1:1)来分割剩余的 Row 横向(Column 纵向)空间:

Row(
  children: <Widget>[
    Expanded(flex: 1, child: Container(color: Colors.yellow, height: 60)), //设置了flex=1,因此宽度由Expanded来分配
    Container(color: Colors.red, width: 100, height: 180,),
    Container(color: Colors.black, width: 60, height: 80,),
    Expanded(flex: 1, child: Container(color: Colors.green,height: 60),)/设置了flex=1,因此宽度由Expanded来分配
  ],
);

expanded.PNG

于 Row 与 Column 而言,Flutter 提供了依据坐标轴的布局对齐行为,即根据布局方向划分出主轴和纵轴:主轴,表示容器依次摆放子 Widget 的方向;纵轴,则是与主轴垂直的另一个方向。

cross.PNG

我们可以根据主轴与纵轴,设置子 Widget 在这两个方向上的对齐规则 mainAxisAlignment 与 crossAxisAlignment。比如,主轴方向 start 表示靠左对齐、center 表示横向居中对齐、end 表示靠右对齐、spaceEvenly 表示按固定间距对齐;而纵轴方向 start 则表示靠上对齐、center 表示纵向居中对齐、end 表示靠下对齐。

主轴:mainAxisAlignment ; 纵轴:crossAxisAlignment

对于Row控件:

主轴对齐方式:

main.PNG

纵轴对齐方式:

crossAXis.PNG

这里需要注意的是,对于主轴而言,Flutter 默认是让父容器决定其长度,即尽可能大,类似 Android 中的 match_parent。

在上面的例子中,Row 的宽度为屏幕宽度,Column 的高度为屏幕高度。主轴长度大于所有子 Widget 的总长度,意味着容器在主轴方向的空间比子 Widget 要大,这也是我们能通过主轴对齐方式设置子 Widget 布局效果的原因。

示例:

Column(
  //测试Row对齐方式,排除Column默认居中对齐的干扰
  crossAxisAlignment: CrossAxisAlignment.start,
  children: <Widget>[
    Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Text(" hello world "),
        Text(" I am Jack "),
      ],
    ),
    Row(
      mainAxisSize: MainAxisSize.min,
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Text(" hello world "),
        Text(" I am Jack "),
      ],
    ),
    Row(
      mainAxisAlignment: MainAxisAlignment.end,
      textDirection: TextDirection.rtl,
      children: <Widget>[
        Text(" hello world "),
        Text(" I am Jack "),
      ],
    ),
    Row(
      crossAxisAlignment: CrossAxisAlignment.start,  
      verticalDirection: VerticalDirection.up,
      children: <Widget>[
        Text(" hello world ", style: TextStyle(fontSize: 30.0),),
        Text(" I am Jack "),
      ],
    ),
  ],
);

实际效果:

column.png

解释:第一个Row很简单,默认为居中对齐;第二个Row,由于mainAxisSize值为MainAxisSize.min,Row的宽度等于两个Text的宽度和,所以对齐是无意义的,所以会从左往右显示;第三个Row设置textDirection值为TextDirection.rtl,所以子组件会从右向左的顺序排列,而此时MainAxisAlignment.end表示左对齐,所以最终显示结果就是图中第三行的样子;第四个 Row 测试的是纵轴的对齐方式,由于两个子 Text 字体不一样,所以其高度也不同,我们指定了verticalDirection值为VerticalDirection.up,即从低向顶排列,而此时crossAxisAlignment值为CrossAxisAlignment.start表示底对齐。

===============================================================

Column

Column可以在垂直方向排列其子组件。参数和Row一样,不同的是布局方向为垂直,主轴纵轴正好相反,读者可类比Row来理解,下面看一个例子: Column的主轴是垂直方向的,纵轴是水平方向的。

import 'package:flutter/material.dart';

class CenterColumnRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.center,
      children: <Widget>[
        Text("hi"),
        Text("world"),
      ],
    );
  }
}

效果图:

column.PNG

由于我们没有指定Column的mainAxisSize,所以使用默认值MainAxisSize.max,则Column会在垂直方向占用尽可能多的空间,此例中会占满整个屏幕高度。

由于我们指定了 crossAxisAlignment 属性为CrossAxisAlignment.center,那么子项在Column纵轴方向(此时为水平方向)会居中对齐。注意,在水平方向对齐是有边界的,总宽度为Column占用空间的实际宽度,而实际的宽度取决于子项中宽度最大的Widget。在本例中,Column有两个子Widget,而显示“world”的Text宽度最大,所以Column的实际宽度则为Text("world") 的宽度,所以居中对齐后Text("hi")会显示在Text("world")的中间部分。

实际上,Row和Column都只会在主轴方向占用尽可能大的空间而纵轴的长度则取决于他们最大子元素的长度。如果我们想让本例中的两个文本控件在整个手机屏幕中间对齐,我们有两种方法:

将Column的宽度指定为屏幕宽度;这很简单,我们可以通过ConstrainedBox或SizedBox来强制更改宽度限制,例如:

ConstrainedBox(
  constraints: BoxConstraints(minWidth: double.infinity), 
  child: Column(
    crossAxisAlignment: CrossAxisAlignment.center,
    children: <Widget>[
      Text("hi"),
      Text("world"),
    ],
  ),
);

将minWidth设为double.infinity,可以使宽度占用尽可能多的空间。

使用Center组件:

import 'package:flutter/cupertino.dart';

class ColumnPageV2 extends StatelessWidget{

  const ColumnPageV2({super.key});

  @override
  Widget build(BuildContext context) {
    return const Center(
      child:
      Column(
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
          Text("hi"),
          Text("world0009")
        ],
      ),
    );
  }
}

=========================================================

如果Row里面嵌套Row,或者Column里面再嵌套Column,那么只有最外面的Row或Column会占用尽可能大的空间,里面Row或Column所占用的空间为实际大小,下面以Column为例说明:

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

class ColumnPageV3 extends StatelessWidget {

  const ColumnPageV3({super.key});

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.green,
      child: Padding(
        padding: EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,//靠左对齐,纵轴(水平方向)宽度为子widget中的最大宽度
          mainAxisSize: MainAxisSize.max,//有效,外层Colum高度为整个屏幕,默认设置
          children: [
            Container(
                color: Colors.red,
                child:const Column(
                  mainAxisSize: MainAxisSize.max, //无效,内层Colum高度为实际高度
                  children: <Widget>[
                    Text("hello world "),
                    Text("I am Jack "),
                  ],
                ),
            )
          ],
        ),
      ),
    );
  }
}

columnv2.PNG

如果要让里面的Column占满外部Column,可以使用Expanded 组件:

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

class ColumnPageV3 extends StatelessWidget {

  const ColumnPageV3({super.key});

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.green,
      child: Padding(
        padding: EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,//靠左对齐,纵轴(水平方向)宽度为子widget中的最大宽度
          mainAxisSize: MainAxisSize.max,//有效,外层Colum高度为整个屏幕,默认设置
          children: [
            // Container(
            //     color: Colors.red,
            //     child:const Column(
            //       mainAxisSize: MainAxisSize.max, //无效,内层Colum高度为实际高度
            //       children: <Widget>[
            //         Text("hello world "),
            //         Text("I am Jack "),
            //       ],
            //     ),
            // )
            Expanded(
              child: Container(
                color: Colors.red,
                child: const Column(
                  mainAxisAlignment: MainAxisAlignment.center, //垂直方向居中对齐
                  children: <Widget>[
                    Text("hello world "),
                    Text("I am Jack "),
                  ],
                ),
              ),
            )
          ],
        ),
      ),
    );
  }
}