前言
底部导航,对于做app的同学再熟悉不过了,不过有时候设计为了突出别样的风格,就会来点不一样的,比如下面的效果图。我们使用系统提供的控件,是无法实现的。在看了系统提供的源码后,发现也不是太复杂,于是乎,自己动手实现一个,下面的代码复制过去,改改资源文件是直接可以用的。
效果图
定义我们需要的主题
class BottomBarItem {
BottomBarItem(
{required this.icon,
required this.activeIcon,
required this.title,
this.activeColor = Colors.blue,
this.inactiveColor,
this.textAlign = TextAlign.center,
this.singleIcon = false});
final Widget icon;///未选中的状态
final Widget activeIcon;///选中的状态
final Widget title;///标题
final Color activeColor;///选中的颜色
final Color? inactiveColor;///未选中的颜色
final TextAlign textAlign;
final bool singleIcon;///是否只包含icon
}
定义BottomNavigatorBar
///作者 : Pig Huitao
///时间 : 2022/1/6
///邮箱 : pig.huitao@gmail.com
class BottomNavigatorBar extends StatelessWidget {
const BottomNavigatorBar(
{Key? key,
this.selectedIndex = 0,
this.iconSize = 24,
this.backgroundColor,
this.showElevation = true,
this.animationDuration = const Duration(milliseconds: 270),
required this.items,
required this.onItemSelected,
this.mainAxisAlignment = MainAxisAlignment.spaceBetween,
this.itemCornerRadius = 50,
this.containerHeight = 56,
this.curve = Curves.linear,
this.singleIcon = false})
: super(key: key);
final int selectedIndex;
final double iconSize;
final Color? backgroundColor;
final bool showElevation;
final Duration animationDuration;
final List<BottomBarItem> items;
final ValueChanged<int> onItemSelected;
final MainAxisAlignment mainAxisAlignment;
final double itemCornerRadius;
final double containerHeight;
final Curve curve;
final bool singleIcon;
@override
Widget build(BuildContext context) {
final bgColor = backgroundColor ?? Theme.of(context).bottomAppBarColor;
return Container(
decoration: BoxDecoration(
color: bgColor,
boxShadow: [
if (showElevation)
const BoxShadow(
color: Colors.black12,
blurRadius: 2,
)
],
),
child: SafeArea(
child: Container(
width: double.infinity,
height: containerHeight,
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 20),
child: Row(
mainAxisAlignment: mainAxisAlignment,
children: items.map((e) {
var index = items.indexOf(e);
return GestureDetector(
onTap: () => onItemSelected(index),
child: _ItemWidget(
iconSize: iconSize,
isSelected: index == selectedIndex,
item: e,
backgroundColor: bgColor,
itemCornerRadius: itemCornerRadius,
animationDuration: animationDuration,
curve: curve,
),
);
}).toList(),
),
),
),
);
}
}
最后的ImteWidget
class _ItemWidget extends StatelessWidget {
const _ItemWidget(
{Key? key,
required this.iconSize,
required this.isSelected,
required this.item,
required this.backgroundColor,
required this.itemCornerRadius,
required this.animationDuration,
this.curve = Curves.linear,
this.singIcon = false})
: super(key: key);
final double iconSize;
final bool isSelected;
final BottomBarItem item;
final Color backgroundColor;
final double itemCornerRadius;
final Duration animationDuration;
final Curve curve;
final bool singIcon;
@override
Widget build(BuildContext context) {
return Semantics(
container: true,
selected: isSelected,
child: AnimatedContainer(
height: double.maxFinite,
duration: animationDuration,
curve: curve,
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(itemCornerRadius),
),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
physics: NeverScrollableScrollPhysics(),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 8),
alignment: Alignment.center,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildIcon(isSelected, item),
const SizedBox(
height: 2,
),
if (!item.singleIcon)
Expanded(
child: Container(
padding: EdgeInsets.symmetric(horizontal: 4),
child: DefaultTextStyle.merge(
child: item.title,
style: _textStyle(isSelected, item)),
))
],
)),
),
),
);
}
TextStyle _textStyle(bool isSelected, BottomBarItem item) {
///返回文字样式
if (isSelected) {
return TextStyle(
color: item.activeColor, fontSize: 12);
} else {
return TextStyle(
color: item.inactiveColor,
fontSize: 12);
}
}
Widget _buildIcon(bool isSelected, BottomBarItem item) {
///根据选中的state,返回不同的icon
if (isSelected) {
return Expanded(child: item.activeIcon);
} else {
return Expanded(child: item.icon);
}
}
}
完整的BottomNavigatorBar
///作者 : Pig Huitao
///时间 : 2022/1/6
///邮箱 : pig.huitao@gmail.com
class BottomNavigatorBar extends StatelessWidget {
const BottomNavigatorBar(
{Key? key,
this.selectedIndex = 0,
this.iconSize = 24,
this.backgroundColor,
this.showElevation = true,
this.animationDuration = const Duration(milliseconds: 270),
required this.items,
required this.onItemSelected,
this.mainAxisAlignment = MainAxisAlignment.spaceBetween,
this.itemCornerRadius = 50,
this.containerHeight = 56,
this.curve = Curves.linear,
this.singleIcon = false})
: super(key: key);
final int selectedIndex;
final double iconSize;
final Color? backgroundColor;
final bool showElevation;
final Duration animationDuration;
final List<BottomBarItem> items;
final ValueChanged<int> onItemSelected;
final MainAxisAlignment mainAxisAlignment;
final double itemCornerRadius;
final double containerHeight;
final Curve curve;
final bool singleIcon;
@override
Widget build(BuildContext context) {
final bgColor = backgroundColor ?? Theme.of(context).bottomAppBarColor;
return Container(
decoration: BoxDecoration(
color: bgColor,
boxShadow: [
if (showElevation)
const BoxShadow(
color: Colors.black12,
blurRadius: 2,
)
],
),
child: SafeArea(
child: Container(
width: double.infinity,
height: containerHeight,
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 20),
child: Row(
mainAxisAlignment: mainAxisAlignment,
children: items.map((e) {
var index = items.indexOf(e);
return GestureDetector(
onTap: () => onItemSelected(index),
child: _ItemWidget(
iconSize: iconSize,
isSelected: index == selectedIndex,
item: e,
backgroundColor: bgColor,
itemCornerRadius: itemCornerRadius,
animationDuration: animationDuration,
curve: curve,
),
);
}).toList(),
),
),
),
);
}
}
class _ItemWidget extends StatelessWidget {
const _ItemWidget(
{Key? key,
required this.iconSize,
required this.isSelected,
required this.item,
required this.backgroundColor,
required this.itemCornerRadius,
required this.animationDuration,
this.curve = Curves.linear,
this.singIcon = false})
: super(key: key);
final double iconSize;
final bool isSelected;
final BottomBarItem item;
final Color backgroundColor;
final double itemCornerRadius;
final Duration animationDuration;
final Curve curve;
final bool singIcon;
@override
Widget build(BuildContext context) {
return Semantics(
container: true,
selected: isSelected,
child: AnimatedContainer(
height: double.maxFinite,
duration: animationDuration,
curve: curve,
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(itemCornerRadius),
),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
physics: NeverScrollableScrollPhysics(),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 8),
alignment: Alignment.center,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildIcon(isSelected, item),
const SizedBox(
height: 2,
),
if (!item.singleIcon)
Expanded(
child: Container(
padding: EdgeInsets.symmetric(horizontal: 4),
child: DefaultTextStyle.merge(
child: item.title,
style: _textStyle(isSelected, item)),
))
],
)),
),
),
);
}
TextStyle _textStyle(bool isSelected, BottomBarItem item) {
///返回文字样式
if (isSelected) {
return TextStyle(
color: item.activeColor, fontSize: 12);
} else {
return TextStyle(
color: item.inactiveColor,
fontSize: 12);
}
}
Widget _buildIcon(bool isSelected, BottomBarItem item) {
///根据选中的state,返回不同的icon
if (isSelected) {
return Expanded(child: item.activeIcon);
} else {
return Expanded(child: item.icon);
}
}
}
class BottomBarItem {
BottomBarItem(
{required this.icon,
required this.activeIcon,
required this.title,
this.activeColor = Colors.blue,
this.inactiveColor,
this.textAlign = TextAlign.center,
this.singleIcon = false});
final Widget icon;///未选中的状态
final Widget activeIcon;///选中的状态
final Widget title;///标题
final Color activeColor;///选中的颜色
final Color? inactiveColor;///未选中的颜色
final TextAlign textAlign;
final bool singleIcon;///是否只包含icon
}
使用
///作者 : Pig Huitao
///时间 : 2022/1/5
///邮箱 : pig.huitao@gmail.com
class MainPage extends StatefulWidget {
const MainPage({Key? key}):super(key: key);
@override
_MainPage createState() => _MainPage();
}
class _MainPage extends State<MainPage>
with SingleTickerProviderStateMixin {
int _currentIndex = 0;
final _inactiveColor = Colors.grey;
final _activeColor = Colors.blue;
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('自定义底部导航'),
),
body: getBody(),
bottomNavigationBar: _buildBottomBar(),
);
}
Widget _buildBottomBar() {
return BottomNavigatorBar(
containerHeight: 55,
backgroundColor: Colors.white,
selectedIndex: _currentIndex,
showElevation: true,
itemCornerRadius: 24,
curve: Curves.easeIn,
onItemSelected: (index) => setState(() => _currentIndex = index),
items: <BottomBarItem>[
BottomBarItem(
icon: const Image(
image: AssetImage('assets/images/icon_home_unselected.png'),
fit: BoxFit.fill,
),
activeIcon: const Image(
image: AssetImage('assets/images/icon_home_selected.png'),
fit: BoxFit.fill,
),
title: const Text('首页'),
activeColor: Colors.blue,
inactiveColor: _inactiveColor,
textAlign: TextAlign.center,
),
BottomBarItem(
icon: const Image(
image: AssetImage('assets/images/icon_circle_unselected.png'),
fit: BoxFit.fill,
),
activeIcon: const Image(
image: AssetImage('assets/images/icon_circle_selected.png'),
fit: BoxFit.fill,
),
title: const Text('圈子'),
activeColor: _activeColor,
inactiveColor: _inactiveColor,
textAlign: TextAlign.center,
),
BottomBarItem(
icon: const Image(
width: 60,
height: 38,
image: AssetImage('assets/images/icon_publish.png'),
fit: BoxFit.fill,
),
activeIcon: const Image(
width: 60,
height: 38,
image: AssetImage('assets/images/icon_publish.png'),
fit: BoxFit.fill,
),
title: const Text(
'发布 ',
),
activeColor: _activeColor,
inactiveColor: _inactiveColor,
textAlign: TextAlign.center,
singleIcon: true),
BottomBarItem(
icon: const Image(
image: AssetImage('assets/images/icon_message_unselected.png'),
fit: BoxFit.fill,
),
activeIcon: const Image(
image: AssetImage('assets/images/icon_message_selected.png'),
fit: BoxFit.fill,
),
title: const Text('消息'),
activeColor: _activeColor,
inactiveColor: _inactiveColor,
textAlign: TextAlign.center,
),
BottomBarItem(
icon: const Image(
image: AssetImage("assets/images/icon_me_unselected.png"),
fit: BoxFit.fill,
),
activeIcon: const Image(
image: AssetImage("assets/images/icon_me_selected.png"),
fit: BoxFit.fill,
),
title: const Text('我的'),
activeColor: Colors.blue,
inactiveColor: _inactiveColor,
textAlign: TextAlign.center,
),
],
);
}
Widget getBody() {
List<Widget> pages = [
Container(
alignment: Alignment.center,
child: const Text(
"Home",
style: TextStyle(fontSize: 25, fontWeight: FontWeight.bold),
),
),
Container(
alignment: Alignment.center,
child: const Text(
"Circles",
style: TextStyle(fontSize: 25, fontWeight: FontWeight.bold),
),
),
Container(
alignment: Alignment.center,
child: const Text(
"Publishes",
style: TextStyle(fontSize: 25, fontWeight: FontWeight.bold),
),
),
Container(
alignment: Alignment.center,
child: const Text(
"Messages",
style: TextStyle(fontSize: 25, fontWeight: FontWeight.bold),
),
),
Container(
alignment: Alignment.center,
child: const Text(
"Users",
style: TextStyle(fontSize: 25, fontWeight: FontWeight.bold),
),
),
];
return IndexedStack(
index: _currentIndex,
children: pages,
);
}
}