Flutter实战 Scaffold、TabBar、底部导航

2021-03-08 10:41 更新

Material 组件库提供了丰富多样的组件,本节介绍一些常用的组件,其余的读者可以自行查看文档或 Flutter Gallery 中 Material 组件部分的示例。

Flutter Gallery 是 Flutter 官方提供的 Flutter Demo,源码位于 flutter 源码中的 examples 目录下,笔者强烈建议用户将 Flutter Gallery 示例跑起来,它是一个很全面的 Flutter 示例应用,是非常好的参考 Demo,也是笔者学习 Flutter 的第一手资料。

#5.6.1 Scaffold

一个完整的路由页可能会包含导航栏、抽屉菜单(Drawer)以及底部Tab导航菜单等。如果每个路由页面都需要开发者自己手动去实现这些,这会是一件非常麻烦且无聊的事。幸运的是,Flutter Material组件库提供了一些现成的组件来减少我们的开发任务。Scaffold是一个路由页的骨架,我们使用它可以很容易地拼装出一个完整的页面。

#示例

我们实现一个页面,它包含:

  1. 一个导航栏
  2. 导航栏右边有一个分享按钮
  3. 有一个抽屉菜单
  4. 有一个底部导航
  5. 右下角有一个悬浮的动作按钮

最终效果如图5-18、图5-19所示:

图5-18 图5-19

实现代码如下:

  1. class ScaffoldRoute extends StatefulWidget {
  2. @override
  3. _ScaffoldRouteState createState() => _ScaffoldRouteState();
  4. }
  5. class _ScaffoldRouteState extends State<ScaffoldRoute> {
  6. int _selectedIndex = 1;
  7. @override
  8. Widget build(BuildContext context) {
  9. return Scaffold(
  10. appBar: AppBar( //导航栏
  11. title: Text("App Name"),
  12. actions: <Widget>[ //导航栏右侧菜单
  13. IconButton(icon: Icon(Icons.share), onPressed: () {}),
  14. ],
  15. ),
  16. drawer: new MyDrawer(), //抽屉
  17. bottomNavigationBar: BottomNavigationBar( // 底部导航
  18. items: <BottomNavigationBarItem>[
  19. BottomNavigationBarItem(icon: Icon(Icons.home), title: Text('Home')),
  20. BottomNavigationBarItem(icon: Icon(Icons.business), title: Text('Business')),
  21. BottomNavigationBarItem(icon: Icon(Icons.school), title: Text('School')),
  22. ],
  23. currentIndex: _selectedIndex,
  24. fixedColor: Colors.blue,
  25. onTap: _onItemTapped,
  26. ),
  27. floatingActionButton: FloatingActionButton( //悬浮按钮
  28. child: Icon(Icons.add),
  29. onPressed:_onAdd
  30. ),
  31. );
  32. }
  33. void _onItemTapped(int index) {
  34. setState(() {
  35. _selectedIndex = index;
  36. });
  37. }
  38. void _onAdd(){
  39. }
  40. }

上面代码中我们用到了如下组件:

组件名称 解释
AppBar 一个导航栏骨架
MyDrawer 抽屉菜单
BottomNavigationBar 底部导航栏
FloatingActionButton 漂浮按钮

下面我们来分别介绍一下它们。

#5.6.2 AppBar

AppBar是一个 Material 风格的导航栏,通过它可以设置导航栏标题、导航栏菜单、导航栏底部的Tab标题等。下面我们看看 AppBar 的定义:

  1. AppBar({
  2. Key key,
  3. this.leading, //导航栏最左侧Widget,常见为抽屉菜单按钮或返回按钮。
  4. this.automaticallyImplyLeading = true, //如果leading为null,是否自动实现默认的leading按钮
  5. this.title,// 页面标题
  6. this.actions, // 导航栏右侧菜单
  7. this.bottom, // 导航栏底部菜单,通常为Tab按钮组
  8. this.elevation = 4.0, // 导航栏阴影
  9. this.centerTitle, //标题是否居中
  10. this.backgroundColor,
  11. ... //其它属性见源码注释
  12. })

