【Flutter学习】页面布局之列表和表格处理
一,概述
Flutter
中拥有30多种预定义的布局widget
,常用的有Container
、Padding
、Center
、Flex
、Row
、Colum
、ListView
、GridView
。按照《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') ], ); } }
效果图:
源码解析:@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, ), ); }, ), ); } }
效果图
源码解析:
@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'), ], ), ], )
效果图:
(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的尺寸。 如果经常关注系列文章的读者,可能会发现,布局控件的布局流程基本上跟上述流程是相似的。
四,参考
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)