一,概述

  Flutter中拥有30多种预定义的布局widget,常用的有ContainerPaddingCenterFlexRowColumListViewGridView。按照《Flutter技术入门与实战》上面来说的话,大概分为四类

  • 基础布局组件Container(容器布局),Center(居中布局),Padding(填充布局),Align(对齐布局),Colum(垂直布局),Row(水平布局),Expanded(配合Colum,Row使用),FittedBox(缩放布局),Stack(堆叠布局),overflowBox(溢出父视图容器)。
  • 宽高尺寸处理SizedBox(设置具体尺寸),ConstrainedBox(限定最大最小宽高布局),LimitedBox(限定最大宽高布局),AspectRatio(调整宽高比),FractionallySizedBox(百分比布局)
  • 列表和表格处理ListView(列表),GridView(网格),Table(表格)
  • 其它布局处理:Transform(矩阵转换),Baseline(基准线布局),Offstage(控制是否显示组件),Wrap(按宽高自动换行布局)

二,列表和表格处理布局组件

  • ListView(列表)  
  • 介绍
    ListView是一个非常常用的控件,涉及到数据列表展示的,一般情况下都会选用该控件。ListView跟GridView相似,基本上是一个slivers里面只包含一个SliverList的CustomScrollView。
  • 布局行为
    ListView在主轴方向可以滚动,在交叉轴方向,则是填满ListView。
  • 继承关系
Object > Diagnosticable > DiagnosticableTree > Widget > StatelessWidget > ScrollView > BoxScrollView > ListView

看继承关系可知,这是一个组合控件。ListView跟GridView类似,都是继承自BoxScrollView。

  • 构造函数
ListView({
  Key key,
  Axis scrollDirection = Axis.vertical,
  bool reverse = false,
  ScrollController controller,
  bool primary,
  ScrollPhysics physics,
  bool shrinkWrap = false,
  EdgeInsetsGeometry padding,
  this.itemExtent,
  bool addAutomaticKeepAlives = true,
  bool addRepaintBoundaries = true,
  double cacheExtent,
  List<Widget> children = const <Widget>[],
})

同时也提供了如下额外的三种构造方法,方便开发者使用。

ListView.builder
ListView.separated
ListView.custom
  • 使用场景
    ListView使用场景太多了,一般涉及到列表展示的,一般都会选择ListView。
    但是需要注意一点,ListView的标准构造函数适用于数目比较少的场景,如果数目比较多的话,最好使用ListView.builder
    ListView的标准构造函数会将所有item一次性创建,而ListView.builder会创建滚动到屏幕上显示的item。
  • 参数解析ListView大部分属性同GridView,想了解的读者可以看一下下面所写的GridView相关的内容。这里只介绍一个属性
    itemExtent:ListView在滚动方向上每个item所占的高度值。
  • GridView(网格)
  • 介绍
GridView在移动端上非常的常见,就是一个滚动的多列列表,实际的使用场景也非常的多。
  • 布局行为
GridView的布局行为不复杂,本身是尽量占满空间区域,布局行为上完全继承自ScrollView。
  • 继承关系
Object > Diagnosticable > DiagnosticableTree > Widget > StatelessWidget > ScrollView > BoxScrollView > GridView
  • 构造函数
GridView({
  Key key,
  Axis scrollDirection = Axis.vertical,
  bool reverse = false,
  ScrollController controller,
  bool primary,
  ScrollPhysics physics,
  bool shrinkWrap = false,
  EdgeInsetsGeometry padding,
  @required this.gridDelegate,
  bool addAutomaticKeepAlives = true,
  bool addRepaintBoundaries = true,
  double cacheExtent,
  List<Widget> children = const <Widget>[],
})

同时也提供了如下额外的四种构造方法,方便开发者使用。

GridView.builder
GridView.custom
GridView.count
GridView.extent
  • 参数解析scrollDirection:滚动的方向,有垂直和水平两种,默认为垂直方向(Axis.vertical)。
    reverse:默认是从上或者左向下或者右滚动的,这个属性控制是否反向,默认值为false,不反向滚动。
    controller:控制child滚动时候的位置。
    primary:是否是与父节点的PrimaryScrollController所关联的主滚动视图。
    physics:滚动的视图如何响应用户的输入。
    shrinkWrap:滚动方向的滚动视图内容是否应该由正在查看的内容所决定。
    padding:四周的空白区域。
    gridDelegate:控制GridView中子节点布局的delegate。
    cacheExtent:缓存区域。
  • Table(表格)
  • 介绍
每一种移动端布局中都会有一种table布局,这种控件太常见了。至于其表现形式,完全可以借鉴其他移动端的,通俗点讲,就是表格。
  • 布局行为表格的每一行的高度,由其内容决定,每一列的宽度,则由columnWidths属性单独控制。 
  • 继承关系
