掌握Flutter布局技巧

发表时间: 2024-01-20 11:49

上一节介绍了Flutter构建界面的一些要素,并通过一些代码和运行结果让大家对Flutter能有一些基本了解。从这一节开始,将对Flutter构建界面的要素进行逐一展开。

不管我们之前是做web开发、桌面端开发、移动客户端开发,其实都知道布局,它是我们构建界面的基础。那么在Flutter中学习布局,需要理解核心要点有哪些呢?

  • 部件是用来构建UI的类。
  • 部件用于布局和UI元素。
  • 组合简单的部件来构建复杂的部件。

Flutter布局机制的核心是widget,即部件。在Flutter中,几乎所有东西都是部件——甚至布局模型也是部件。在Flutter应用程序中看到的图像、图标和文本都是部件。还有看不到的东西也是部件,例如排列、约束和对齐可见部件的行、列和网格。

通过组合部件来创建布局,从而构建更复杂的部件。例如,下图显示了可视化布局,显示了一行3列,每列包含一个图标和一个标签。

下面是这个UI的部件树:

上图中大部分应该与我们所期望的一样,但是可能会对Container(粉红色显示)感到疑惑。Container是一个部件类,允许自定义它的子部件。当需要添加填充、边距、边框或背景颜色时,可以使用Container来命名它的一些功能。

在本例中,每个Text小部件都放置在一个Container中,并添加边距,整个Row也被放置在一个Container中,以实现在行周围添加填充。

本例中UI的其余部分由属性控制。使用color属性设置图标的颜色,使用Text.style属性设置字体、颜色、粗细等。列和行也具有一些属性,这些属性允许您指定它们的子元素如何垂直或水平对齐,以及子元素应该占用多少空间。

实现一个简单的布局

如何在Flutter中布局一个部件?后续将展示如何创建和显示一个简单的部件,它还显示了一个简单的Hello World应用程序的完整代码。在Flutter中,只需要几个步骤就可以在屏幕上放置文本、图标或图像。

1.1 选择一个布局部件

根据希望如何对齐或约束可见小部件的方式,从各种布局小部件中进行选择,因为这些特征通常会传递给所包含的部件。这个例子使用Center来水平和垂直地居中它的内容。

1.2 创建一个可见部件

例如,创建一个Text部件:

Text('Hello World'),

创建一个icon部件‘

Icon(  Icons.star,  color: Colors.red[500],),

1.3 将可见部件添加到布局部件

所有布局部件都有以下两种:

  • 一个子属性,如果它们有一个子属性,例如CenterContainer
  • 子属性接受一个部件列表,例如RowColumnListViewStack

将Text部件添加到Center部件:

const Center(  child: Text('Hello World'),),

1.4 将布局部件添加到页面

Flutter应用程序本身就是一个部件,大多数部件都有一个build()方法。在应用程序的build()方法中实例化并返回一个部件,并将显示该部件。

对于Material应用,可以使用Scaffold部件,它提供了一个默认的banner、背景色,并提供了用于添加drawerssnack barsbottom sheets的API,然后可以将Center小部件直接添加到主页的body属性中。

class MyApp extends StatelessWidget {  const MyApp({super.key});  @override  Widget build(BuildContext context) {    const String appTitle = 'Flutter layout demo';    return MaterialApp(      title: appTitle,      home: Scaffold(        appBar: AppBar(          title: const Text(appTitle),        ),        body: const Center(          child: Text('Hello World'),        ),      ),    );  }}

如果想要构建苹果风格的界面,可以使用CupertinoAppCupertinoPageScaffold,具体代码就不详细展示了。

如果以上两种风格我们都不需要,我们可以直接使用Container即可,但是它不包括AppBar、标题或背景颜色等待。如果需要使用这些功能,就必须自己构建它们。下面的代码实现的功能是:将背景颜色更改为白色,文本更改为深灰色。

class MyApp extends StatelessWidget {  const MyApp({super.key});  @override  Widget build(BuildContext context) {    return Container(      decoration: const BoxDecoration(color: Colors.white),      child: const Center(        child: Text(          'Hello World',          textDirection: TextDirection.ltr,          style: TextStyle(            fontSize: 32,            color: Colors.black87,          ),        ),      ),    );  }}

