第3章 Scaffold组件的详细综述 Scaffold 在英文中的意思为脚手架、建筑架,在Flutter应用程序中,也可称为脚手架,也就是说在Flutter应用开发中,通过Scaffold可以搭建页面的基本结构,一个页面可以理解为由3个部分组成: header头部,或者称之为标题栏; body体,可称之为内容主体页面; bottom脚,可称之为页面的尾部,例如bottomBar。 对于Scaffold来讲,AppBar就是它的头,body中配置加载的Widget就是它的体,底部菜单栏就是它的尾部,如图31所示。 图31Scaffold组件结构图 3.1Scaffold的基本使用 在实际应用开发中,当创建一个移动应用程序项目时,一般会设置一个启动初始化页面,那么这个页面一般没有标题,也没有底部内容区,只有一个内容body主区,在Flutter项目中,通过main函数,执行runApp方法通过MaterialApp组件来构建一个根布局Widget,然后通过home或者其他属性来配置这个启动页面如图32所示,代码如下: //3.1 /lib/code2/main_data21.dart //Scaffold的基本使用,内容主体页面 import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; //应用入口 main() => runApp(themeDataFunction()); MaterialApp themeDataFunction() { return MaterialApp(home: FirstPage(),); } class FirstPage extends StatefulWidget { @override State<StatefulWidget> createState() { return FirstThemeState(); } } class FirstThemeState extends State<FirstPage> { @override Widget build(BuildContext context) { //Scaffold 用来搭建页面的主体结构 return Scaffold( //页面的主内容区 //可以是单独的StatefulWidget,也可以是当前页面构建的,如Text文本组件 body: Center(child: Text("启动页面"),),); } } 所以对于Scaffold来讲,它实现了基本的Material Design设计结构,一般可以用作单页面的父结构,在实际项目开发中,由标题栏与页面主体body构成的页面也是比较常见的,如图33所示,在Flutter中也可通过Scaffold来实现,代码如下: //3.1 /lib/code2/main_data22.dart //Scaffold的基本使用,有AppBar的页面 import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; //应用入口 main() => runApp(themeDataFunction()); MaterialApp themeDataFunction() { return MaterialApp(home: FirstPage(),); } class FirstPage extends StatefulWidget { @override State<StatefulWidget> createState() { return FirstThemeState(); } } class FirstThemeState extends State<FirstPage> { @override Widget build(BuildContext context) { //Scaffold 用来搭建页面的主体结构 return Scaffold( //页面的头部 appBar: AppBar(title: Text("标题"),), //页面的主内容区 //可以是单独的StatefulWidget,也可以是当前页面构建的,如Text文本组件 body: Center(child: Text("显示日期"),),); } } 图32启动页面 图33常用页面格局 Scaffold组件的属性appBar用来配置一个AppBar,用来构成页面的头部,类似于Android原生开发中的AppBar与ToolBar,以及类似于iOS原生开发中的UINavigationBar。 3.2FloatingActionButton的详细配置 FloatingActionButton悬浮按钮,简称FAB(下文中会使用简称),Scaffold组件中属性FloatingActionButton用来配置页面右下角的悬浮按钮功能,一般是一个圆形,中间有一个图标,悬浮按钮起源于Material Design设计理念的z轴概念,效果像是浮在页面之上,同Android原生开发中的FloatingActionButton,在Scaffold中配置FAB按钮,程序运行效果与原生开发一致,如图34所示,悬浮按钮在Flutter中的实现代码如下: //2.3 /lib/code2/main_data23.dart //Scaffold 悬浮按钮 import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; //应用入口 main() => runApp(themeDataFunction()); MaterialApp themeDataFunction() { return MaterialApp(home: FirstPage(),); } class FirstPage extends StatefulWidget { @override State<StatefulWidget> createState() { return FirstThemeState(); } } class FirstThemeState extends State<FirstPage> { @override Widget build(BuildContext context) { //Scaffold 用来搭建页面的主体结构 return Scaffold( //页面的头部 appBar: AppBar(title: Text("标题"),), //页面的主内容区 //可以是单独的StatefulWidget,也可以是当前页面构建的,如Text文本组件 body: Center(child: Text("显示日期"),), //悬浮按钮 floatingActionButton: FloatingActionButton( //一般建议使用Icon child: Icon(Icons.add), onPressed: () { print("单击了悬浮按钮"); },),); } } 图34悬浮按钮使用 3.2.1FloatingActionButton的类型 对于FloatingActionButton的属性child,它是用来配置按钮上显示子Widget的,能够配置使用的范畴比较大,源码中推荐使用Icon,但笔者认为诸如图34中所示效果,FAB适用于强调操作类型,一个图标简单而容易理解,在互联网产品设计中,简单直白而不复杂,也是一种思路。 FAB有3种类型: regular、mini、extended。regular在这里与mini是相对的,不同之处在于mini类型是缩小的版本,默认创建的FAB是regular类型的,创建的FAB的width和height都为56.0,可通过配置FAB的属性mini为true,将创建的FAB类型由默认的regular标准尺寸修改为mini类型,此时创建的FAB的width和height都为46.0,效果如图35所示,代码如下: //3.2.1 /lib/code2/main_data24.dart //FloatingActionButton的类型:regular类型 //此处通过new Object方式来创建的悬浮按钮为regular类型 FloatingActionButton buildFAB1(){ return FloatingActionButton( //一般建议使用Icon child: Icon(Icons.add), onPressed: () { print("单击了悬浮按钮"); },); } //3.2.1 FloatingActionButton的类型: mini类型 //此处通过new Object方式来创建的悬浮按钮为mini类型 FloatingActionButton buildFAB2() { return FloatingActionButton( //一般建议使用Icon child: Icon(Icons.add), onPressed: () { print("单击了悬浮按钮"); }, //默认是false mini: true,); } 图35悬浮按类型对比 对于extended类型,可以通过FloatingActionButton.extended方式来创建,也可以通过构造函数来创建,不过要指定属性isExtended的值为true,与前两者不同的是它可以指定一个Label来显示文本,效果如图36所示,同理也限制了子Widget为Icon类型,对应添加子Widget的方式已不是child,而是封装成icon属性,代码如下: //3.2.1 /lib/code2/main_data24.dart //FloatingActionButton的类型:extended类型 //通过 FloatingActionButton.extended方式来创建悬浮按钮 FloatingActionButton buildFAB3() { return FloatingActionButton.extended( //通过icon来配置显示的图标 icon: Icon(Icons.add), onPressed: () { print("单击了悬浮按钮"); }, //通过labe来添加文本信息 label: Text("添加信息"),); } 图36extended类型FAB 3.2.2FloatingActionButton的常用属性使用分析 属性tooltip用来配置FAB长按时的提示文本,如图37所示。 图37FAB的tooltip提示 图37所示效果对应代码如下: //3.2.2 /lib/code2/main_data24.dart //FloatingActionButton的常用属性使用分析 FloatingActionButton buildFAB4(){ return FloatingActionButton( child: Icon(Icons.add), onPressed: () { print("单击了悬浮按钮");}, tooltip: "这里是FAB!", ); } 属性backgroundColor用来配置FAB的背景色,splashColor用来配置FAB单击时的水波纹颜色,foregroundColor用来配置FAB的文字与图标中内容的颜色,如图38所示,代码如下: //3.2.2 /lib/code2/main_data25.dart //FloatingActionButton的常用属性使用分析:颜色配置 FloatingActionButton buildFAB5(){ return FloatingActionButton( child: Icon(Icons.add), //单击事件响应 onPressed: () {}, //背景色为红色 backgroundColor: Colors.red, //单击水波纹颜色为黄色 splashColor: Colors.yellow, //前景色为紫色 foregroundColor: Colors.deepPurple, ); } 图38FAB的颜色配置 对于颜色配置,这也属于主题配置的范畴,在第2章MaterialApp组件中提到默认主题配置ThemeData,为应变App中的多主题样式使用,笔者在这里建议还是在MaterialApp组件中通过FloatingActionButtonThemeData来配置FAB的使用样式,对于FAB来讲,MaterialApp配置对应代码如下: //3.2.2 /lib/code2/main_data26.dart //FloatingActionButton的常用属性使用分析,MaterialApp中主题配置 MaterialApp( home: FirstPage(), //主题配置 theme: ThemeData( //FAB悬浮按钮主题样式配置 floatingActionButtonTheme: FloatingActionButtonThemeData( //背景色为红色 backgroundColor: Colors.red, //单击水波纹颜色为黄色 splashColor: Colors.yellow, //前景色为紫色 foregroundColor: Colors.deepPurple, //默认显示下的阴影高度 elevation: 6.0, //点按下去时阴影的高度 highlightElevation: 10.0, //不可被单击时的阴影的高度 disabledElevation: 1.0, ), ),); 3.2.3FloatingActionButton的shape属性分析 shape用来配置FAB的形状,在FAB创建时,默认的形状是圆形的,默认使用的是CircleBorder,创建的是圆形的FAB,对于CircleBorder还可以设置边框,如图39所示,代码如下: //3.2.3 /lib/code2/main_data27.dart //FAB的shape属性分析,默认创建使用的shape ShapeBorder fabShapeBorder1() { //圆形 return CircleBorder( //配置边框 side: BorderSide( //边框颜色 color: Colors.blue, //边框的宽度 width: 4.0, //边框的样式,solid为实线,none为不显示边框 style: BorderStyle.solid), ); } 可以通过修改shape的值来修改FAB的形状,如图310所示,代码如下: //3.2.3 /lib/code2/main_data28.dart //FloatingActionButton的常用属性使用分析,shape属性分析 MaterialApp( home: FirstPage(), //主题配置 theme: ThemeData( ...//省略 //FAB悬浮按钮主题样式配置 floatingActionButtonTheme: FloatingActionButtonThemeData( ...//省略 //用来指定FAB的形状 shape:RoundedRectangleBorder(), ), ),); 图39设置边框的悬浮按钮 对于RoundedRectangleBorder圆角矩形来讲,也可以使用side来配置边框,使用方式与CircleBorder中的side配置的BorderSide一致,不过它还可以配置矩形四周的圆角,如图311所示,代码如下: //3.2.3 /lib/code2/main_data29.dart //FAB的shape属性分析,使用圆角矩形shape ShapeBorder fabShapeBorder2() { //圆形 return RoundedRectangleBorder( //设置四周的圆角 borderRadius: BorderRadius.circular(10), ); } 图310修改shape的悬浮按钮 borderRadius取值为BorderRadius.circular,可理解为将当前矩形的上下左右4个边角同时设置为圆角,对于圆角一般由x轴方向的一个半径和y轴方向的一个半径决定,circular指定的就是由这两个半径值保持一致所限制形成的圆角,如图311所示。 BorderRadius也可以单独指定一个角的圆角,如图312所示,代码如下: //3.2.3/lib/code2/main_data29.dart //FAB的shape属性分析,使用圆角矩形shape ShapeBorder fabShapeBorder3() { //圆形 return RoundedRectangleBorder( //设置左上角的圆角 borderRadius: BorderRadius.only(topLeft: Radius.elliptical(10, 20)), ); } 图311设置四周圆角 图312单独设置左上角圆角 对于Scaffold的属性floatingActionButtonLocation是用来配置FAB的位置的,Scaffold配置的FAB可选位置如图313所示。 图313FAB的位置说明 3.3Drawer配置侧拉页面 Scaffold的drawer用来配置页面左侧侧拉页面,属性endDrawer配置右侧侧拉页面,对于drawer来讲,它接收的是一个Widget,可以是一个容器,也可以是一个Text,还可以是一StatefulWidget,在实际项目开发中,侧拉页面一般是由一个ListView或是一个Column线性布局多个条目,如图314所示,当配置了Scaffold的drawer内容时,就会在AppBar的左侧多出一个菜单按钮,当单击这个按钮时,就会从左侧向右滑出一个侧拉页面,同时用户可从页面左侧向右侧滑动而触发出这个页面。 当配置了Scaffold的endDrawer属性时,在AppBar的右侧多出一个菜单按钮,同理单击时,会有一个页面从当前页面的右侧滑出来,用户也可以通过从当前页面右侧边缘向左滑动而将这个页面触发出来,代码如下: //3.3/lib/code2/main_data30.dart //drawer配置侧拉页面 import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; //应用入口 main() => runApp(themeDataFunction()); MaterialApp themeDataFunction() { return MaterialApp( home: FirstPage6(),); } class FirstPage6 extends StatefulWidget { @override State<StatefulWidget> createState() { return FirstThemeState(); } } class FirstThemeState extends State<FirstPage6> { @override Widget build(BuildContext context) { //Scaffold 用来搭建页面的主体结构 return Scaffold( //左侧侧拉页面 drawer:buildDrawer() , //右侧侧拉页面 endDrawer: buildDrawer(), //页面的头部 appBar: AppBar(title: Text("标题"),), //页面的主内容区 //可以是单独的StatefulWidget,也可以是当前页面构建的,如Text文本组件 body: Center(child: Text("显示日期"),), ); } //封装方法 Container buildDrawer(){ //Container可看作一个容器,用来包裹一些Widget return Container( //背景颜色 color: Colors.white, width: 200, //Column可以让子Widget在垂直方向线性排列 child: Column( children: <Widget>[ Container(color: Colors.blue,height: 200,child: Text("这是一个Text"),), Container(color: Colors.red,height: 200,child: Text("这是一个Text2"),), ], ), ); } } 图314配置侧拉页面 3.3.1用户信息组件UserAccountsDrawerHeader UserAccountsDrawerHeader从组件名字中就可得知此组件是专门用来配置侧拉页面中用户账号信息的,如图315所示,代码如下: //3.3 /lib/code2/main_data31.dart //drawer配置侧拉页面的UserAccountsDrawerHeader组件 Container buildDrawer2() { //Container可看作一个容器,用来包裹一些Widget return Container( //背景颜色 color: Colors.white, width: 200, //Column可以让子Widget在垂直方向线性排列 child: Column( children: <Widget>[ UserAccountsDrawerHeader( //显示的二级标题 accountEmail: Text('928***994@qq.com'), //显示的小标题 accountName: Text('这里是Drawer'), //小箭头单击响应 onDetailsPressed: () {}, //当前显示的背景图片 currentAccountPicture: CircleAvatar( child: Icon(Icons.message), ), ), ], ), ); } 图315用户信息组件的侧拉页面 UserAccountsDrawerHeader显示的布局样式是定义好的一个范式,在实际应用开发中似乎用得较少,因为很少有设计师会将用户的一个简介信息通过这样的方式展现出来,这个组件还有一个decoration属性,用来设置边框样式。 3.3.2DrawerHeader 在3.3.1节中,使用UserAccountsDrawerHeader来设置并显示侧拉页面中用户的简介信息,它的固定样式如图315所示,在实际开发中与设计师眼中的布局样式可能不相符合,此时,可通过DrawerHeader来构建一个自定义的页面,效果如图316所示,其属性child用来配置显示的内容,curve用来配置侧拉页面滑出的动画效果,代码如下: //3.3 /lib/code2/main_data32.dart //drawer配置侧拉页面,自定义的DrawerHeader Container buildDrawer() { //Container可看作一个容器,用来包裹一些Widget return Container( //背景颜色 color: Colors.white, width: 200, //Column可以让子Widget在垂直方向线性排列 child: Column( children: <Widget>[ DrawerHeader( //设置Header的上下左右内边距为0 padding: EdgeInsets.zero, //显示的具体内容 child: buildDrawerBody(), //侧拉页面滑出的动画效果,先快后慢 curve: Curves.fastOutSlowIn, ), ], ), ); } //Stack为帧布局页面,Widget可重合显示 Stack buildDrawerBody() { return Stack( children: <Widget>[ //可以用来设置背景图片 Container(color: Colors.grey,), //Align用来对齐组件 Align( //底部左对齐 alignment: FractionalOffset.bottomLeft, //设置用户的显示信息 child: Container( //底部外边距为12 margin: EdgeInsets.only(bottom: 12), child: Row( children: <Widget>[ SizedBox(width: 12,), //圆形的头像 CircleAvatar( child: Icon(Icons.message), ), SizedBox(width: 16,), Column( //使用Column线性布局包裹子Widget大小 mainAxisSize: MainAxisSize.min, children: <Widget>[ Text("这里是用户的姓名"), SizedBox(height: 8,), Text("这里是用户的简介"), ], ), ], ),),) ], ); } 图316自定义实现DrawerHeader 3.3.3单击按钮打开与关闭侧拉页面 在实际项目开发中,当配置了左侧拉页面时,如果要自定义AppBar默认打开侧拉页面的显示按钮,如图317所示需要配置AppBar中的leading属性,代码如下: //3.3.3 /lib/code2/main_data33.dart //单击按钮打开与关闭侧拉页面,修改默认AppBar的按钮配置 class FirstThemeState extends State<FirstPage6> { @override Widget build(BuildContext context) { //Scaffold 用来搭建页面的主体结构 return Scaffold( //左侧侧拉页面 drawer: buildDrawer(), //右侧侧拉页面 endDrawer: buildDrawer(), //页面的头部 appBar: AppBar(title: Text("标题"), leading: InkWell(onTap: (){ },child: Icon(Icons.add),),), //页面的主内容区 //可以是单独的StatefulWidget,也可以是当前页面构建的,如Text文本组件 body: Center(child: Text("显示日期"),), ); } 图317自定义leading按钮 当不需要显示这个按钮时,可将leading值赋为null。 当配置了leading自定义的按钮样式后,就需要在按钮的单击事件中配置打开侧拉页面的代码,代码如下: //打开左侧拉页面 Scaffold.of(context).openDrawer(); 当直接在配置的按钮中调用打开侧拉页面的方法时,有时会出报错信息,日志代码如下: ════════ Exception caught by gesture ════════ The following assertion was thrown while handling a gesture: Scaffold.of() called with a context that does not contain a Scaffold. 这里因为在Scaffold所配置的按钮中直接引用了context,这个context是由build方法传入的,与Scaffold是同一级别的,从源码中可以看到Scaffold的of方法会根据当前传入的context通过findAncestorStateOfType方法去找当前对应的ScaffoldState实例,找到后成功返回,找不到则抛出异常。 而在这个findAncestorStateOfType方法中,是从当前of方法中传入的context 的父节点开始查找的。而从build方法中传入的context的父节点中并没有ScaffoldState实例,因为Scaffold与build方法传入的context是在同一节点中。 解决方案是在Scaffold的AppBar的leading中再使用Builder组件包裹,然后再调用openDrawer方法,此时就可以正常打开侧拉页面了,代码如下: //3.3.3 /lib/code2/main_data34.dart //单击按钮打开与关闭侧拉页面,配置Scaffold.of(context) class FirstThemeState extends State<FirstPage6> { @override Widget build(BuildContext context) { //Scaffold 用来搭建页面的主体结构 return Scaffold( //禁用页面的侧滑效果 drawerEdgeDragWidth: 0, //左侧侧拉页面 drawer: buildDrawer(), //右侧侧拉页面 endDrawer: buildDrawer(), //页面的头部 appBar: AppBar(title: Text("标题"), //使用Builder包裹后,在Scaffold.of中使用Builder回调context //这个context的父级就是ScaffoldState了 leading: Builder(builder: (BuildContext context) { return InkWell(onTap: () { //打开左侧拉页面 Scaffold.of(context).openDrawer(); }, child: Icon(Icons.add),); }),), body: ...//省略 ); } 打开侧拉页面后,单击页面的遮罩层,侧拉页面会自动收缩回去,有时项目要求在页面中单击一个按钮,然后打开一个新的页面,在打开新的页面时,当前的侧拉页面要关闭,关闭侧拉页面的代码如下: Navigator.of(context).pop(); 在实际应用项目开发中,有时不需要通过左滑或者右滑将侧拉页面滑出来,这里就需要禁用,此时可通过配置Scaffold组件的drawerEdgeDragWidth为0来实现。 3.4BottomNavigationBar配置底部导航栏菜单 在Scaffold中,通过bottomNavigationBar来配置底部导航栏。结合BottomNavigationBar组件来实现,创建一个实战,效果如图318所示。代码如下: //3.3 /lib/code2/main_data35.dart //bottomNavigationBar配置底部导航栏菜单 import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; //应用入口 main() => runApp(MaterialApp( home: FirstPage(),),); class FirstPage extends StatefulWidget { @override State<StatefulWidget> createState() { return FirstThemeState(); } } class FirstThemeState extends State<FirstPage> { @override Widget build(BuildContext context) { //Scaffold 用来搭建页面的主体结构 return Scaffold( //页面的头部 appBar: AppBar(title: Text("标题"),), //页面的主内容区 //可以是单独的StatefulWidget,也可以是当前页面构建的,如Text文本组件 body: Center(child: Text("当前选中的页面是$_tabIndex"),), //底部导航栏 bottomNavigationBar: buildBottomNavigation(), ); } //选中的标签 int _tabIndex =0; //底部导航栏使用的图标 List<Icon> normalIcon = [ Icon(Icons.home), Icon(Icons.message), Icon(Icons.people) ]; //底部导航栏使用的标题文字 List<String> normalTitle =[ "首页", "消息", "我的" ]; //构建底部导航栏 BottomNavigationBar buildBottomNavigation(){ //创建一个 BottomNavigationBar return new BottomNavigationBar( items: <BottomNavigationBarItem>[ new BottomNavigationBarItem( icon: normalIcon[0], title: Text(normalTitle[0])), new BottomNavigationBarItem( icon:normalIcon[1], title: Text(normalTitle[1])), new BottomNavigationBarItem( icon:normalIcon[2], title: Text(normalTitle[2])), ], //显示效果 type: BottomNavigationBarType.fixed, //当前选中的页面 currentIndex: _tabIndex, //导航栏的背景颜色 backgroundColor: Colors.white, //固定图标与文字的颜色 //fixedColor: Colors.deepPurple, //选中时图标与文字的颜色 selectedItemColor: Colors.blue, //未选中时图标与文字的颜色 unselectedItemColor: Colors.grey, //图标的大小 iconSize: 24.0, //单击事件 onTap: (index) { setState(() { _tabIndex = index; }); }, ); } } 图318BottomNavigationBar导航栏 3.4.1items属性分析 BottomNavigationBar的属性items用来配置底部导航栏的每一个选项,它接收的是BottomNavigationBarItem组件类型,关于BottomNavigationBarItem的属性分析如表31所示。 表31BottomNavigationBarItem的属性 属性说明类型 icon默认显示的图标Widget activeIcon选中时显示的图标Widget title对应的标题Widget backgroundColorshifting时的背景颜色模式下的背景颜色Color 3.4.2type属性分析 BottomNavigationBar的type属性影响的是底部导航栏切换时的动画效果,源码代码如下: //3.4.2 type属性分析 enum BottomNavigationBarType { //The [BottomNavigationBar]'s [BottomNavigationBarItem]s have fixed width. fixed, //The location and size of the [BottomNavigationBar] [BottomNavigationBarItem]s //animate and labels fade in when they are tapped. shifting, } bottomNavigationBar默认配置的type是fixed,fixed限制的切换效果是在导航栏菜单切换时图标和文字标题会有微缩放的动画效果,而对于shifting来讲,切换动画效果更明显,在shifting模式下,只有当前选中的item的图标与文字才会显示出来,而未选中的item的标题文字是隐藏的,如图319所示。 图319shifting模式 3.4.3bottomNavigationBar结合独立的StatefulWidget使用 在图318所示效果中,页面body显示的只是一个Widget,在实际项目开发中,这里往往配置的是单独的页面,在这里笔者创建了3个页面,代码如下: //3.4.3/lib/code2/main_data36.dart //bottomNavigationBar结合独立的StatefulWidget使用,首页面 class ScaffoldHomeItemPage extends StatefulWidget { //页面标识 int pageIndex; //构造函数 ScaffoldHomeItemPage(this.pageIndex,{Key key}) : super(key: key); @override State<StatefulWidget> createState() { return ScaffoldHomeItemState(); } } //3.4.3/lib/code2/main_data36.dart class ScaffoldHomeItemState extends State<ScaffoldHomeItemPage> { //页面创建时初始化函数 @override void initState() { super.initState(); print("页面创建${widget.pageIndex}"); } @override Widget build(BuildContext context) { return Center(child: Text("当前页面标识为${widget.pageIndex}"), ); } //页面销毁时回调函数 @override void dispose() { super.dispose(); print("页面消失${widget.pageIndex}"); } } 第2个页面与第3个页面源码与第1个页面效果一样,然后在bottomNavigationBar中配置使用,代码如下: //3.4.3/lib/code2/main_data36.dart List<Widget> bodyWidgetList=[ ScaffoldHomeItemPage(0), ScaffoldHomeItemPage1(1), ScaffoldHomeItemPage2(2), ]; @override Widget build(BuildContext context) { //Scaffold 用来搭建页面的主体结构 return Scaffold( //页面的头部 appBar: AppBar(title: Text("标题"),), //页面的主内容区 //可以是单独的StatefulWidget,也可以是当前页面构建的,如Text文本组件 body:bodyWidgetList[_tabIndex], //底部导航栏 bottomNavigationBar: buildBottomNavigation(), ); } _tabIndex是底部导航栏当前选中的页面角标,每当底部导航栏进行切换时,调用setState方法进行页面刷新,在Scaffold的body中也会动态地从当前配置页面的bodyWidgetList中取出对应的页面进行渲染。 3.4.4bottomNavigationBar页面保活解决方案 在3.4.3节的实例中,默认显示的页面是_tabIndex为0首页面,然后依次单击“消息”、“我的”陆续加载第2个页面与第3个页面,body中的页面正常切换,如图320所示。 图320页面切换效果图 在Android Studio中的日志输出控制台可看到的日志如下: flutter: 页面创建0 flutter: 页面创建1 flutter: 页面消失0 flutter: 页面创建2 flutter: 页面消失1 从日志信息可以得出结论,在单击底部导航按钮进行页面切换时,当前配置的这几个页面会被销毁,在实际应用开发中,一般这种页面结构用在App的首页面,用户单击切换的频率相对高,所以每切换一次页面重新加载渲染一次,这种无论从设计方面还是从体验方面,以及从流量方面都相对不好,最好的结果就是这样的页面只创建一次就可以了。 为了实现页面只创建一次的程序设计,笔者在这里提出一个方案可以达到目的,首先Scaffold的body用来加载页面的主体信息,这个body加载一个帧布局Stack,接着将这3个页面一次性全部加载并创建出来,然后将这3个页面重叠显示在帧布局中,再结合透明控制组件Opacity将当前选中的页面透明度设置为1,从而显示出来,其他的页面透明度设置为0,用户不可见,运行程序,最后再进行3个页面重复单击底部导航栏进行切换,日志输入如下: Syncing files to device iPhone 11 Pro Max... flutter: 页面创建0 flutter: 页面创建1 flutter: 页面创建2 透明度结合帧布局确实实现了这一解决方案,代码如下: //3.4.4 /lib/code2/main_data40.dart //bottomNavigationBar页面保活解决方案 @override Widget build(BuildContext context) { //Scaffold 用来搭建页面的主体结构 return Scaffold( //页面的头部 appBar: AppBar(title: Text("标题"),), //页面的主内容区 body:buildBodyFunction(), //底部导航栏 bottomNavigationBar: buildBottomNavigation(), ); } Widget buildBodyFunction(){ //帧布局结合透明布局 return Stack(children: <Widget>[ Opacity(opacity: _tabIndex==0?1:0,child: ScaffoldHomeItemPage(0),), Opacity(opacity: _tabIndex==1?1:0,child: ScaffoldHomeItemPage(1),), Opacity(opacity: _tabIndex==2?1:0,child: ScaffoldHomeItemPage(2),), ],); } Opacity组件用来设置子Widget的透明度,属性opacity接收一个double值,取值范围为0.0~1.0,从透明到不透明,0.0代表透明,1.0代表不透明。 3.5BottomAppBar配置底部导航栏菜单 BottomAppBar 组件在Flutter中可用来配置底部导航菜单栏,同样可实现如图320所示的效果,代码如下: //3.5 /lib/code2/main_data37.dart //BottomAppBar配置底部导航栏菜单 @override Widget build(BuildContext context) { return Scaffold( //页面的头部 appBar: AppBar(title: Text("标题"),), //页面的主内容区 body:buildBodyFunction(), //底部导航栏 bottomNavigationBar: buildBottomNavigation(), ); } //构建底部导航栏 BottomAppBar buildBottomNavigation(){ return BottomAppBar( //底部导航栏的背景 color: Colors.white, //Row中的子Widget在水平方向非线性排列 child: Row( //使每一个子Widget平均分配Row的宽度 mainAxisAlignment: MainAxisAlignment.spaceAround, children: <Widget>[ buildBottomItem( 0, Icons.home, "首页"), buildBottomItem( 1, Icons.message, "消息"), buildBottomItem( 2, Icons.people, "我的"), ], ), ); } 对于buildBottomItem方法是用来构建底部导航菜单栏的图标与标题排列的,如图321所示,底部导航栏通过线性布局Column与Row结合构成。 图321底部导航栏结构说明 对于图标与标题的组件,代码如下: //3.5 /lib/code2/main_data37.dart //BottomAppBar配置底部导航栏菜单,图标与代码的组合 //[index]为每个条目对应的角标 //[iconData]为每个条目对应的图标 //[title]为每个条目对应的标题 buildBottomItem( int index, IconData iconData, String title) { //未选中状态的样式 TextStyle textStyle = TextStyle(fontSize: 12.0,color: Colors.grey); MaterialColor iconColor = Colors.grey; double iconSize=20; EdgeInsetsGeometry padding =EdgeInsets.only(top: 8.0); if(_tabIndex==index){ //选中状态的文字样式 textStyle = TextStyle(fontSize: 13.0,color: Colors.blue); //选中状态的按钮样式 iconColor = Colors.blue; //状态图标的大小 iconSize=25; padding =EdgeInsets.only(top: 6.0); } //上下竖直方向排列的图标与标题文字 Widget padItem = Padding( padding: padding, child: Container( color: Colors.white, child: Center( child: Column( children: <Widget>[ Icon( iconData, color: iconColor, size: iconSize, ), Text( title, style: textStyle, ) ], ), ), ), ); //Row中通过Expanded进行权重布局排列 Widget item = Expanded( flex: 1, child: new GestureDetector( onTap: () { if (index != _tabIndex) { setState(() { _tabIndex = index; }); } }, child: SizedBox( height: 52, child: padItem, ), ), ); return item; } 在Scaffold中,使用BottomAppBar与悬浮按钮FloatingActionButton结合使用,可以达到如图322所示页面效果,代码如下: //3.5.1 /lib/code2/main_data38.dart //BottomAppBar结合悬浮按钮使用,Scaffold中的配置 @override Widget build(BuildContext context) { //Scaffold 用来搭建页面的主体结构 return Scaffold( //页面的头部 appBar: AppBar(title: Text("标题"),), //页面的主内容区 body:buildBodyFunction(), //底部导航栏 bottomNavigationBar: buildBottomNavigation(), //悬浮按钮的位置 floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, //悬浮按钮 floatingActionButton: FloatingActionButton( child: const Icon(Icons.add), onPressed: () { print("add press ");}, ), ); } 这里需要注意的是悬浮按钮的位置需要配置为Docked模式,在对应的BottomAppBar组件中,也需要做相应的配置,代码如下: //3.5.1 /lib/code2/main_data38.dart //BottomAppBar结合悬浮按钮使用,BottomAppBar的配置 BottomAppBar buildBottomNavigation(){ return BottomAppBar( //悬浮按钮与其他菜单栏的结合方式 shape: CircularNotchedRectangle(), //FloatingActionButton和BottomAppBar 之间的差距 notchMargin: 6.0, //底部导航栏的背景 color: Colors.white, //Row中的子Widget在水平方向非线性排列 child: Row( //使每一个子Widget平均分配Row的宽度 mainAxisAlignment: MainAxisAlignment.spaceAround, children: <Widget>[ ...//省略 ], ), ); } 图322不规则导航栏 3.6底部标签栏bottomSheet Scaffold的属性bottomSheet用来配置底部固定的标签样式提示栏,如图323所示。 图323bottomSheet配置的标签样式提示栏 bottomSheet在这里接收的是一个Widget,也就是说可以配置任意的Widget,不过在实际项目开发中,如果使用了bottomSheet,一般配置的是一个水平方向线性排列的内容,如使用Row布局或者Stack帧布局等,代码如下: //3.6 /lib/code2/main_data39.dart //底部标签栏bottomSheet @override Widget build(BuildContext context) { //Scaffold 用来搭建页面的主体结构 return Scaffold( //页面的头部 appBar: AppBar(title: Text("标题"),), //页面的主内容区 body:buildBodyFunction(), //固定的标签栏 bottomSheet: Container( color: Colors.blue, height: 44, child: Row(children: <Widget>[ Container(margin: EdgeInsets.only(left: 10, right: 2), color: Colors.white, child: Text("标签1"),), Container(margin: EdgeInsets.only(left: 10, right: 2), color: Colors.white, child: Text("标签1"),), Container(margin: EdgeInsets.only(left: 10, right: 2), color: Colors.white, child: Text("标签1"),), Container(margin: EdgeInsets.only(left: 10, right: 2), color: Colors.white, child: Text("标签1"),), ],),), //底部导航栏 bottomNavigationBar: buildBottomNavigation(), ); } 小结 本章详细介绍了用来构建页面主体结构的Scaffold,笔者建议push的普通页面全部使用Scaffold来组装,一个独立的视图中建议只使用一个Scaffold,在PageView、TabBarView中会有多个子Widget,在PageView与TabBarView所在页面的Widget视图中使用Scaffold构建,其中在每个子项中建议不使用Scaffold。