初学Flutter自定义BottomNavigationBar,支持选中Lottie动画、图片、文字等。
看效果:
SSBottomNavBar.gif
下面是所有代码:
import 'package:flutter/material.dart';
import 'package:lottie/lottie.dart';
class SSBottomNavBar extends StatefulWidget {
SSBottomNavBar({
super.key,
this.selectedIndex = 0,
this.iconSize = 24,
this.containerHeight = 56,
this.backgroundColor,
this.shadowColor,
this.onItemTap,
this.onItemDoubleTap,
required this.items,
});
final int selectedIndex;
final double iconSize;
final double containerHeight;
final Color? backgroundColor;
final Color? shadowColor;
final List<SSBottomNavBarItem> items;
final ValueChanged<int>? onItemTap;
final ValueChanged<int>? onItemDoubleTap;
@override
State<SSBottomNavBar> createState() => _SSBottomNavBarState();
}
class _SSBottomNavBarState extends State<SSBottomNavBar> {
late int _currentIndex;
@override
void initState() {
super.initState();
SSBottomNavBarListener().items = widget.items;
_currentIndex = widget.selectedIndex;
}
void _onTap(SSBottomNavBarItem item) {
SSBottomNavBarItem currentItem = widget.items[_currentIndex];
if (currentItem.itemId != item.itemId) {
widget.onItemTap != null ? widget.onItemTap!(item.index) : null;
setState(() {
_currentIndex = item.index;
});
SSBottomNavBarListener().currentItemId = item.index;
//SSBottomNavBarListener().tabBadgeChangeNotifier(item.itemId, false);
}
}
void _onDoubleTap(SSBottomNavBarItem item) {
SSBottomNavBarItem currentItem = widget.items[_currentIndex];
widget.onItemDoubleTap != null ? widget.onItemDoubleTap!(currentItem.index) : null;
}
@override
Widget build(BuildContext context) {
final bgColor = widget.backgroundColor ??
(Theme.of(context).bottomAppBarTheme.color ?? Colors.white);
const double itemMerge = 4.0;
return Container(
decoration: BoxDecoration(
color: bgColor,
boxShadow: widget.shadowColor != null ? [
BoxShadow(
color: widget.shadowColor!,
blurRadius: 4,
),
] : null,
),
child: SafeArea(
child: Container(
width: double.infinity,
height: widget.containerHeight,
padding: const EdgeInsets.symmetric(vertical: 0, horizontal: itemMerge),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: widget.items.map((item) {
var index = widget.items.indexOf(item);
item.index = index;
if (item.itemId == 0) item.itemId = index;
SSBottomNavBarItem currentItem = widget.items[_currentIndex];
item.isSelected = (index == _currentIndex);
return SSNavBarItemWidget(
itemWidth: (MediaQuery.of(context).size.width - itemMerge*2) / widget.items.length,
iconSize: widget.iconSize,
item: item,
backgroundColor: bgColor,
onTap: _onTap,
onDoubleTap: _onDoubleTap,
);
}).toList(),
),
),
),
);
}
}
/// item
class SSNavBarItemWidget extends StatefulWidget {
SSNavBarItemWidget({
super.key,
required this.itemWidth,
required this.iconSize,
required this.item,
required this.backgroundColor,
this.onTap,
this.onDoubleTap,
});
final double itemWidth;
final double iconSize;
final SSBottomNavBarItem item;
final Color backgroundColor;
final Function(SSBottomNavBarItem)? onTap;
final Function(SSBottomNavBarItem)? onDoubleTap;
@override
State<SSNavBarItemWidget> createState() => _SSNavBarItemWidgetState();
}
class _SSNavBarItemWidgetState extends State<SSNavBarItemWidget> with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late bool _lottieCompleted;
@override
void initState() {
super.initState();
_lottieCompleted = false;
widget.item.addListener(_itemStateChange);
_animationController = AnimationController(vsync: this);
_animationController.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_lottieCompleted = true;
setState(() {});
}
});
}
@override
void dispose() {
widget.item.removeListener(_itemStateChange);
_animationController.dispose();
super.dispose();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
}
@override
void didUpdateWidget(covariant SSNavBarItemWidget oldWidget) {
_lottieCompleted = false;
super.didUpdateWidget(oldWidget);
}
void _itemStateChange() {
setState(() {});
if (widget.item.notifierType == 2) {
if (widget.onTap != null) {
widget.onTap!(widget.item);
}
}
}
Widget _tabIcon() {
return SizedBox(
width: widget.iconSize,
height: widget.iconSize,
child: FittedBox(
fit: BoxFit.contain,
child: _customIcon(),
),
);
}
Widget _customIcon() {
iconTheme() {
return
widget.item.isSelected ?
(widget.item.selectIcon ?? widget.item.unselectIcon) :
widget.item.unselectIcon;
// return IconTheme(
// data: IconThemeData(
// size: widget.iconSize,
// color: widget.isSelected ? widget.item.selectTextColor : widget.item.unselectTextColor,
// ),
// child: widget.isSelected ?
// (widget.item.selectIcon ?? widget.item.unselectIcon)
// : widget.item.unselectIcon,
// );
}
if (widget.item.isSelected) {
if (widget.item.lottiePath is String && widget.item.lottiePath!.isNotEmpty) {
if (!_lottieCompleted) {
return Lottie.asset(
widget.item.lottiePath!,
controller: _animationController,
onLoaded: (composition) {
_animationController
..duration = composition.duration
..forward(from: 0.0);
},
);
} else {
return iconTheme();
}
} else {
return iconTheme();
}
} else {
return iconTheme();
}
}
Widget _tabText() {
return Flexible(
child: Text(
widget.item.title,
maxLines: 1,
textAlign: widget.item.textAlign,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: widget.item.isSelected ? widget.item.selectTextColor : widget.item.unselectTextColor,
fontSize: widget.item.isSelected ? widget.item.selectFontSize : widget.item.unselectFontSize,
height: 1.2,
fontWeight: FontWeight.w500,
)),
);
}
Widget _tabBadge() {
return widget.item.showBadge ? Positioned(
top: 5,
left: widget.itemWidth/2.0 + 15,
child: ClipOval(
child: Container(
color: widget.item.badgeColor,
width: 7,
height: 7,
),
)
) : Container();
}
@override
Widget build(BuildContext context) {
Semantics semantic = Semantics(
container: true,
selected: widget.item.isSelected,
child: GestureDetector(
onTap: () {
if (widget.onTap != null) {
widget.onTap!(widget.item);
}
},
onDoubleTap: widget.item.isSelected ? () {
if (widget.onDoubleTap != null) {
widget.onDoubleTap!(widget.item);
}
} : null,
child: Stack(
children: [
Container(
width: widget.itemWidth,
height: 56,
decoration: BoxDecoration(
color: widget.item.isSelected ? widget.item.selectTextColor.withOpacity(0.2) : null,
),
child: Container(
width: widget.itemWidth,
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_tabIcon(),
_tabText()
],
)
),
),
// 红点
_tabBadge(),
],
),
)
);
return widget.item.tooltipText == null
? semantic
: Tooltip(
message: widget.item.tooltipText!,
child: semantic,
);
}
}
/// 数据model
class SSBottomNavBarItem extends ChangeNotifier {
SSBottomNavBarItem({
required this.unselectIcon,
this.selectIcon,
required this.title,
this.textAlign = TextAlign.center,
this.tooltipText,
this.selectTextColor = Colors.green,
this.unselectTextColor = Colors.black,
this.selectFontSize = 12.0,
this.unselectFontSize = 12.0,
this.badgeColor = Colors.red,
this.showBadge = false,
this.index = 0,
this.itemId = 0,
this.lottiePath,
this.isSelected = false,
this.notifierType = 0,
});
final Widget? selectIcon;
final Widget unselectIcon;
final String title;
final TextAlign? textAlign;
final String? tooltipText;
String? lottiePath;
final Color selectTextColor;
final Color unselectTextColor;
final double selectFontSize;
final double unselectFontSize;
final Color? badgeColor;
bool showBadge;
int index;
int itemId;
bool isSelected;
int notifierType; // 通知类型,1:badge更新,2:下标更新
void postNotify() {
notifyListeners();
}
}
/// badge状态管理 单例
class SSBottomNavBarListener {
// 工厂方法构造函数 - 通过UserModel()获取对象1
factory SSBottomNavBarListener() => _getInstance();
// instance的getter方法 - 通过UserModel.instance获取对象2
static SSBottomNavBarListener get instance => _getInstance();
// 静态变量_instance,存储唯一对象
static SSBottomNavBarListener? _instance;
// 获取唯一对象
static SSBottomNavBarListener _getInstance() {
_instance ??= SSBottomNavBarListener._internal();
return _instance!;
}
//初始化...
SSBottomNavBarListener._internal() {
//初始化其他操作...
}
late List<SSBottomNavBarItem> items;
int currentItemId = 0;
void tabBadgeChangeNotifier(int itemId, bool isShowBadge) {
for (SSBottomNavBarItem item in items) {
item.notifierType = 1;
if (item.itemId == itemId && item.showBadge != isShowBadge) {
item.showBadge = isShowBadge;
item.postNotify();
}
}
}
void tabSelectItemId(int itemId) {
for (SSBottomNavBarItem item in items) {
item.notifierType = 2;
if (itemId == item.itemId && !item.isSelected) {
item.isSelected = true;
item.postNotify();
}
}
}
}
使用
SSBottomNavBar _bottomNavBar() {
List<String> titles = ['首页', '视频', '赛程', '数据', '会员'];
List<SSBottomNavBarItem> items = List.generate(titles.length, (index) {
String title = titles[index];
Image selectImage = SSAssets.images.common.rightMarkGreen.image();
Image unselectIcon = SSAssets.images.common.netError.image();
String? lottie;
bool showBadge = false;
if (index == 0) {
lottie = SSAssets.lotties.homeYearAnimation.path;
}
if (index == 1) {
unselectIcon = SSAssets.images.common.dataEmpty.image();
} else if (index == 2) {
unselectIcon = SSAssets.images.common.headerFootball2x.image();
} else if (index == 3) {
unselectIcon = SSAssets.images.common.dataError.image();
} else if (index == 4) {
unselectIcon = SSAssets.images.common.rightMarkRed.image();
showBadge = true;
}
return SSBottomNavBarItem(
title: title,
selectIcon: selectImage,
unselectIcon: unselectIcon,
lottiePath: lottie,
showBadge: showBadge,
);
});
return SSBottomNavBar(
selectedIndex: _selectIndex,
iconSize: 35,
backgroundColor: Colors.white,
onItemTap: (index) {
setState(() {
_selectIndex = index;
print('index = $index');
});
},
items: items,
);
}