Object > Diagnosticable > DiagnosticableTree > Widget > RenderObjectWidget > Table
  • 构造函数
Table({
  Key key,
  this.children = const <TableRow>[],
  this.columnWidths,
  this.defaultColumnWidth = const FlexColumnWidth(1.0),
  this.textDirection,
  this.border,
  this.defaultVerticalAlignment = TableCellVerticalAlignment.top,
  this.textBaseline,
})
  • 参数解析columnWidths:设置每一列的宽度。
    defaultColumnWidth:默认的每一列宽度值,默认情况下均分。
    textDirection:文字方向,一般无需考虑。
    border:表格边框。
    defaultVerticalAlignment:每一个cell的垂直方向的alignment。
    总共包含5种:
  • top:被放置在的顶部;
  • middle:垂直居中;
  • bottom:放置在底部;
  • baseline:文本baseline对齐;
  • fill:充满整个cell。

textBaseline:defaultVerticalAlignment为baseline的时候,会用到这个属性。

三,常用示例

  • ListView(列表)
/**
 * ListView
 * 第一个展示四行文字
 */
class MyListView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return new ListView(
      shrinkWrap: true,
      padding: EdgeInsets.all(20.0),
      children: <Widget>[
        new Text('I\m dedicationg every day to you'),
        new Text('Domestic life was never quite my style'),
        new Text('When you smile, you knock me out, I fall apart'),
        new Text('And I thought I was so smart')
      ],
    );
  }
}

效果图:

flutter如何组织一个典型的多页面架构 flutter常用页面布局_构造函数


源码解析:

@override
Widget buildChildLayout(BuildContext context) {
if (itemExtent != null) {
return new SliverFixedExtentList(
delegate: childrenDelegate,
itemExtent: itemExtent,
);
}
return new SliverList(delegate: childrenDelegate);
}

ListView标准构造布局代码如上所示,底层是用到的SliverList去实现的。ListView是一个slivers里面只包含一个SliverList的CustomScrollView。源码这块儿可以参考GridView,在此不做更多的说明。

  • GridView(网格)
/**
 * GridView
 * 代码直接用了Creating a Grid List中的例子,创建了一个2列总共100个子节点的列表。
 */
class  MyGridView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return new GridView.count(
      crossAxisCount: 2,
      children: List.generate(100, (index){
         return new Center(
           child: new Text(
             'Item $index',
             style: Theme.of(context).textTheme.headline,
           ),
         );
      },
      ),
    );
  } 
}

效果图


flutter如何组织一个典型的多页面架构 flutter常用页面布局_List_02

源码解析:

@override
Widget build(BuildContext context) {
 final List<Widget> slivers = buildSlivers(context);
 final AxisDirection axisDirection = getDirection(context);

 final ScrollController scrollController = primary
 ? PrimaryScrollController.of(context)
 : controller;
 final Scrollable scrollable = new Scrollable(
 axisDirection: axisDirection,
 controller: scrollController,
 physics: physics,
 viewportBuilder: (BuildContext context, ViewportOffset offset) {
return buildViewport(context, offset, axisDirection, slivers);
 },
);
return primary && scrollController != null
? new PrimaryScrollController.none(child: scrollable)
: scrollable;
}

上面这段代码是ScrollView的build方法,GridView就是一个特殊的ScrollView。GridView本身代码没有什么,基本上都是ScrollView上的东西,主要会涉及到Scrollable、Sliver、Viewport等内容,这些内容比较多,因此源码就先略了,后面单独出一篇文章对ScrollView进行分析吧。

  • Table(表格)
Table(
  columnWidths: const <int, TableColumnWidth>{
    0: FixedColumnWidth(50.0),
    1: FixedColumnWidth(100.0),
    2: FixedColumnWidth(50.0),
    3: FixedColumnWidth(100.0),
  },
  border: TableBorder.all(color: Colors.red, width: 1.0, style: BorderStyle.solid),
  children: const <TableRow>[
    TableRow(
      children: <Widget>[
        Text('A1'),
        Text('B1'),
        Text('C1'),
        Text('D1'),
      ],
    ),
    TableRow(
      children: <Widget>[
        Text('A2'),
        Text('B2'),
        Text('C2'),
        Text('D2'),
      ],
    ),
    TableRow(
      children: <Widget>[
        Text('A3'),
        Text('B3'),
        Text('C3'),
        Text('D3'),
      ],
    ),
  ],
)

效果图:

flutter如何组织一个典型的多页面架构 flutter常用页面布局_Text_03



(1)样例其实并不复杂,FlowDelegate需要自己实现child的绘制,其实大多数时候就是位置的摆放。上面例子中,对每个child按照给定的margin值,进行排列,如果超出一行,则在下一行进行布局。

