Material组件库提供了丰富多样的组件,本节介绍一些常用的组件,其余的读者可以自行查看文档或Flutter Gallery中Material组件部分的示例。
Flutter Gallery是Flutter官方提供的Flutter Demo,源码位于flutter源码中的examples目录下,笔者强烈建议用户将Flutter Gallery示例跑起来,它是一个很全面的Flutter示例应用,是非常好的参考Demo,也是笔者学习Flutter的第一手资料。
Scaffold
一个完整的路由页可能会包含导航栏抽屉菜单(Drawer)以及底部Tab导航菜单等。如果每个路由页面都需要开发者自己手动去实现这些,这会是一件非常麻烦且无聊的事。幸运的是,Flutter Material组件库提供了一些现成的组件来减少我们的开发任务。Scaffold是一个路由页的骨架,我们使用它可以很容易地拼装出一个完整的页面。
我们实现一个页面,它包含:
- 一个导航栏
- 导航栏右边有一个分享按钮
- 有一个抽屉菜单
- 有一个底部导航
- 右下角有一个悬浮的动作按钮
最终效果如图5-18、图5-19所示:

实现代码如下:
import 'package:flutter/material.dart';// import 'dart:math' as math;void main() => runApp(new MyApp());class MyApp extends StatelessWidget {@overrideWidget build(BuildContext context) {return MaterialApp(debugShowCheckedModeBanner: false,title: '装饰容器DecoratedBox',theme: ThemeData(primarySwatch: Colors.blue,),home: ScaffoldRoute());}}class ScaffoldRoute extends StatefulWidget {@override_ScaffoldRouteState createState() => _ScaffoldRouteState();}class _ScaffoldRouteState extends State<ScaffoldRoute> {int _selectedIndex = 1;@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(//导航栏title: Text("App Name"),actions: <Widget>[//导航栏右侧菜单IconButton(icon: Icon(Icons.share), onPressed: () {}),],),drawer: new MyDrawer(), //抽屉bottomNavigationBar: BottomNavigationBar(// 底部导航items: <BottomNavigationBarItem>[BottomNavigationBarItem(icon: Icon(Icons.home), title: Text('Home')),BottomNavigationBarItem(icon: Icon(Icons.business), title: Text('Business')),BottomNavigationBarItem(icon: Icon(Icons.school), title: Text('School')),],currentIndex: _selectedIndex,fixedColor: Colors.blue,onTap: _onItemTapped,),floatingActionButton: FloatingActionButton(//悬浮按钮child: Icon(Icons.add),onPressed: _onAdd),);}void _onItemTapped(int index) {setState(() {_selectedIndex = index;});}void _onAdd() {}}class MyDrawer extends StatelessWidget {const MyDrawer({Key key,}) : super(key: key);@overrideWidget build(BuildContext context) {return Drawer(child: MediaQuery.removePadding(context: context,//移除抽屉菜单顶部默认留白removeTop: true,child: Column(crossAxisAlignment: CrossAxisAlignment.start,children: <Widget>[Padding(padding: const EdgeInsets.only(top: 38.0),child: Row(children: <Widget>[Padding(padding: const EdgeInsets.symmetric(horizontal: 16.0),child: ClipOval(child: Image.asset("imgs/avatar.png",width: 80,),),),Text("Wendux",style: TextStyle(fontWeight: FontWeight.bold),)],),),Expanded(child: ListView(children: <Widget>[ListTile(leading: const Icon(Icons.add),title: const Text('Add account'),),ListTile(leading: const Icon(Icons.settings),title: const Text('Manage accounts'),),],),),],),),);}}
上面代码中我们用到了如下组件:
| 组件名称 | 解释 |
|---|---|
| AppBar | 一个导航栏骨架 |
| MyDrawer | 抽屉菜单 |
| BottomNavigationBar | 底部导航栏 |
| FloatingActionButton | 漂浮按钮 |
AppBar
AppBar是一个Material风格的导航栏,通过它可以设置导航栏标题、导航栏菜单、导航栏底部的Tab标题等。下面我们看看AppBar的定义:
AppBar({Key key,this.leading, //导航栏最左侧Widget,常见为抽屉菜单按钮或返回按钮。this.automaticallyImplyLeading = true, //如果leading为null,是否自动实现默认的leading按钮this.title,// 页面标题this.actions, // 导航栏右侧菜单this.bottom, // 导航栏底部菜单,通常为Tab按钮组this.elevation = 4.0, // 导航栏阴影this.centerTitle, //标题是否居中this.backgroundColor,... //其它属性见源码注释})
如果给Scaffold添加了抽屉菜单,默认情况下Scaffold会自动将AppBar的leading设置为菜单按钮(如上面截图所示),点击它便可打开抽屉菜单。如果我们想自定义菜单图标,可以手动来设置leading,如:
Scaffold(appBar: AppBar(title: Text("App Name"),leading: Builder(builder: (context) {return IconButton(icon: Icon(Icons.dashboard, color: Colors.white), //自定义图标onPressed: () {// 打开抽屉菜单Scaffold.of(context).openDrawer();},);}),...)
代码运行效果如图5-20所示:
可以看到左侧菜单已经替换成功。
代码中打开抽屉菜单的方法在ScaffoldState中,通过Scaffold.of(context)可以获取父级最近的Scaffold组件的State对象
**
AppBar和自制AppBar做比较

| leading | 左上角的控件,一般放一个icon,位置如上图 |
|---|---|
| title | 标题,位置如上图 |
| actions | 一系列的组件,位置如上图 |
| flexibleSpace | 此小组件堆叠在工具栏和标签栏后面。它的高度与应用栏的整体高度相同 |
| bottom | 位置如上图 |
| elevation | 阴影Z轴 |
| backgroundColor | 背景颜色 |
| brightness | 亮度 |
| iconTheme | 图标样式(用于应用栏图标的颜色,不透明度和大小。通常,这与backgroundColor,brightness,textTheme一起设置。) |
| textTheme | 字体样式 |
| centerTitle | title是否显示在中间 |
| automaticallyImplyLeading | 如果为true且leading为null,则自动尝试推断出主要小部件应该是什么。如果为false且leading为null,则为title提供前导空格。如果leading小部件不为null,则此参数无效。 |
| toolbarOpacity | 应用栏的工具栏部分是多么不透明。 |
| bottomOpacity | 应用栏底部的不透明程度。 |
| primary | true 此应用栏是否显示在屏幕顶部。 |
| titleSpacing | 横轴上标题内容 周围的间距。即使没有前导内容或操作,也会应用此间距。如果希望 title占用所有可用空间,请将此值设置为0.0 |
import 'package:flutter/material.dart';void main() => runApp(new MyApp());class MyApp extends StatelessWidget {@overrideWidget build(BuildContext context) {return MaterialApp(debugShowCheckedModeBanner: true,title: '主题测试',theme: ThemeData(primaryColor: Colors.red,textTheme: TextTheme(display4: TextStyle(color: Colors.pink, fontSize: 24.0))),home: Scaffold(// appBar: AppBar(title: Text("主题测试")),body: ThemeTestRoute(),));}}class ThemeTestRoute extends StatefulWidget {@override_ThemeTestRouteState createState() => new _ThemeTestRouteState();}class _ThemeTestRouteState extends State<ThemeTestRoute> {Color _themeColor = Colors.teal; //当前路由主题色@overrideWidget build(BuildContext context) {ThemeData themeData = Theme.of(context);return Theme(data: ThemeData(primaryColor: _themeColor, //用于导航栏、FloatingActionButton的背景色等iconTheme: IconThemeData(color: _themeColor) //用于Icon颜色),child: Scaffold(appBar: AppBar(title: Text("主题测试")),body: Column(mainAxisAlignment: MainAxisAlignment.center,children: <Widget>[//第一行Icon使用主题中的iconThemeRow(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[Icon(Icons.favorite),Icon(Icons.airport_shuttle),Text(" 颜色跟随主题")]),//为第二行Icon自定义颜色(固定为黑色)Theme(data: themeData.copyWith(iconTheme: themeData.iconTheme.copyWith(color: Colors.black),),child: Row(mainAxisAlignment: MainAxisAlignment.center,children: <Widget>[Icon(Icons.favorite),Icon(Icons.airport_shuttle),Text(" 颜色固定黑色",style: themeData.textTheme.display4,)]),),Row(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[Icon(Icons.favorite),Icon(Icons.airport_shuttle),Text(" 颜色固定黑色")])],),floatingActionButton: FloatingActionButton(onPressed: () => //切换主题setState(() => _themeColor =_themeColor == Colors.teal ? Colors.blue : Colors.teal),child: Icon(Icons.palette),backgroundColor: Theme.of(context).primaryColor),),);}}
TabBar
下面我们通过“bottom”属性来添加一个导航栏底部Tab按钮组,将要实现的效果如图5-21所示:
Material组件库中提供了一个TabBar组件,它可以快速生成Tab菜单,下面是上图对应的源码:
class ScaffoldRoute extends StatefulWidget {@override_ScaffoldRouteState createState() => _ScaffoldRouteState();}class _ScaffoldRouteState extends State<ScaffoldRoute>with SingleTickerProviderStateMixin {TabController _tabController; //需要定义一个ControllerList<Map<String, String>> tabs = [{'tabName': '新闻','description':'这是新闻频道'}]..add({'tabName': '历史','description':'这是历史频道'})..add({'tabName': '主播','description':'这是主播频道'})..add({'tabName': "博客",'description':'这是博客频道'})..add({'tabName': "头条",'description':'这是头条频道'})..add({'tabName': "体育",'description':'这是体育频道'});@overridevoid initState() {super.initState();// 创建Controller_tabController = TabController(length: tabs.length, vsync: this);}@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(leading: IconButton(icon: Icon(Icons.receipt), onPressed: () {}),actions: <Widget>[//导航栏右侧菜单IconButton(icon: Icon(Icons.share), onPressed: () {}),],title: Text('标题'),bottom: TabBar(//生成Tab菜单controller: _tabController,tabs: tabs.map((item) => Tab(text: item['tabName'])).toList()),),body: TabBarView(controller: _tabController,children: tabs.map((item) => Text(item['description'])).toList()),// children: <Widget>[// Text('1111'),// Text('2222'),// Text('3333')// ],);}}
上面代码首先创建了一个TabController ,它是用于控制/监听Tab菜单切换的
接下来通过TabBar生成了一个底部菜单栏,TabBar的tabs属性接受一个Widget数组,表示每一个Tab子菜单,我们可以自定义,也可以像示例中一样直接使用TabBar 组件,它是Material组件库提供的Material风格的TabBar菜单。
TabBar组件有三个可选参数,除了可以指定文字外,还可以指定Tab菜单图标,或者直接自定义组件样式。Tab组件定义如下:
Tab({Key key,this.text, // 菜单文本this.icon, // 菜单图标this.child, // 自定义组件样式})
开发者可以根据实际需求来定制
TabBarView
通过TabBar我们只能生成一个静态的菜单,真正的Tab页还没有实现。由于Tab菜单和Tab页的切换需要同步,我们需要通过TabController去监听Tab菜单的切换去切换Tab页,代码如:
_tabController.addListener((){switch(_tabController.index){case 1: ...;case 2: ... ;}});
如果我们Tab页可以滑动切换的话,还需要在滑动过程中更新TabBar指示器的偏移!显然,要手动处理这些是很麻烦的,为此,Material库提供了一个TabBarView组件,通过它不仅可以轻松的实现Tab页,而且可以非常容易的配合TabBar来实现同步切换和滑动状态同步,示例如下:
Scaffold(appBar: AppBar(... //省略无关代码bottom: TabBar(controller: _tabController,tabs: tabs.map((e) => Tab(text: e)).toList()),),drawer: new MyDrawer(),body: TabBarView(controller: _tabController,children: tabs.map((e) { //创建3个Tab页return Container(alignment: Alignment.center,child: Text(e, textScaleFactor: 5),);}).toList(),),... // 省略无关代码)
运行后效果如图所示:
现在,无论是点击导航栏Tab菜单还是在页面上左右滑动,Tab页面都会切换,并且Tab菜单的状态和Tab页面始终保持同步!那它们是如何实现同步的呢?
- 上例中TabBar和TabBarView的controller是同一个!正是如此,TabBar和TabBarView正是通过同一个controller来实现菜单切换和滑动状态同步的
- 有关TabController的详细信息,我们不在本书做过多介绍,使用时读者直接查看SDK即可。
另外,Material组件库也提供了一个PageView 组件,它和TabBarView功能相似,读者可以自行了解一下。
抽屉菜单Drawer
Scaffold的drawer和endDrawer属性可以分别接受一个Widget来作为页面的左、右抽屉菜单。如果开发者提供了抽屉菜单,那么当用户手指从屏幕左(或右)侧向里滑动时便可打开抽屉菜单。本节开始部分的示例中实现了一个左抽屉菜单MyDrawer,它的源码如下:
class MyDrawer extends StatelessWidget {const MyDrawer({Key key,}) : super(key: key);@overrideWidget build(BuildContext context) {return Drawer(child: MediaQuery.removePadding(context: context,//移除抽屉菜单顶部默认留白removeTop: true,child: Column(crossAxisAlignment: CrossAxisAlignment.start,children: <Widget>[Padding(padding: const EdgeInsets.only(top: 38.0),child: Row(children: <Widget>[Padding(padding: const EdgeInsets.symmetric(horizontal: 16.0),child: ClipOval(child: Image.asset("imgs/avatar.png",width: 80,),),),Text("Wendux",style: TextStyle(fontWeight: FontWeight.bold),)],),),Expanded(child: ListView(children: <Widget>[ListTile(leading: const Icon(Icons.add),title: const Text('Add account'),),ListTile(leading: const Icon(Icons.settings),title: const Text('Manage accounts'),),],),),],),),);}}
抽屉菜单通常将Drawer组件作为根节点,它实现了Material风格的菜单面板,MediaQuery.removePadding可以移除Drawer默认的一些留白(比如Drawer默认顶部会留和手机状态栏等高的留白),读者可以尝试传递不同的参数来看看实际效果。抽屉菜单页由顶部和底部组成,顶部由用户头像和昵称组成,底部是一个菜单列表,用ListView实现,关于ListView我们将在后面“可滚动组件”一节详细介绍。
FloatingActionButton
FloatingActionButton是Material设计规范中的一种特殊Button,通常悬浮在页面的某一个位置作为某种常用动作的快捷入口,如本节示例中页面右下角的”➕”号按钮。
- 我们可以通过Scaffold的floatingActionButton属性来设置一个FloatingActionButton,
- 同时通过floatingActionButtonLocation属性来指定其在页面中悬浮的位置,这个比较简单,不再赘述
底部Tab导航栏
我们可以通过Scaffold的bottomNavigationBar属性来设置底部导航,如本节开始示例所示,我们通过Material组件库提供的BottomNavigationBar和BottomNavigationBarItem两种组件来实现Material风格的底部导航栏。可以看到上面的实现代码非常简单,所以不再赘述
但是如果我们想实现如图所示效果的底部导航栏应该怎么做呢?
Material组件库中提供了一个BottomAppBar 组件,它可以和FloatingActionButton配合实现这种“打洞”效果,源码如下:
bottomNavigationBar: BottomAppBar(color: Colors.white,shape: CircularNotchedRectangle(), // 底部导航栏打一个圆形的洞child: Row(children: [IconButton(icon: Icon(Icons.home)),SizedBox(), //中间位置空出IconButton(icon: Icon(Icons.business)),],mainAxisAlignment: MainAxisAlignment.spaceAround, //均分底部导航栏横向空间),)
可以看到,上面代码中没有控制打洞位置的属性,实际上,打洞的位置取决于FloatingActionButton的位置,上面FloatingActionButton的位置为:
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
所以打洞位置在底部导航栏的正中间。
- BottomAppBar的shape属性决定洞的外形,CircularNotchedRectangle实现了一个圆形的外
- 我们也可以自定义外形,比如,Flutter Gallery示例中就有一个“钻石”形状的示例,读者感兴趣可以自行查看。
整合代码
import 'package:flutter/material.dart';// import 'dart:math' as math;void main() => runApp(new MyApp());class MyApp extends StatelessWidget {@overrideWidget build(BuildContext context) {return MaterialApp(debugShowCheckedModeBanner: false,title: '装饰容器DecoratedBox',theme: ThemeData(primarySwatch: Colors.blue,),home: ScaffoldRoute());}}class ScaffoldRoute extends StatefulWidget {@override_ScaffoldRouteState createState() => _ScaffoldRouteState();}class _ScaffoldRouteState extends State<ScaffoldRoute> {@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(//导航栏title: Text("App Name"),actions: <Widget>[//导航栏右侧菜单IconButton(icon: Icon(Icons.share), onPressed: () {}),],),bottomNavigationBar: BottomAppBar(color: Colors.white,shape: CircularNotchedRectangle(), // 底部导航栏打一个圆形的洞child: Row(children: [IconButton(icon: Icon(Icons.home),onPressed: () {},),SizedBox(), //中间位置空出IconButton(icon: Icon(Icons.business), onPressed: () {}),],mainAxisAlignment: MainAxisAlignment.spaceAround, //均分底部导航栏横向空间),),floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,floatingActionButton: FloatingActionButton(//悬浮按钮child: Icon(Icons.add),onPressed: () {}),);}}