运行结果:

垂直和水平布局多个部件

最常见的布局模式之一是垂直或水平排列部件。可以使Row部件水平排列部件,使用Column部件垂直排列部件。以下是这两个布局的

  • Row和Column各有一个子部件列表。
  • 子部件本身可以是行、列或其他复杂部件。
  • 可以指定行或列如何垂直和水平对齐其子元素。
  • 可以拉伸或约束特定的部件。
  • 可以指定子部件如何使用行或列的可用空间。

可以使用mainAxisAlignmentcrossAxisAlignment属性控制行或列如何对齐其子行或列。

对于Row,主轴水平运行,交叉轴垂直运行。

对于Column,主轴垂直运行,十字轴水平运行。

在下面的例子中,这3张图片的宽度都是100px。渲染框(在本例中是整个屏幕)的宽度超过300px,因此将主轴对齐方式设置为spaceEvenly将每个图像前后的自由水平空间均匀划分。

Row(  mainAxisAlignment: MainAxisAlignment.spaceEvenly,  children: [    Image.asset('images/pic1.jpg'),    Image.asset('images/pic2.jpg'),    Image.asset('images/pic3.jpg'),  ],);

效果:

垂直方向代码如下:

Column(  mainAxisAlignment: MainAxisAlignment.spaceEvenly,  children: [    Image.asset('images/pic1.jpg'),    Image.asset('images/pic2.jpg'),    Image.asset('images/pic3.jpg'),  ],);

效果:


2.1 组件大小

在Flutter中当布局太大而不适合设备时,受影响的边缘会出现黄黑条纹图案。下面是一个行太宽的例子:

通过使用Expanded部件,可以调整小部件的大小,使其适合一行或一列。要修复前面的示例,即图像行对于呈现框来说太宽,请使用Expanded部件包装每个图像。

Row(  crossAxisAlignment: CrossAxisAlignment.center,  children: [    Expanded(      child: Image.asset('images/pic1.jpg'),    ),    Expanded(      child: Image.asset('images/pic2.jpg'),    ),    Expanded(      child: Image.asset('images/pic3.jpg'),    ),  ],);

效果:

如果我们希望一个部件占用的空间是同类部件的两倍。为此,请使用Expanded部件的flex属性,这是一个决定小部件伸缩系数的整数。默认的伸缩因子是1。下面的代码将中间图像的伸缩系数设置为2:

Row(  crossAxisAlignment: CrossAxisAlignment.center,  children: [    Expanded(      child: Image.asset('images/pic1.jpg'),    ),    Expanded(      flex: 2,      child: Image.asset('images/pic2.jpg'),    ),    Expanded(      child: Image.asset('images/pic3.jpg'),    ),  ],);

效果:

2.2 组件填充方式

默认情况下,一行或列沿其主轴占用尽可能多的空间,但如果你想让子组件挤在一起,可通过设置mainAxisSizeMainAxisSize.min

Row(  mainAxisSize: MainAxisSize.min,  children: [    Icon(Icons.star, color: Colors.green[500]),    Icon(Icons.star, color: Colors.green[500]),    Icon(Icons.star, color: Colors.green[500]),    const Icon(Icons.star, color: Colors.black),    const Icon(Icons.star, color: Colors.black),  ],)

效果:

2.3 嵌套行和列

下面一段代码:

final stars = Row(  mainAxisSize: MainAxisSize.min,  children: [    Icon(Icons.star, color: Colors.green[500]),    Icon(Icons.star, color: Colors.green[500]),    Icon(Icons.star, color: Colors.green[500]),    const Icon(Icons.star, color: Colors.black),    const Icon(Icons.star, color: Colors.black),  ],);final ratings = Container(  padding: const EdgeInsets.all(20),  child: Row(    mainAxisAlignment: MainAxisAlignment.spaceEvenly,    children: [      stars,      const Text(        '170 Reviews',        style: TextStyle(          color: Colors.black,          fontWeight: FontWeight.w800,          fontFamily: 'Roboto',          letterSpacing: 0.5,          fontSize: 20,        ),      ),    ],  ),);

部件树:

效果:


其他常用布局部件

