布局构建

2,159 阅读7分钟

欢迎点赞,转载请注明出处

本节示例项目源代码下载点击这里 layout_demos

前面几节里,我们已经接触了不少跟Flutter Widget相关的知识点,本节参考官方示例将继续展开一些讨论。如果你有Web CSS3,Android Activity/Frangment Layout,iOS Storyboard, Xib等UI布局经验,你将很容易理解Flutter的布局方式。

Widgets Tree

Flutter 布局的核心机制是Widgets。可以通过组合Widgets来构建更复杂的Widgets来创建布局,如下图一个UI片段:

对应的Widgets Tree如下:

横向或纵向布局

  • Row 和 Column 是两种最常用的布局模式,下面两张图可以很直观地看到这种布局的特点:

对齐Widgets

可以使用mainAxisAlignment和crossAxisAlignment属性控制行或列如何对齐其子项。

一个图像列表对齐的布局效果如下,红框框出的部分是需要注意的:
pubspec.yaml是项目构建配置部分,当它内容发生改变时,需要先获取对应的包依赖,点击Get dependenciesPackages get均可。
将debugPaintSizeEnabled设置为true以后,可以看到运行程序的的可视布局。
引用本地图片,除了在项目本地增加图片文件外(如images目录下),同时还需要在pubspec.yaml的flutter下增加assets的文件配置。
Column的mainAxisAlignment属性设置为MainAxisAlignment.spaceEvenly,则图片列表主轴均匀分布对齐。

嵌套行和列

参考官方pavlova示例运行效果如下:

  • 本例的布局适用于平板横屏模式,可以选择创建一个安卓的Tablet模拟器:
    或者更改苹果模拟器应用Simulator,选择Hardware > Device 菜单选择iPad类设备,选择Hardware > Rotate 菜单将其方向更改为横向模式。
  • 为了最大限度地减少高度嵌套的布局代码可能导致的视觉混乱,以及代码复用,可以在变量和函数中实现UI 的各个部分,如第158行的mainImage函数;
  • iPad运行图最右侧的Flutter自动标记的黄黑条纹区域表示界面溢出(overflow)了,具体控制台错误如下:
