简述
https://api.flutter.dev/flutter/material/Scaffold-class.html
一个完整的路由页可能会包含导航栏、抽屉菜单(Drawer)以及底部Tab导航菜单等。如果每个路由页面都需要开发者自己手动去实现这些,这会是一件非常麻烦且无聊的事。幸运的是,Flutter Material组件库提供了一些现成的组件来减少我们的开发任务。Scaffold
是一个路由页的骨架,我们使用它可以很容易地拼装出一个完整的页面。
此类提供了用于显示drawer、snackbar和底部sheet等的API。
Scaffold({
Key key,
this.appBar, //显示在界面顶部的一个 AppBar。
this.body, //当前界面所显示的主要内容 Widget。
this.floatingActionButton,
this.floatingActionButtonLocation,
this.floatingActionButtonAnimator,
this.persistentFooterButtons,
this.drawer, //抽屉菜单控件。
this.endDrawer,
this.bottomNavigationBar,
this.bottomSheet,
this.backgroundColor,
this.resizeToAvoidBottomPadding,
this.resizeToAvoidBottomInset,
this.primary = true,
this.drawerDragStartBehavior = DragStartBehavior.start,
this.extendBody = false,
this.extendBodyBehindAppBar = false,
this.drawerScrimColor,
this.drawerEdgeDragWidth,
this.drawerEnableOpenDragGesture = true,
this.endDrawerEnableOpenDragGesture = true,
})
示例:
我们实现一个页面,它包含:
- 一个导航栏
- 导航栏右边有一个分享按钮
- 有一个抽屉菜单
- 有一个底部导航
- 右下角有一个悬浮的动作按钮
import 'package:flutter/material.dart';
import "./MyDrawer.dart";
class ScaffoldDemo extends StatefulWidget {
@override
_ScaffoldDemoState createState() => _ScaffoldDemoState();
}
class _ScaffoldDemoState extends State<ScaffoldDemo> {
int _selectedIndex = 1;
List _list = ['home', 'business', 'school'];
@override
Widget build(BuildContext context) {
return Scaffold(
body: Text(_list[_selectedIndex]),
// 顶部导航栏
appBar: AppBar(
title: Text('scaffold demo'), //导航栏标题
actions: [
//导航栏右侧分享
IconButton(icon: Icon(Icons.share), onPressed: () {}),
],
),
//侧栏
drawer: MyDrawer(),
// 底部导航栏
bottomNavigationBar: BottomNavigationBar(
items: [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'home'),
BottomNavigationBarItem(icon: Icon(Icons.business), label: 'business'),
BottomNavigationBarItem(icon: Icon(Icons.school), label: 'school'),
],
currentIndex: _selectedIndex,
onTap: (index) {
setState(() {
_selectedIndex = index;
});
},
),
// 悬浮按钮
floatingActionButton: FloatingActionButton(
child: Text('悬浮'),
onPressed: () {},
),
);
}
}
TabBar
下面我们通过“bottom”属性来添加一个导航栏底部Tab按钮组,将要实现的效果如图5-21所示:
Material组件库中提供了一个TabBar
组件,它可以快速生成Tab
菜单,下面是上图对应的源码:
class _ScaffoldRouteState extends State<ScaffoldRoute> with SingleTickerProviderStateMixin {
TabController _tabController; //需要定义一个Controller
List tabs = ["新闻", "历史", "图片"];
@override
void initState() {
super.initState();
// 创建Controller
_tabController = TabController(length: tabs.length, vsync: this);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
bottom: TabBar( //生成Tab菜单
controller: _tabController,
tabs: tabs.map((e) => Tab(text: e)).toList()
),
),
}
上面代码首先创建了一个TabController
,它是用于控制/监听Tab
菜单切换的。接下来通过TabBar生成了一个底部菜单栏,TabBar
的tabs
属性接受一个Widget数组,表示每一个Tab子菜单,我们可以自定义,也可以像示例中一样直接使用Tab
组件,它是Material组件库提供的Material风格的Tab菜单。
Tab
组件有三个可选参数,除了可以指定文字外,还可以指定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来作为页面的左、右抽屉菜单。如果开发者提供了抽屉菜单,那么当用户手指从屏幕左(或右)侧向里滑动时便可打开抽屉菜单。
示例
import 'package:flutter/material.dart';
class MyDrawer extends StatelessWidget {
const MyDrawer({Key key}) : super(key: key);
@override
Widget 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(
"assets/images/avatar.jpg",
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风格的底部导航栏。可以看到上面的实现代码非常简单,所以不再赘述,但是如果我们想实现如图5-23所示效果的底部导航栏应该怎么做呢?
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示例中就有一个“钻石”形状的示例,读者感兴趣可以自行查看。