场景:
在实际的项目开发过程中,app底部tabbar每个公司经常有不同的项目。,或自定义动画,或;但是系统自带BottomNavigationBarItem和pub.dev中间的第三方库,不符合实际用途。
本文主要给BottomNavigationBarItem点击添加,并调整了BottomNavigationBarItem的主要。 可以参考,添加自己的专属动画,也可以参考BottomNavigationBarItem的修改为自己的。
先贴效果图:

粘贴完整代码的原因是为了方便大家懒得下载和直接复制。我希望它能帮助你,并为你提供各种修改flutter灵感来自原始组件。 点击下载demo完成源码
正文
1.重写系统BottomNavigationBarItem
新建lo_bottom_navigation_bar_item.dart, 这里增加了两个属性
animation
和index
;
import 'package:flutter/cupertino.dart'; class BottomNavigationBarItem {
BottomNavigationBarItem({
required this.icon, this.label, Widget? activeIcon, this.backgroundColor, this.tooltip, }) : activeIcon = activeIcon ?? icon, assert(icon != null); Animation<double>? animation; late int index; late Widget icon; final Widget activeIcon; final String? label; final Color? backgroundColor; final String? tooltip; }
2.重写系统BottomNavigationBar
新建lo_bottom_navigation_bar.dart,, 这里主要有变化
InkResponse
中的child
,改变结构,添加动画;
import 'dart:collection' show Queue; import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:vector_math/vector_math_64.dart' show Vector3;
import 'lo_bottom_navigation_bar_item.dart' as MyBarItem;
enum MyBottomNavigationBarType {
fixed,
shifting,
}
enum MyBottomNavigationBarLandscapeLayout {
spread,
centered,
linear,
}
class MyBottomNavigationBar extends StatefulWidget {
MyBottomNavigationBar({
Key? key,
required this.items,
this.onTap,
this.currentIndex = 0,
this.elevation,
this.type,
Color? fixedColor,
this.backgroundColor,
this.iconSize = 24.0,
Color? selectedItemColor,
this.unselectedItemColor,
this.selectedIconTheme,
this.unselectedIconTheme,
this.selectedFontSize = 14.0,
this.unselectedFontSize = 12.0,
this.selectedLabelStyle,
this.unselectedLabelStyle,
this.showSelectedLabels,
this.showUnselectedLabels,
this.mouseCursor,
this.enableFeedback,
this.landscapeLayout,
}) : assert(items != null),
assert(items.length >= 2),
assert(
items.every((MyBarItem.BottomNavigationBarItem item) => item.label != null),
'Every item must have a non-null label',
),
assert(0 <= currentIndex && currentIndex < items.length),
assert(elevation == null || elevation >= 0.0),
assert(iconSize != null && iconSize >= 0.0),
assert(
selectedItemColor == null || fixedColor == null,
'Either selectedItemColor or fixedColor can be specified, but not both',
),
assert(selectedFontSize != null && selectedFontSize >= 0.0),
assert(unselectedFontSize != null && unselectedFontSize >= 0.0),
selectedItemColor = selectedItemColor ?? fixedColor,
super(key: key);
final List<MyBarItem.BottomNavigationBarItem> items;
final ValueChanged<int>? onTap;
final int currentIndex;
final double? elevation;
final MyBottomNavigationBarType? type;
Color? get fixedColor => selectedItemColor;
final Color? backgroundColor;
final double iconSize;
final Color? selectedItemColor;
final Color? unselectedItemColor;
final IconThemeData? selectedIconTheme;
final IconThemeData? unselectedIconTheme;
final TextStyle? selectedLabelStyle;
final TextStyle? unselectedLabelStyle;
final double selectedFontSize;
final double unselectedFontSize;
final bool? showUnselectedLabels;
final bool? showSelectedLabels;
final MouseCursor? mouseCursor;
final bool? enableFeedback;
final MyBottomNavigationBarLandscapeLayout? landscapeLayout;
@override
State<MyBottomNavigationBar> createState() => _MyBottomNavigationBarState();
}
// This represents a single tile in the bottom navigation bar. It is intended
// to go into a flex container.
class _BottomNavigationTile extends StatelessWidget {
const _BottomNavigationTile(
this.type,
this.item,
this.animation,
this.iconSize, {
this.onTap,
this.colorTween,
this.flex,
this.selected = false,
required this.selectedLabelStyle,
required this.unselectedLabelStyle,
required this.selectedIconTheme,
required this.unselectedIconTheme,
required this.showSelectedLabels,
required this.showUnselectedLabels,
this.indexLabel,
required this.mouseCursor,
required this.enableFeedback,
required this.layout,
}) : assert(type != null),
assert(item != null),
assert(animation != null),
assert(selected != null),
assert(selectedLabelStyle != null),
assert(unselectedLabelStyle != null),
assert(mouseCursor != null);
final MyBottomNavigationBarType type;
final MyBarItem.BottomNavigationBarItem item;
final Animation<double> animation;
final double iconSize;
final VoidCallback? onTap;
final ColorTween? colorTween;
final double? flex;
final bool selected;
final IconThemeData? selectedIconTheme;
final IconThemeData? unselectedIconTheme;
final TextStyle selectedLabelStyle;
final TextStyle unselectedLabelStyle;
final String? indexLabel;
final bool showSelectedLabels;
final bool showUnselectedLabels;
final MouseCursor mouseCursor;
final bool enableFeedback;
final MyBottomNavigationBarLandscapeLayout layout;
@override
Widget build(BuildContext context) {
final int size;
final double selectedFontSize = selectedLabelStyle.fontSize!;
final double selectedIconSize = selectedIconTheme?.size ?? iconSize;
final double unselectedIconSize = unselectedIconTheme?.size ?? iconSize;
final double selectedIconDiff = math.max(selectedIconSize - unselectedIconSize, 0);
final double unselectedIconDiff = math.max(unselectedIconSize - selectedIconSize, 0);
final String? effectiveTooltip = item.tooltip == '' ? null : item.tooltip ?? item.label;
double bottomPadding;
double topPadding;
if (showSelectedLabels && !showUnselectedLabels) {
bottomPadding = Tween<double>(
begin: selectedIconDiff / 2.0,
end: selectedFontSize / 2.0 - unselectedIconDiff / 2.0,
).evaluate(animation);
topPadding = Tween<double>(
begin: selectedFontSize + selectedIconDiff / 2.0,
end: selectedFontSize / 2.0 - unselectedIconDiff / 2.0,
).evaluate(animation);
} else if (!showSelectedLabels && !showUnselectedLabels) {
bottomPadding = Tween<double>(
begin: selectedIconDiff / 2.0,
end: unselectedIconDiff / 2.0,
).evaluate(animation);
topPadding = Tween<double>(
begin: selectedFontSize + selectedIconDiff / 2.0,
end: selectedFontSize + unselectedIconDiff / 2.0,
).evaluate(animation);
} else {
bottomPadding = Tween<double>(
begin: selectedFontSize / 2.0 + selectedIconDiff / 2.0,
end: selectedFontSize / 2.0 + unselectedIconDiff / 2.0,
).evaluate(animation);
topPadding = Tween<double>(
begin: selectedFontSize / 2.0 + selectedIconDiff / 2.0,
end: selectedFontSize / 2.0 + unselectedIconDiff / 2.0,
).evaluate(animation);
}
switch (type) {
case MyBottomNavigationBarType.fixed:
size = 1;
break;
case MyBottomNavigationBarType.shifting:
size = (flex! * 1000.0).round();
break;
}
Widget result = InkResponse(
onTap: onTap,
mouseCursor: mouseCursor,
enableFeedback: enableFeedback,
child: Padding(
padding: EdgeInsets.only(top: topPadding, bottom: bottomPadding),
child: ScaleTransition(
scale: item.animation!,
child: _Tile(
layout: layout,
icon: _TileIcon(
colorTween: colorTween!,
animation: animation,
iconSize: iconSize,
selected: selected,
item: item,
selectedIconTheme: selectedIconTheme,
unselectedIconTheme: unselectedIconTheme,
),
label: _Label(
colorTween: colorTween!,
animation: animation,
item: item,
selectedLabelStyle: selectedLabelStyle,
unselectedLabelStyle: unselectedLabelStyle,
showSelectedLabels: showSelectedLabels,
showUnselectedLabels: showUnselectedLabels,
),
),
),
),
);
if (effectiveTooltip != null) {
result = Tooltip(
message: effectiveTooltip,
preferBelow: false,
verticalOffset: selectedIconSize + selectedFontSize,
excludeFromSemantics: true,
child: result,
);
}
result = Semantics(
selected: selected,
container: true,
child: Stack(
children: <Widget>[
result,
Semantics(
label: indexLabel,
),
],
),
);
return Expanded(
flex: size,
child: result,
);
}
}
class _Tile extends StatelessWidget {
const _Tile({
Key? key,
required this.layout,
required this.icon,
required this.label
}) : super(key: key);
final MyBottomNavigationBarLandscapeLayout layout;
final Widget icon;
final Widget label;
@override
Widget build(BuildContext context) {
final MediaQueryData data = MediaQuery.of(context);
if (data.orientation == Orientation.landscape && layout == MyBottomNavigationBarLandscapeLayout.linear) {
return Align(
heightFactor: 1,
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[icon, const SizedBox(width: 8), label],
),
);
}
return Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min,
children: <Widget>[icon, label],
);
}
}
class _TileIcon extends StatelessWidget {
const _TileIcon({
Key? key,
required this.colorTween,
required this.animation,
required this.iconSize,
required this.selected,
required this.item,
required this.selectedIconTheme,
required this.unselectedIconTheme,
}) : assert(selected != null),
assert(item != null),
super(key: key);
final ColorTween colorTween;
final Animation<double> animation;
final double iconSize;
final bool selected;
final MyBarItem.BottomNavigationBarItem item;
final IconThemeData? selectedIconTheme;
final IconThemeData? unselectedIconTheme;
@override
Widget build(BuildContext context) {
final Color? iconColor = colorTween.evaluate(animation);
final IconThemeData defaultIconTheme = IconThemeData(
color: iconColor,
size: iconSize,
);
final IconThemeData iconThemeData = IconThemeData.lerp(
defaultIconTheme.merge(unselectedIconTheme),
defaultIconTheme.merge(selectedIconTheme),
animation.value,
);
return Align(
alignment: Alignment.topCenter,
heightFactor: 1.0,
child: IconTheme(
data: iconThemeData,
child: selected ? item.activeIcon : item.icon,
),
);
}
}
class _Label extends StatelessWidget {
const _Label({
Key? key,
required this.colorTween,
required this.animation,
required this.item,
required this.selectedLabelStyle,
required this.unselectedLabelStyle,
required this.showSelectedLabels,
required this.showUnselectedLabels,
}) : assert(colorTween != null),
assert(animation != null),
assert(item != null),
assert(selectedLabelStyle != null),
assert(unselectedLabelStyle != null),
assert(showSelectedLabels != null),
assert(showUnselectedLabels != null),
super(key: key);
final ColorTween colorTween;
final Animation<double> animation;
final MyBarItem.BottomNavigationBarItem item;
final TextStyle selectedLabelStyle;
final TextStyle unselectedLabelStyle;
final bool showSelectedLabels;