前言
需求情景: 需要实现一个类似闲鱼APP的底部导航栏的实现,Flutter上实现方式有两种:
- BottomNavigationBar
- BottomAppBar
思路
不论用哪种方式实现,底部总共五个按钮,可以在底部平均分布5个同样大小的按钮,再在中间放一个FloatingActionButton,将floatingActionButtonLocation 设置为FloatingActionButtonLocation.centerDocked遮住中间的按钮,达到中部凸起的效果。
BottomNavigationBar实现
用BottomNavigationBar实现比较简单,代码如下:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_chip/utils/toast_utils.dart';
import 'view/view_home.dart';
import 'view/view_forum.dart';
import 'view/view_mine.dart';
import 'view/view_news.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(primarySwatch: Colors.deepOrange),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int lastTime = 0;
PageController pageController;
int page = 0;
@override
Widget build(BuildContext context) {
return WillPopScope(
child: Scaffold(
body: PageView(
children: <Widget>[
HomePage(),
ForumPage(),
NewsPage(),
MinePage()
],
onPageChanged: onPageChanged,
controller: pageController,
),
floatingActionButton: FloatingActionButton(
onPressed: () {},
child: Icon(
Icons.add,
color: Colors.white,
),
),
floatingActionButtonLocation:
FloatingActionButtonLocation.centerDocked,
bottomNavigationBar: BottomNavigationBar(
items: [
BottomNavigationBarItem(
icon: Icon(Icons.home), title: Text('首页')),
BottomNavigationBarItem(
icon: Icon(Icons.store), title: Text('论坛')),
BottomNavigationBarItem(
icon: Icon(Icons.home), title: Text('发布')),
BottomNavigationBarItem(
icon: Icon(Icons.home), title: Text('消息')),
BottomNavigationBarItem(
icon: Icon(Icons.home), title: Text('我的')),
],
type: BottomNavigationBarType.fixed,
fixedColor: Theme.of(context).primaryColor,
onTap: onTap,
currentIndex: page)
),
onWillPop: () {
int newTime = DateTime.now().millisecondsSinceEpoch;
int result = newTime - lastTime;
lastTime = newTime;
if (result > 2000) {
Toast.show(context, '再按一次退出');
} else {
SystemNavigator.pop();
}
return null;
});
}
@override
void initState() {
super.initState();
pageController = PageController(initialPage: this.page);
}
void onPageChanged(int value) {
if (value >= 2) {
value = value + 1;
}
setState(() {
this.page = value;
});
}
void onTap(int value) {
if (value == 2) return;
if (value > 2) {
value = value - 1;
}
pageController.animateToPage(value,
duration: const Duration(milliseconds: 100), curve: Curves.ease);
}
}
用BottomNavigationBar的好处就是按钮选中变化状态不用我们处理,组件已经封装好了,唯一需要处理的地方就是每个item按钮都对应一个页面,我们总共4个页面却对应5个按钮,所以在我们要去除中间按钮选中和点击的功能,那么就得在onTap和onPageChanged里面处理,不要忘记中间按钮图标色改成透明,免得不用尺寸屏幕把中间底部按钮露出来。实现效果如下:
BottomAppBar
用BottomAppBar的话导航栏每个图标都需要自己实现选中和未选中动态效果,还需要给每个按钮添加点击事件,比较繁琐。但是BottomAppBar比较灵活,可以实现很炫酷的效果,如下图:
代码如下:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_chip/utils/toast_utils.dart';
import 'view/view_home.dart';
import 'view/view_forum.dart';
import 'view/view_mine.dart';
import 'view/view_news.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(primarySwatch: Colors.deepOrange),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int lastTime = 0;
PageController pageController;
int page = 0;
@override
Widget build(BuildContext context) {
return WillPopScope(
child: Scaffold(
body: PageView(
children: <Widget>[HomePage(), ForumPage(), NewsPage(), MinePage()],
onPageChanged: onPageChanged,
controller: pageController,
),
floatingActionButton: FloatingActionButton(
onPressed: () {},
child: Icon(
Icons.add,
color: Colors.white,
),
),
floatingActionButtonLocation:
FloatingActionButtonLocation.centerDocked,
bottomNavigationBar: BottomAppBar(
color: Theme.of(context).accentColor,
shape: CircularNotchedRectangle(),
child: Padding(
padding: EdgeInsets.fromLTRB(0, 6, 0, 6),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
GestureDetector(
onTap: () {
onTap(0);
},
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Icon(Icons.home, color: getColor(0)),
Text("首页", style: TextStyle(color: getColor(0)))
],
)),
GestureDetector(
onTap: () {
onTap(1);
},
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Icon(Icons.forum, color: getColor(1)),
Text("论坛", style: TextStyle(color: getColor(1)))
],
)),
Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Icon(
Icons.home,
color: Colors.transparent,
),
Text("发布", style: TextStyle(color: Color(0xFFEEEEEE)))
],
),
GestureDetector(
onTap: () {
onTap(2);
},
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Icon(Icons.mail, color: getColor(2)),
Text("消息", style: TextStyle(color: getColor(2)))
],
)),
GestureDetector(
onTap: () {
onTap(3);
},
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Icon(Icons.person, color: getColor(3)),
Text("我的", style: TextStyle(color: getColor(3)))
],
))
],
),
),
),
),
onWillPop: () {
int newTime = DateTime.now().millisecondsSinceEpoch;
int result = newTime - lastTime;
lastTime = newTime;
if (result > 2000) {
Toast.show(context, '再按一次退出');
} else {
SystemNavigator.pop();
}
return null;
});
}
@override
void initState() {
super.initState();
pageController = PageController(initialPage: this.page);
}
void onPageChanged(int value) {
setState(() {
this.page = value;
});
}
Color getColor(int value) {
return this.page == value
? Theme.of(context).cardColor
: Color(0XFFBBBBBB);
}
void onTap(int value) {
pageController.animateToPage(value,
duration: const Duration(milliseconds: 100), curve: Curves.ease);
}
}
用Row组件放5个按钮品均分布,每个组件用Column包裹一个图标和一个文本,再放一个悬浮按钮在中间,shape: CircularNotchedRectangle()该属性使底部导航栏和悬浮按钮完美融合。
总结
对比两种实现效果,第一种中间按钮和底部导航栏没有融合,略显死板。第二种虽然代码多一些,但是实现效果很炫酷。个人推荐第二种写法!!!