如果给Scaffold添加了抽屉菜单,默认情况下Scaffold会自动将AppBarleading设置为菜单按钮(如上面截图所示),点击它便可打开抽屉菜单。如果我们想自定义菜单图标,可以手动来设置leading,如:

  1. Scaffold(
  2. appBar: AppBar(
  3. title: Text("App Name"),
  4. leading: Builder(builder: (context) {
  5. return IconButton(
  6. icon: Icon(Icons.dashboard, color: Colors.white), //自定义图标
  7. onPressed: () {
  8. // 打开抽屉菜单
  9. Scaffold.of(context).openDrawer();
  10. },
  11. );
  12. }),
  13. ...
  14. )

代码运行效果如图5-20所示:

可以看到左侧菜单已经替换成功。

代码中打开抽屉菜单的方法在ScaffoldState中,通过Scaffold.of(context)可以获取父级最近的Scaffold 组件的State对象。

#TabBar

下面我们通过“bottom”属性来添加一个导航栏底部 Tab 按钮组,将要实现的效果如图5-21所示:

图5-21

Material 组件库中提供了一个TabBar组件,它可以快速生成Tab菜单,下面是上图对应的源码:

  1. class _ScaffoldRouteState extends State<ScaffoldRoute>
  2. with SingleTickerProviderStateMixin {
  3. TabController _tabController; //需要定义一个Controller
  4. List tabs = ["新闻", "历史", "图片"];
  5. @override
  6. void initState() {
  7. super.initState();
  8. // 创建Controller
  9. _tabController = TabController(length: tabs.length, vsync: this);
  10. }
  11. @override
  12. Widget build(BuildContext context) {
  13. return Scaffold(
  14. appBar: AppBar(
  15. ... //省略无关代码
  16. bottom: TabBar( //生成Tab菜单
  17. controller: _tabController,
  18. tabs: tabs.map((e) => Tab(text: e)).toList()
  19. ),
  20. ),
  21. ... //省略无关代码
  22. }

上面代码首先创建了一个TabController ,它是用于控制/监听Tab菜单切换的。接下来通过 TabBar 生成了一个底部菜单栏,TabBartabs属性接受一个 Widget 数组,表示每一个 Tab 子菜单,我们可以自定义,也可以像示例中一样直接使用Tab 组件,它是 Material 组件库提供的 Material 风格的 Tab 菜单。

Tab组件有三个可选参数,除了可以指定文字外,还可以指定Tab菜单图标,或者直接自定义组件样式。Tab组件定义如下:

  1. Tab({
  2. Key key,
  3. this.text, // 菜单文本
  4. this.icon, // 菜单图标
  5. this.child, // 自定义组件样式
  6. })

开发者可以根据实际需求来定制。

#TabBarView

通过TabBar我们只能生成一个静态的菜单,真正的 Tab 页还没有实现。由于Tab菜单和 Tab 页的切换需要同步,我们需要通过TabController去监听 Tab 菜单的切换去切换 Tab 页,代码如:

  1. _tabController.addListener((){
  2. switch(_tabController.index){
  3. case 1: ...;
  4. case 2: ... ;
  5. }
  6. });

如果我们 Tab 页可以滑动切换的话,还需要在滑动过程中更新 TabBar 指示器的偏移!显然,要手动处理这些是很麻烦的,为此,Material 库提供了一个TabBarView组件,通过它不仅可以轻松的实现 Tab 页,而且可以非常容易的配合 TabBar 来实现同步切换和滑动状态同步,示例如下:

  1. Scaffold(
  2. appBar: AppBar(
  3. ... //省略无关代码
  4. bottom: TabBar(
  5. controller: _tabController,
  6. tabs: tabs.map((e) => Tab(text: e)).toList()),
  7. ),
  8. drawer: new MyDrawer(),
  9. body: TabBarView(
  10. controller: _tabController,
  11. children: tabs.map((e) { //创建3个Tab页
  12. return Container(
  13. alignment: Alignment.center,
  14. child: Text(e, textScaleFactor: 5),
  15. );
  16. }).toList(),
  17. ),
  18. ... // 省略无关代码
  19. )

运行后效果如图5-22所示:

图5-22

现在,无论是点击导航栏 Tab 菜单还是在页面上左右滑动,Tab 页面都会切换,并且 Tab 菜单的状态和 Tab 页面始终保持同步!那它们是如何实现同步的呢?细心的读者可能已经发现,上例中TabBarTabBarViewcontroller是同一个!正是如此,TabBarTabBarView正是通过同一个controller来实现菜单切换和滑动状态同步的,有关TabController的详细信息,我们不在本书做过多介绍,使用时读者直接查看 SDK 即可。

另外,Material 组件库也提供了一个PageView 组件,它和TabBarView功能相似,读者可以自行了解一下。

#5.6.3 抽屉菜单Drawer

ScaffolddrawerendDrawer属性可以分别接受一个 Widget 来作为页面的左、右抽屉菜单。如果开发者提供了抽屉菜单,那么当用户手指从屏幕左(或右)侧向里滑动时便可打开抽屉菜单。本节开始部分的示例中实现了一个左抽屉菜单MyDrawer,它的源码如下:

  1. class MyDrawer extends StatelessWidget {
  2. const MyDrawer({
  3. Key key,
  4. }) : super(key: key);
  5. @override
  6. Widget build(BuildContext context) {
  7. return Drawer(
  8. child: MediaQuery.removePadding(
  9. context: context,
  10. //移除抽屉菜单顶部默认留白
  11. removeTop: true,
  12. child: Column(
  13. crossAxisAlignment: CrossAxisAlignment.start,
  14. children: <Widget>[
  15. Padding(
  16. padding: const EdgeInsets.only(top: 38.0),
  17. child: Row(
  18. children: <Widget>[
  19. Padding(
  20. padding: const EdgeInsets.symmetric(horizontal: 16.0),
  21. child: ClipOval(
  22. child: Image.asset(
  23. "imgs/avatar.png",
  24. width: 80,
  25. ),
  26. ),
  27. ),
  28. Text(
  29. "Wendux",
  30. style: TextStyle(fontWeight: FontWeight.bold),
  31. )
  32. ],
  33. ),
  34. ),
  35. Expanded(
  36. child: ListView(
  37. children: <Widget>[
  38. ListTile(
  39. leading: const Icon(Icons.add),
  40. title: const Text('Add account'),
  41. ),
  42. ListTile(
  43. leading: const Icon(Icons.settings),
  44. title: const Text('Manage accounts'),
  45. ),
  46. ],
  47. ),
  48. ),
  49. ],
  50. ),
  51. ),
  52. );
  53. }
  54. }

抽屉菜单通常将Drawer组件作为根节点,它实现了 Material 风格的菜单面板,MediaQuery.removePadding可以移除 Drawer 默认的一些留白(比如 Drawer 默认顶部会留和手机状态栏等高的留白),读者可以尝试传递不同的参数来看看实际效果。抽屉菜单页由顶部和底部组成,顶部由用户头像和昵称组成,底部是一个菜单列表,用 ListView 实现,关于 ListView 我们将在后面“可滚动组件”一节详细介绍。

#5.6.4 FloatingActionButton

FloatingActionButton是 Material 设计规范中的一种特殊 Button,通常悬浮在页面的某一个位置作为某种常用动作的快捷入口,如本节示例中页面右下角的"➕"号按钮。我们可以通过ScaffoldfloatingActionButton属性来设置一个FloatingActionButton,同时通过floatingActionButtonLocation属性来指定其在页面中悬浮的位置,这个比较简单,不再赘述。

#5.6.5 底部Tab导航栏

我们可以通过ScaffoldbottomNavigationBar属性来设置底部导航,如本节开始示例所示,我们通过 Material 组件库提供的BottomNavigationBarBottomNavigationBarItem两种组件来实现 Material 风格的底部导航栏。可以看到上面的实现代码非常简单,所以不再赘述,但是如果我们想实现如图5-23所示效果的底部导航栏应该怎么做呢?

图5-23

Material组件库中提供了一个BottomAppBar 组件,它可以和FloatingActionButton配合实现这种“打洞”效果,源码如下:

  1. bottomNavigationBar: BottomAppBar(
  2. color: Colors.white,
  3. shape: CircularNotchedRectangle(), // 底部导航栏打一个圆形的洞
  4. child: Row(
  5. children: [
  6. IconButton(icon: Icon(Icons.home)),
  7. SizedBox(), //中间位置空出
  8. IconButton(icon: Icon(Icons.business)),
  9. ],
  10. mainAxisAlignment: MainAxisAlignment.spaceAround, //均分底部导航栏横向空间
  11. ),
  12. )

可以看到,上面代码中没有控制打洞位置的属性,实际上,打洞的位置取决于FloatingActionButton的位置,上面FloatingActionButton的位置为:

  1. floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,

所以打洞位置在底部导航栏的正中间。

BottomAppBarshape属性决定洞的外形,CircularNotchedRectangle实现了一个圆形的外形,我们也可以自定义外形,比如,Flutter Gallery 示例中就有一个“钻石”形状的示例,读者感兴趣可以自行查看。

以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号