Launching lib/main.dart on iPad Pro (9.7-inch) in debug mode...
Running Xcode build...
Xcode build done.                                           42.7s
flutter: ══╡ EXCEPTION CAUGHT BY RENDERING LIBRARY ╞═════════════════════════════════════════════════════════
flutter: The following assertion was thrown during layout:
flutter: A RenderFlex overflowed by 312 pixels on the right.
flutter:
flutter: User-created ancestor of the error-causing widget was:
flutter:   Card file:///Users/dreamtouch/zqy/projects/pavlova/lib/main.dart:150:18
flutter:
flutter: The overflowing RenderFlex has an orientation of Axis.horizontal.
flutter: The edge of the RenderFlex that is overflowing has been marked in the rendering with a yellow and
flutter: black striped pattern. This is usually caused by the contents being too big for the RenderFlex.
flutter: Consider applying a flex factor (e.g. using an Expanded widget) to force the children of the
flutter: RenderFlex to fit within the available space instead of being sized to their natural size.
flutter: This is considered an error condition because it indicates that there is content that cannot be
flutter: seen. If the content is legitimately bigger than the available space, consider clipping it with a
flutter: ClipRect widget before putting it in the flex, or using a scrollable container rather than a Flex,
flutter: like a ListView.
flutter: The specific RenderFlex in question is: RenderFlex#af837 relayoutBoundary=up10 OVERFLOWING:
flutter:   creator: Row ← Semantics ← DefaultTextStyle ← AnimatedDefaultTextStyle ←
flutter:     _InkFeatures-[GlobalKey#36077 ink renderer] ← NotificationListener<LayoutChangedNotification> ←
flutter:     CustomPaint ← _ShapeBorderPaint ← PhysicalShape ← _MaterialInterior ← Material ← Padding ← ⋯
flutter:   parentData: <none> (can use size)
flutter:   constraints: BoxConstraints(0.0<=w<=1016.0, h=592.0)
flutter:   size: Size(1016.0, 592.0)
flutter:   direction: horizontal
flutter:   mainAxisAlignment: start
flutter:   mainAxisSize: max
flutter:   crossAxisAlignment: start
flutter:   textDirection: ltr
flutter:   verticalDirection: down
flutter: ◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤
flutter: ════════════════════════════════════════════════════════════════════════════════════════════════════
Syncing files to device iPad Pro (9.7-inch)...

从上面错误输出信息可以看到,整体宽度溢出了312个像素(A RenderFlex overflowed by 312 pixels on the right)。为什么是312个像素呢?我们来分析一步步分析下。首先我们选择的设备是 iPad Pro (9.7-inch) ,它的逻辑分辨率是1024*768,我们从Flutter Inspector里也可以看到:

第149行代码height:600限制了Card的高度,Card本身内部margin为4个像素,所以第151行Row的高度为600-4-4=592个像素高,同样宽度为1024-4-4=1016个像素,可参见下图红框标记处:
Row有2个Widgets,第1个Widget宽度为440像素,见155行。第2个Widget是Image,引用的是pavlova.jpg图片,下图可以看到它的分辨率是1280*853,它的对齐方式BoxFit.cover,保持图片比例不失真的前提下,尽可能地充满父容器,所以它的宽度被等比缩小到:1280*592/853=888.347个像素。
综合以上的分析,Row可视宽度为1016个像素,它的2个子Widget宽度之和为440+888.37个像素,同时Row默认布局方式是MainAxisAlignment.start,所以是右侧溢出440+888.37-1016=312个像素。你可以通过Flutter Inspector的Render Tree的功能,看到具体Widget的信息,如下图:
问题分析清楚了,我们就可以着手解决问题了,无非有这么几个方法:

  1. 将图片的宽度控制在888-312=576像素以内;
  2. 将图片超出的312像素裁剪掉,不显示;
  3. 为Row控件增加水平滚动条;
  4. 将图片拉伸充满容器,如BoxFit.fill;
  5. 重新裁剪物理图片大小和比例,以适应布局;
  6. 重新设计布局方案,如将Row改为Column;
  7. 更换更高分辨率的平板设备,如12.9-inch iPad Pro。

下面我们用方法1来解决溢出问题,很容易想到使用下图红框所示的代码:

或者这样:
显然我们不可能每次都去计算576这个数值,而且使用具体的数字也无法做到自动适配各种分辨率的设备,我们可以使用Expanded Widget来达到自适应效果,将原先的mainImage替换为下面的代码,运行效果如下图:

 Expanded(
        child: mainImage,
         ),

试一试

尝试用上述的方法2和方法3,解决显示溢出问题。可以参考控制台输出的提示:"If the content is legitimately bigger than the available space, consider clipping it with a ClipRect widget before putting it in the flex, or using a scrollable container rather than a Flex, like a ListView."

Container Widget

前面的几个例子,我们已经对Container的作用了有了直观认识,Containder可以设置padding、margins、borders,改变背景色或者图片,只包含一个子widget,但是这个子widget可以是行、列或者是widget树的根widget。类似于Web的盒模型,div标签。border外观可以通过Decoration类进行控制。

GridView Widget

GridView将widget作为二维列表展示。当GridView检测到内容太长而无法适应渲染盒时,它就会自动支持滚动。

ListView Widget

和列很相似的Widget,当内容长于自己的渲染盒时,就会自动支持滚动。

Stack Widget

Stack在基础widget(通常是图片)上排列widget。widget可以完全或者部分覆盖基础 widget。

Card Widget

Card通常和ListTile一起使用。Card只有一个子项,这个子项可以是列、行、列表、网格或者其他支持多个子项的 widget。默认情况下,Card 的大小是 0x0 像素,可以使用 SizedBox 控制card的大小。

ListTile Widget

ListTile可以很轻松的创建一个包含三行文本以及可选的行前和行尾图标的行。ListTile在Card或者ListView 中最常用,但是也可以在别处使用。

进一步练习

  • 布局基本练习 将学习 Row,Column,Container,Flexible,Expanded, SizedBox,Spacer,Text,Icon,Image Widgets基础用法;
  • 完整布局练习 将学习如何从UI设计稿一步步构建一个完整的Flutter布局;
  • Widgets示例库 这里有各种Material Widget在线效果演示,包括源代码,建议都预览看下,对常见的Widget特点有个初步认知。

实验七

模拟以下界面布局效果(2选1或2选2):

拓展:高效布局

虽然IDE都会提供了一些智能提示和代码补全功能,Flutter Outline插件也可以对布局直接进行一些简单的控制,但对于复杂界面构建来说,总是有些力不从心。Flutter官方还未提供所见即所得的Widget布局编辑器。你可以尝试一些Flutter插件,如Flutter App Template Generator,或者试试一款在线Widget编辑器。如果你是Windows操作系统的话,还可以试试下载这个Windows应用Widget-Maker

上一篇 Widget概览 下一篇 UI交互控制