Flutter有一个丰富的布局部件库。这里有一些最常用的部件,以达到快速实现各种效果的目的,而不用我们去重复的造轮子。当然Flutter还有更多其他的部件部件,后续章节会单独列出来进行介绍。

标准部件

  • Container: 为部件添加填充、边距、边框、背景色或其他装饰。
  • GridView: 将部件作为一个可滚动的网格展开。
  • ListView: 将部件以可滚动列表的形式展开。
  • Stack: 将一个部件重叠在另一个部件的顶部。

Material部件

  • Card: 将相关信息组织到圆角和阴影框中。
  • ListTile: 将最多3行文本,以及可选的前后图标组织成一行。

3.1 Container

许多布局可以自由地使用Container来使用填充来分隔部件,或者添加边框或边距。可以通过将整个布局放入Container中并更改其背景颜色或图像来更改设备的背景。以下是Container的概要:

  • 添加padding,margin,border
  • 更改背景颜色或图像
  • 包含单个子部件,但该子部件可以是行、列,甚至是部件树的根

通过以下代码,进一步理解Container部件:

Widget _buildImageColumn() {  return Container(    decoration: const BoxDecoration(      color: Colors.black26,    ),    child: Column(      children: [        _buildImageRow(1),        _buildImageRow(3),      ],    ),  );}Widget _buildDecoratedImage(int imageIndex) => Expanded(      child: Container(        decoration: BoxDecoration(          border: Border.all(width: 10, color: Colors.black38),          borderRadius: const BorderRadius.all(Radius.circular(8)),        ),        margin: const EdgeInsets.all(4),        child: Image.asset('images/pic$imageIndex.jpg'),      ),    );Widget _buildImageRow(int imageIndex) => Row(      children: [        _buildDecoratedImage(imageIndex),        _buildDecoratedImage(imageIndex + 1),      ],    );

效果:

3.2 GridView

使用GridView将部件布置为二维列表。GridView提供了两个预制列表,或者您可以构建自己的自定义网格。当GridView检测到它的内容太长而不适合渲染框时,它会自动滚动。以下是GridView的概要:

  • 在网格中布局部件
  • 检测列内容何时超过呈现框并自动提供滚动
  • 构建自定义网格,或使用提供的网格之一:
    GridView.count 允许指定列的数量
    GridView.extent 允许指定贴图的最大像素宽度
Widget _buildGrid() => GridView.extent(    maxCrossAxisExtent: 150,    padding: const EdgeInsets.all(4),    mainAxisSpacing: 4,    crossAxisSpacing: 4,    children: _buildGridTileList(30));List<Container> _buildGridTileList(int count) => List.generate(    count, (i) => Container(child: Image.asset('images/pic$i.jpg')));

使用GridView.extent创建一个最大150像素宽的网格,效果如下:

3.3 ListView

ListView是一个类似列的部件,当它的内容对于呈现框来说太长时,它会自动提供滚动。

  • 一个特殊的Column,并以特定的规则组织内部的部件
  • 可以水平布局,也可以垂直布局
  • 检测其内容,并决定何时提供滚动
Widget _buildList() {  return ListView(    children: [      _tile('CineArts at the Empire', '85 W Portal Ave', Icons.theaters),      _tile('The Castro Theater', '429 Castro St', Icons.theaters),      _tile('Alamo Drafthouse Cinema', '2550 Mission St', Icons.theaters),      _tile('Roxie Theater', '3117 16th St', Icons.theaters),      _tile('United Artists Stonestown Twin', '501 Buckingham Way',          Icons.theaters),      _tile('AMC Metreon 16', '135 4th St #3000', Icons.theaters),      const Divider(),      _tile('K\'s Kitchen', '757 Monterey Blvd', Icons.restaurant),      _tile('Emmy\'s Restaurant', '1923 Ocean Ave', Icons.restaurant),      _tile('Chaiya Thai Restaurant', '272 Claremont Blvd', Icons.restaurant),      _tile('La Ciccia', '291 30th St', Icons.restaurant),    ],  );}ListTile _tile(String title, String subtitle, IconData icon) {  return ListTile(    title: Text(title,        style: const TextStyle(          fontWeight: FontWeight.w500,          fontSize: 20,        )),    subtitle: Text(subtitle),    leading: Icon(      icon,      color: Colors.blue[500],    ),  );}