(2)另外,对这个例子多做一个说明,对于上述child宽度的变化,这个例子是没问题的,如果每个child的高度不同,则需要对代码进行调整,具体的调整是换行的时候,需要根据上一行的最大高度来确定下一行的起始y坐标。


源码解析:

我们直接来看其布局源码:

第一步,当行或者列为0的时候,将自身尺寸设为0x0。

if (rows * columns == 0) {
size = constraints.constrain(const Size(0.0, 0.0));
return;
}

第二步,根据textDirection值,设置方向,一般在阿拉伯语系中,一些文本都是从右往左现实的,平时使用时,不需要去考虑这个属性。

switch (textDirection) {
 case TextDirection.rtl:
       positions[columns - 1] = 0.0;
       for (int x = columns - 2; x >= 0; x -= 1)
          positions[x] = positions[x+1] + widths[x+1];
          _columnLefts = positions.reversed;
          tableWidth = positions.first + widths.first;
      break;
case TextDirection.ltr:
          positions[0] = 0.0;
       for (int x = 1; x < columns; x += 1)
          positions[x] = positions[x-1] + widths[x-1];
          _columnLefts = positions;
          tableWidth = positions.last + widths.last;
          break;
       }

第三步,设置每一个cell的尺寸。

for (int x = 0; x < columns; x += 1) {
  final int xy = x + y * columns;
  final RenderBox child = _children[xy];
if (child != null) {
  final TableCellParentData childParentData = child.parentData;
  childParentData.x = x;
  childParentData.y = y;
 switch (childParentData.verticalAlignment ?? defaultVerticalAlignment) {
    case TableCellVerticalAlignment.baseline:
         child.layout(new BoxConstraints.tightFor(width: widths[x]), parentUsesSize: true);
         final double childBaseline = child.getDistanceToBaseline(textBaseline, onlyReal: true);
         if (childBaseline != null) {
           beforeBaselineDistance = math.max(beforeBaselineDistance, childBaseline);
           afterBaselineDistance = math.max(afterBaselineDistance, child.size.height - childBaseline);
           baselines[x] = childBaseline;
           haveBaseline = true;
         } else {
           rowHeight = math.max(rowHeight, child.size.height);
           childParentData.offset = new Offset(positions[x], rowTop);
 }
     break;
case TableCellVerticalAlignment.top:
case TableCellVerticalAlignment.middle:
case TableCellVerticalAlignment.bottom:
     child.layout(new BoxConstraints.tightFor(width: widths[x]), parentUsesSize: true);
     rowHeight = math.max(rowHeight, child.size.height);
break;
case TableCellVerticalAlignment.fill:
break;
}
}
}

第四步,如果有baseline则进行相关设置。

if (haveBaseline) {
  if (y == 0) _baselineDistance = beforeBaselineDistance;
  rowHeight = math.max(rowHeight, beforeBaselineDistance + afterBaselineDistance);
}

第五步,根据alignment,调整child的位置。

for (int x = 0; x < columns; x += 1) {
  final int xy = x + y * columns;
  final RenderBox child = _children[xy];
 if (child != null) {
  final TableCellParentData childParentData = child.parentData;
 switch (childParentData.verticalAlignment ?? defaultVerticalAlignment) {
   case TableCellVerticalAlignment.baseline:
     if (baselines[x] != null)
       childParentData.offset = new Offset(positions[x], rowTop + beforeBaselineDistance - baselines[x]);
     break;
   case TableCellVerticalAlignment.top:
       childParentData.offset = new Offset(positions[x], rowTop);
     break;
   case TableCellVerticalAlignment.middle:
       childParentData.offset = new Offset(positions[x], rowTop + (rowHeight - child.size.height) / 2.0);
     break;
   case TableCellVerticalAlignment.bottom:
       childParentData.offset = new Offset(positions[x], rowTop + rowHeight - child.size.height); 
     break;
   case TableCellVerticalAlignment.fill:
       child.layout(new BoxConstraints.tightFor(width: widths[x], height: rowHeight));
       childParentData.offset = new Offset(positions[x], rowTop);
     break;
  }
 }
}

最后一步,则是根据每一行的宽度以及每一列的高度,设置Table的尺寸。

size = constraints.constrain(new Size(tableWidth, rowTop));

最后梳理一下整个的布局流程:

当行或者列为0的时候,将自身尺寸设为0x0;
根据textDirection进行相关设置;
设置cell的尺寸;
如果设置了baseline,则进行相关设置;
根据alignment设置cell垂直方向的位置;
设置Table的尺寸。
如果经常关注系列文章的读者,可能会发现,布局控件的布局流程基本上跟上述流程是相似的。

四,参考  

《Flutter学习之认知基础组件》
Flutter布局