情感语录:如果你想得到从未拥有过的东西,那么你必须去做从未做过的事情。
欢迎来到本章节,上一章节我们讲了路由
还记得吗?知识点回顾 戳这里 Flutter基础第五章
本章主要围绕底部导航条
,顶部导航条
以及自定义AppBar
和优化方案TabController
进行讲解,现在无论什么应用底部导航菜单和顶部导航菜单似乎都成了标配 ;有关这样的应用可以说是多不胜数”QQ、微信、头条、京东、饿了么等等…..”
1、BottomNavigationBar 底部导航条
2、 AppBar 自定义顶部样式
3、 TabBar 的样式配置
4、 TabController 的使用
BottomNavigationBar
是底部导航条,可以让我们定义底部 Tab 切换,BottomNavigationBar是 Scaffold
组件的参数。抛开IOS不说,因为我是做Android开发的,在Android原生中你没有封装有自己的底部导航栏菜单模块,你可能最常用的就是 Activity + RadioButton + Fragment的方式去实现了,这对于刚入Android的新手来说,可能并没那么好切入,因为我在技术群里看到好多初学者都在问这样的问题。而在Flutter中这将变得轻松自如。
构造函数如下:
BottomNavigationBar({
Key key,
@required this.items,
this.onTap,
this.currentIndex = 0,
this.elevation = 8.0,
BottomNavigationBarType type,
Color fixedColor,
this.backgroundColor,
this.icOnSize= 24.0,
Color selectedItemColor,
this.unselectedItemColor,
this.selectedIcOnTheme= const IconThemeData(),
this.unselectedIcOnTheme= const IconThemeData(),
this.selectedFOntSize= 14.0,
this.unselectedFOntSize= 12.0,
this.selectedLabelStyle,
this.unselectedLabelStyle,
this.showSelectedLabels = true,
bool showUnselectedLabels,
})
items List 底部导航条按钮集合
iconSize 设置icon大小
currentIndex 默认选中第几个
onTap 选中变化回调函数
fixedColor 选中的颜色
type BottomNavigationBarType.fixed (固定类型)
BottomNavigationBarType.shifting (流动类型)
selectedLabelStyle 选中时文本的样式
unselectedLabelStyle 未选中时文本的样式
selectedFontSize 选中时文本的大小
unselectedFontSize 未选中时文本的大小
selectedItemColor 选择中的颜色和 fixedColor互斥
unselectedItemColor 未选择中的颜色
showUnselectedLabels 是否显示未选中的文本标签
BottomNavigationBar的属性都大致了解了,下面我们来结合案例去实践,感受它妖娆的魅力吧O(∩_∩)O
本案例不会再按照以前的写法,把代码都写在main.dart 程序入口中,方便后面的文章写案例和查阅源码学习,在本章开始进行代码抽离。
案例要求: 利用上面的 BottomNavigationBar 组件实现类似微信手机端的底部菜单导航,点击底部导航菜单可以进行页面之间的切换。
首先我们先准备三个页面,主要用于点击底部导航菜单按钮,显示不同的页面效果,内容随便写点,有区分即可:
import 'package:flutter/material.dart';
class HomePage extends StatefulWidget {
HomePage({Key key}) : super(key: key);
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State {
@override
Widget build(BuildContext context) {
return Container(
color: Colors.cyan,
child: Center(child: Text("主页"),),
);
}
}
import 'package:flutter/material.dart';
class CommunityPage extends StatefulWidget {
CommunityPage({Key key}) : super(key: key);
_CommunityPageState createState() => _CommunityPageState();
}
class _CommunityPageState extends State {
@override
Widget build(BuildContext context) {
return Container(
color: Colors.green,
child: Center(child: Text("社区"),),
);
}
}
import 'package:flutter/material.dart';
class PersionPage extends StatefulWidget {
PersionPage({Key key}) : super(key: key);
_PersionPageState createState() => _PersionPageState();
}
class _PersionPageState extends State {
@override
Widget build(BuildContext context) {
return Container(
color: Colors.blue,
child: Center(child: Text("个人中心"),),
);
}
}
StatefulWidget
,让Tabs类继承StatefulWidget: import 'package:flutter/material.dart';
//引入准备好的界面
import 'tabs/CommunityPage.dart';
import 'tabs/PersionPage.dart';
import 'tabs/HomePage.dart';
class Tabs extends StatefulWidget {
_TabsState createState() => _TabsState();
}
class _TabsState extends State {
//默认选中的索引
int _currentIndex=0;
//将页面封装在集合中,方便通过索引取出
List _pageList=[
HomePage(),
CommunityPage(),
PersionPage(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("呆萌"),
),
body: this._pageList[this._currentIndex],
bottomNavigationBar: BottomNavigationBar(
//配置选中的索引值
currentIndex: this._currentIndex,
onTap: (int index){
//改变状态
setState(() {
this._currentIndex=index;
});
},
//icon的大小
iconSize:36.0,
//选中的颜色
fixedColor:Colors.red,
selectedFontSize: 18,
selectedLabelStyle: TextStyle(color: Colors.red),
// selectedItemColor: Colors.red, 和 fixedColor互斥
unselectedFontSize: 18,
unselectedItemColor:Colors.deepPurple ,
unselectedLabelStyle: TextStyle(color: Colors.deepPurple),
showUnselectedLabels: true,
//配置底部tabs为固定效果,即没有点击后的凸出感
type:BottomNavigationBarType.fixed,
// BottomNavigationBarItem 包装的底部按钮
items: [
BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text("首页")
),
BottomNavigationBarItem(
icon: Icon(Icons.question_answer),
title: Text("社区")
),
BottomNavigationBarItem(
icon: Icon(Icons.people),
title: Text("个人中心")
)
],
),
);
}
}
Tabs类中并没什么难点,部分也给出了介绍,值得注意的是 这里在点击事件 onTap
中我们接收了一个点击后的索引值 并通过调用 setState()
方法去改变了当前的索引值和界面的重新 build
渲染视图,然后根据变化后的索引值重新在 _pageList 页面集合中取出对应的页面并赋值给 body
,然而这就实现了页面切换效果的过程,第四章知识点还记得吗?戳这里回顾 StatefulWidget
import 'package:flutter/material.dart';
import 'package:flutter_learn/page/Tabs.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home:Tabs(), // 返回 Tabs包装的菜单Widget
);
}
}
运行效果见下图,还是挺简单的吧O(∩_∩)O
AppBar中的属性较多,构造函数如下:
AppBar({
Key key,
this.leading,
this.automaticallyImplyLeading = true,
this.title,
this.actions,
this.flexibleSpace,
this.bottom,
this.elevation,
this.shape,
this.backgroundColor,
this.brightness,
this.iconTheme,
this.actionsIconTheme,
this.textTheme,
this.primary = true,
this.centerTitle,
this.titleSpacing = NavigationToolbar.kMiddleSpacing,
this.toolbarOpacity = 1.0,
this.bottomOpacity = 1.0,
})
属性 描述
leading 在标题前面显示的一个控件,在首页通常显示应用
的 logo;在其他界面通常显示为返回按钮
title 标题,通常显示为当前界面的标题文字,可以放组件
actions 通常使用 IconButton 来表示,可以放按钮组
bottom 通常放 tabBar,标题下面显示一个 Tab 导航栏
backgroundColor 导航背景颜色
iconTheme 图标样式
textTheme 文字样式
centerTitle 标题是否居中显示
AppBar在之前的案例中一直都有使用只是没有过多使用它的属性而已,现在介绍了,写个简单的页面来练习下。这里先不在上个案例中进行配置,新建一个 AppBarPage界面。
import 'package:flutter/material.dart';
class AppBarPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
//标题
title: Text("呆萌"),
//设置Bar背景色
backgroundColor: Colors.red,
//标题居中
centerTitle: true,
leading: Builder(
builder: (BuildContext context) {
return IconButton(
icon: Icon(Icons.menu),
onPressed: () {
Scaffold.of(context)
.showSnackBar(SnackBar(content: new Text('点击了左侧按钮')));
},
);
},
),
actions: [
IconButton(
icon: Icon(Icons.search ,color: Colors.white,),
),
IconButton(
icon: Icon(Icons.settings,color: Colors.white,),
)
],
),
body: Center(
child: Text(
'这是AppBar测试界面',
style: TextStyle(
color: Colors.green,
fontSize: 22,
),
),
),
);
}
}
首先是在AppBar左边leading
添加了一个 IconButton 并显示了一个菜单Icon,在点击的时候 弹出个SnackBar提示信息,SnackBar这里不具体讲,这个会在后面章节中详细介绍,需要注意的是当BuildContext在Scaffold之前时,调用Scaffold.of(context)会报错。这时可以通过Builder Widget来解决这个问题。其次通过actions
添加了一组图片按钮,而actions接收的是一个List 集合,所以这里你可以放多个组件。
接下来配置路由,使得跳转可以到我们刚新建的AppBarPage页面中去。路由还记得吗?戳这里回顾 路由
//配置路由
final routes={
//如有多个,请在这里添加即可。
'/SecondPage':(context,{arguments})=>SecondPage(arguments:arguments),
//让AppBarPage 做为launcher界面
'/':(context)=>AppBarPage(),
};
为了方便观看 AppBar 中添加的组件,这里使用 debugShowCheckedModeBanner:false
把 debug 图标进行去掉。并在入口类中初始路由的时候去加载我们的 AppBarPage 页面
import 'package:flutter/material.dart';
import 'route/Routes.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
//去掉debug图标
debugShowCheckedModeBanner:false ,
//初始化的时候加载的路由
initialRoute: '/',
onGenerateRoute: onGenerateRoute
);
}
}
运行看看效果吧:
效果还是不错吧,debug 图标也被我们成功的去掉了,这只是模拟器,在真设备上显示效果应该会更好,有没有觉得比原生实现起来简单很多呢?
在Flutter中可以通过 TabBar 组件可以轻松实现顶部导航条,如不使用 TabBar 组件也是可以,比如通过Row组件去嵌套也是可以的,只是实现起来相对复杂很多。
构造函数如下:
const TabBar({
Key key,
@required this.tabs,
this.controller,
this.isScrollable = false,
this.indicatorColor,
this.indicatorWeight = 2.0,
this.indicatorPadding = EdgeInsets.zero,
this.indicator,
this.indicatorSize,
this.labelColor,
this.labelStyle,
this.labelPadding,
this.unselectedLabelColor,
this.unselectedLabelStyle,
this.dragStartBehavior = DragStartBehavior.start,
this.onTap,
})
属性 描述
tabs 显示的标签内容,一般使用 Tab 对象,也可以是其他的Widget
controller TabController 对象
isScrollable 是否可滚动
indicatorColor 指示器颜色
indicatorWeight 指示器高度
indicatorPadding 底部指示器的 Padding
indicator 指示器 decoration,例如边框等
indicatorSize 指示器大小计算方式,TabBarIndicatorSize.label 跟文字等宽,
TabBarIndicatorSize.tab 跟每个 tab 等宽
labelColor 选中 label 颜色
labelStyle 选中 label 的 Style
labelPadding 每个 label 的 padding 值
unselectedLabelColor 未选中 label 颜色
unselectedLabelStyle 未选中 label 的 Style
在上面介绍AppBar中有一个bottom
属性刚好接收的就是 Widget 组件,TabBar也是一个组件,瞌睡遇到枕头啊!! 下面我们就来创建一个TabBar 组件将其插入。
import 'package:flutter/material.dart';
class AppBarPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return DefaultTabController(
//这个一定的和tabs的子元素大小一致
length: 3,
child: Scaffold(
appBar: AppBar(
//标题
title: Text("呆萌"),
//设置Bar背景色
backgroundColor: Colors.red,
//标题居中
centerTitle: true,
bottom: TabBar(
//配置指示器颜色
indicatorColor: Colors.green,
//让指示器和文字等宽
indicatorSize: TabBarIndicatorSize.label,
// 导航Tab
tabs: [
Tab(text: "视频"),
Tab(text: "段子"),
Tab(text: "趣图")],
),
leading: Builder(
builder: (BuildContext context) {
return IconButton(
icon: Icon(Icons.menu),
onPressed: () {
Scaffold.of(context)
.showSnackBar(SnackBar(content: new Text('点击了左侧按钮')));
},
);
},
),
actions: [
IconButton(
icon: Icon(
Icons.search,
color: Colors.white,
),
),
IconButton(
icon: Icon(
Icons.settings,
color: Colors.white,
),
)
],
),
body: Container(child: Center(child: Text("测试TabBar"),),),
),
);
}
}
这段代码中有个非常重要的配置 DefaultTabController
其中 length
的大小一定等于 TabBar 中 tabs
的子元素数量,否则报错。而 tabs 接收的是一个List 集合,在实际开发中,length往往需要动态配置。
可以看到我们实现了顶部菜单导航条,你可能马上会问,顶部导航是实现了,但我想点击顶部的菜单按钮后让下面空白区域跟随着进行页面联动,那该怎样实现呢? 想想下,Flutter中一个页面也是Widget,那我能不能给每个导航按钮绑定一个页面呢? 点击每个按钮就显示绑定的页面Widget即可!!
嘿嘿,确实是可以的,Flutter中也是这么做的并提供了 TabBarView
组件让我们去实现。
TabBarView构造函数:
const TabBarView({
Key key,
@required this.children,
this.controller,
this.physics,
this.dragStartBehavior = DragStartBehavior.start,
})
TabBarView中 children
属性接收的也是一个List 那便不难看出:TabBar中的 tabs 的子元素和 TabBarView 中的子元素通过下标
进行了一 一绑定。也就是说导航菜单有多少个按钮,TabBarView中就要准备多少个页面组件。基于这个原理我们把上面案例中的 body
替换成如下
body: TabBarView(
children: [
Container(
child: Center(
child: Text("视频专区"),
),
),
Container(
child: Center(
child: Text("段子专区"),
),
),
Container(
child: Center(
child: Text("趣图专区"),
),
)
],
),
运行试看下效果:
果不其然,确实这样就实现了联动。是不是很激动?下面我来看一种情况:我们将顶部导航菜单增加到 7 个 甚至更多看看是个什么效果,添加之后注意修改 length
的长度 :
// 导航Tab
tabs: [
Tab(text: "视频"),
Tab(text: "段子"),
Tab(text: "趣图"),
Tab(text: "测试"),
Tab(text: "测试"),
Tab(text: "测试"),
Tab(text: "测试"),
],
尼玛啊! 菜单被逐渐覆盖了,也就是说当前一屏的宽度不能够足以均分给这么多个导航菜单,导致了按钮不能完全显示出来!!那能不能让它能够自适应,如果有很多菜单,就让它能左右滑动呢? 当然可以 上面TabBar属性介绍中 isScrollable
就能让 TabBar 实现可以左右滑动。下面添加一个属性试试。
import 'package:flutter/material.dart';
class AppBarPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
//去掉debug图标
debugShowCheckedModeBanner:false ,
home: DefaultTabController(
//这个一定的和tabs的子元素大小一致
length: 7,
child: Scaffold(
appBar: AppBar(
//标题
title: Text("呆萌"),
//设置Bar背景色
backgroundColor: Colors.red,
//标题居中
centerTitle: true,
bottom: TabBar(
//设置选中菜单文字颜色
labelColor: Colors.white,
//未选中文字菜单的颜色
unselectedLabelColor: Colors.deepPurple,
isScrollable: true,
//配置指示器颜色
indicatorColor: Colors.green,
//让指示器和文字等宽
indicatorSize: TabBarIndicatorSize.label,
// 导航Tab
tabs: [
Tab(text: "视频"),
Tab(text: "段子"),
Tab(text: "趣图"),
Tab(text: "测试"),
Tab(text: "测试"),
Tab(text: "测试"),
Tab(text: "测试"),
],
),
leading: Builder(
builder: (BuildContext context) {
return IconButton(
icon: Icon(Icons.menu),
onPressed: () {
Scaffold.of(context)
.showSnackBar(SnackBar(content: new Text('点击了左侧按钮')));
},
);
},
),
actions: [
IconButton(
icon: Icon(
Icons.search,
color: Colors.white,
),
),
IconButton(
icon: Icon(
Icons.settings,
color: Colors.white,
),
)
],
),
body: TabBarView(
children: [
Container(
child: Center(
child: Text("视频专区"),
),
),
Container(
child: Center(
child: Text("段子专区"),
),
),
Container(
child: Center(
child: Text("趣图专区"),
),
),
Container(
child: Center(
child: Text("趣图专区"),
),
),
Container(
child: Center(
child: Text("趣图专区"),
),
),
Container(
child: Center(
child: Text("趣图专区"),
),
),
Container(
child: Center(
child: Text("趣图专区"),
),
),
],
),
),
),
);
}
}
效果如下:
特别需要注意的是在不知道顶部菜单具体有多少个的时候(尤其是菜单是根据服务器获取的情况)或者顶部有很多个菜单需要放置时一定得加上 isScrollable
属性。
此时你可能会说,感觉这样的方式实现好复杂,有没有像底部导航菜单那样更简单高效的实现方案啊? 当然是有的,请继续往下看。
三、TabController
上面的例子中 DefaultTabController 组件实现了AppBar里面的顶部导航切换,但是在项目中有数据请求,上拉加载更多等操作的时候,或者想在统一回调函数中处理点击菜单事件的监听,那么前面的写法,就不是很方便操作,因此,在flutter里面,还提供了一个用于实现顶部导航的组件 TabController
。
新建一个TabBarController.dart 页面 ,并继承 StatefulWidget 的有状态组件,并且实现 SingleTickerProviderStateMixin
这个类,这里用到了Dart 中的 mixin
。你可以简单的理解就是通过 with
实现了多继承。
import 'package:flutter/material.dart';
class TabBarControllerPage extends StatefulWidget {
TabBarControllerPage({Key key}) : super(key: key);
_TabBarControllerPageState createState() => _TabBarControllerPageState();
}
class _TabBarControllerPageState extends State
with SingleTickerProviderStateMixin {
TabController _tabController;
@override
void dispose() {
super.dispose();
//在组件树中移除时销毁_tabController
_tabController.dispose();
}
@override
void initState() {
super.initState();
_tabCOntroller= new TabController(vsync: this, length: 4);
//监听点击事件的回调
_tabController.addListener(() {
print( getView()[_tabController.index].key.toString());
});
}
List getView() {
List viewWidget = new List();
//配置下key 方便日子看出 是否回调了 对应的菜单
Tab tab = Tab(key: Key("video"), text: "视频");
Tab tab1 = Tab(key: Key("Duan Zi"), text: "段子");
Tab tab2 = Tab(key: Key("Pic"), text: "趣图");
Tab tab3 = Tab(key: Key("Test"), text: "测试");
viewWidget.add(tab);
viewWidget.add(tab1);
viewWidget.add(tab2);
viewWidget.add(tab3);
return viewWidget;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("呆萌"),
bottom: TabBar(
//根据你的场景进行配置
// isScrollable: true,
//指示器颜色
indicatorColor: Colors.red,
//设置选中菜单文字颜色
labelColor: Colors.white,
//未选中文字菜单的颜色
unselectedLabelColor: Colors.yellowAccent,
//使得 TabBar 和 TabBarView之间进行关联,需要同时配置该对象 _tabController
controller: this._tabController,
tabs: getView()),
),
body: TabBarView(
controller: this._tabController,
children: [
Container(
child: Center(
child: Text("视频专区"),
),
),
Container(
child: Center(
child: Text("段子专区"),
),
),
Container(
child: Center(
child: Text("趣图专区"),
),
),
Container(
child: Center(
child: Text("测试专区"),
),
),
],
),
);
}
}
特别需要注意的是 在 TabController
依然是需要指定 tabs
的长度,且 TabBar
和 TabBarView
产生关联关系都是通过 controller
属性进行配置。
重新配置初始化路由到 TabBarControllerPage 界面,下面我们来运行看下效果。
点击切换菜单按钮后控制台输出结果:
I/flutter ( 2731): []
I/flutter ( 2731): []
I/flutter ( 2731): []
I/flutter ( 2731): []
I/flutter ( 2731): []
I/flutter ( 2731): []
I/flutter ( 2731): []
I/flutter ( 2731): []
I/flutter ( 2731): []
I/flutter ( 2731): []
I/flutter ( 2731): []
在回调中确实能获取到点击菜单按钮的后的下标,并通过下标取出了对应页面的Key值信息。在案例中明显可以看出 通过 TabController 方式去实现顶部导航菜单会更加简洁且灵活,因此开发中这样的需求我们应该多使用这种方式去实现。
好了本章节就此结束,又到了说再见的时候了,如果你喜欢请留下你的小红星,你们的支持才是创作的动力,如有错误,请热心的你留言指正, 谢谢大家观看,下章再会 O(∩_∩)O
实例源码地址: https://github.com/zhengzaihong/flutter_learn