运行效果:

3.4 Stack

使用Stack将部件排列在基本部件之上,部件可以完全或部分地与基本部件重叠。概要如下:

  • 用于将一个部件叠加到另一个部件
  • 子部件列表中的第一个部件是基本部件,随后的子部件被覆盖在该基本部件的顶部
  • Stack的内容不能滚动
Widget _buildStack() {  return Stack(    alignment: const Alignment(0.6, 0.6),    children: [      const CircleAvatar(        backgroundImage: AssetImage('images/pic.jpg'),        radius: 100,      ),      Container(        decoration: const BoxDecoration(          color: Colors.black45,        ),        child: const Text(          'Mia B',          style: TextStyle(            fontSize: 20,            fontWeight: FontWeight.bold,            color: Colors.white,          ),        ),      ),    ],  );}

运行效果:

3.5 Card

Material库中的Card包含相关的信息,几乎可以由任何小部件组成,但通常与ListTile一起使用。Card有一个子节点,但它的子节点可以是列、行、列表、网格或其他支持多个子节点的小部件。默认情况下,Card将其大小缩小为0 × 0像素。您可以使用SizedBox来限制卡片的大小。

在Flutter中,Card具有圆角和阴影,使其具有3D效果。改变Card的elevation属性可以让你控制器阴影效果。

Widget _buildCard() {  return SizedBox(    height: 210,    child: Card(      child: Column(        children: [          ListTile(            title: const Text(              '1625 Main Street',              style: TextStyle(fontWeight: FontWeight.w500),            ),            subtitle: const Text('My City, CA 99984'),            leading: Icon(              Icons.restaurant_menu,              color: Colors.blue[500],            ),          ),          const Divider(),          ListTile(            title: const Text(              '(408) 555-1212',              style: TextStyle(fontWeight: FontWeight.w500),            ),            leading: Icon(              Icons.contact_phone,              color: Colors.blue[500],            ),          ),          ListTile(            title: const Text('costa@example.com'),            leading: Icon(              Icons.contact_mail,              color: Colors.blue[500],            ),          ),        ],      ),    ),  );}

效果:

3.6 ListTile

ListTile 是Material库中的一个专门的行部件,可以轻松地创建包含最多3行文本和可选的前后图标的行。ListTile最常用于Card或ListView,但也可以用于其他地方。具体代码和效果,可参考上面的Card代码。

布局约束

在Flutter中,"Constraints"(约束)通常指的是一个描述 Widget 可以使用的空间大小的对象。布局系统使用约束来决定 Widget 的大小以及其在屏幕上的位置。

BoxConstraints(盒子约束): BoxConstraints 是描述 Widget 可以使用的空间范围的对象。它包含了最小宽度、最大宽度、最小高度、最大高度等属性。这些属性用于定义 Widget 可以占用的空间的范围。

ConstrainedBox: ConstrainedBox 是一个 Widget,它允许您为其子 Widget 指定额外的约束条件,强制子 Widget 在给定的约束条件下进行布局。

IntrinsicHeight 和 IntrinsicWidth: 这两个 Widget 允许其子 Widget 自适应其固有的高度或宽度。它们在包含的 Widget 可以根据其内容自动调整大小时非常有用。

MediaQuery: MediaQuery 允许您查询当前应用程序的屏幕大小和其他屏幕相关的信息,从而确定约束条件。

LayoutBuilder: LayoutBuilder 是一个用于构建依赖于父 Widget 约束的 Widget 的常见工具。它接受一个构建函数,该函数会接收约束参数并返回一个 Widget。

这一小节主要列举了一些布局约束组件和基本概念,布局约束涉及的内容比较多,后续会专门使用一个章节进行详细介绍。

小结

这一节,主要介绍了Flutter的布局部件,也是我们构建Flutter UI的基础。用户界面通过Widget树的层次结构构建,常用的布局Widget包括Container、Row、Column、Stack、Expanded等,它们可以用于灵活排列和组织子Widget。Padding和Margin通过EdgeInsets类用于定义内边距和外边距,调整Widget的间距。ListView和GridView分别用于垂直和二维网格排列子Widget。Flutter提供丰富的布局工具和技术,适用于各种应用场景,通过递归构建Widget树来实现整个界面的绘制。