上一节介绍了Flutter构建界面的一些要素,并通过一些代码和运行结果让大家对Flutter能有一些基本了解。从这一节开始,将对Flutter构建界面的要素进行逐一展开。
不管我们之前是做web开发、桌面端开发、移动客户端开发,其实都知道布局,它是我们构建界面的基础。那么在Flutter中学习布局,需要理解核心要点有哪些呢?
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 将可见部件添加到布局部件
所有布局部件都有以下两种:
将Text部件添加到Center部件:
const Center( child: Text('Hello World'),),
1.4 将布局部件添加到页面
Flutter应用程序本身就是一个部件,大多数部件都有一个build()方法。在应用程序的build()方法中实例化并返回一个部件,并将显示该部件。
对于Material应用,可以使用Scaffold部件,它提供了一个默认的banner、背景色,并提供了用于添加drawers、snack bars和bottom 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'), ), ), ); }}
如果想要构建苹果风格的界面,可以使用CupertinoApp和CupertinoPageScaffold,具体代码就不详细展示了。
如果以上两种风格我们都不需要,我们可以直接使用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部件垂直排列部件。以下是这两个布局的
可以使用mainAxisAlignment和crossAxisAlignment属性控制行或列如何对齐其子行或列。
对于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 组件填充方式
默认情况下,一行或列沿其主轴占用尽可能多的空间,但如果你想让子组件挤在一起,可通过设置mainAxisSize为MainAxisSize.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还有更多其他的部件部件,后续章节会单独列出来进行介绍。
标准部件
Material部件
3.1 Container
许多布局可以自由地使用Container来使用填充来分隔部件,或者添加边框或边距。可以通过将整个布局放入Container中并更改其背景颜色或图像来更改设备的背景。以下是Container的概要:
通过以下代码,进一步理解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的概要:
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是一个类似列的部件,当它的内容对于呈现框来说太长时,它会自动提供滚动。
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将部件排列在基本部件之上,部件可以完全或部分地与基本部件重叠。概要如下:
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树来实现整个界面的绘制。