小菜刚接触 Flutter 时接触到底部状态栏 BottomNavigationBar 方便快捷,但随着使用过程发现依然有一些限制,包括图片选择/样式凸出/固定 NavigationItem 位等。小菜不才,准备照葫芦画瓢,自定义一个底部状态栏,并尝试封装成一个 Pub 插件。
小菜首先了解了一下 BottomNavigationBar,主要由整体填充布局与子NavigationItem,小菜也是这样设计的,但 BottomNavigationBar 设计的配置部分主要是在 BottomNavigationBar 中完成的,而 BottomNavigationBarItem 可以看作只是一个单纯的实体类,小菜认为这样设计的好处就是统一管理,减少冗余配置等;而小菜为了配置项更多更灵活选择在 NavigationItem 中进行配置判断,这样实现的缺点就是冗余项较多,小菜也会不断学习完善。
设计尝试
一:类型确定
小菜尝试用枚举类型确定不同的样式,明确且方便,延展性也较好;
enum ACEBottomNavigationBarType {
normal, // 普通类型,选中变色,样式不变
zoom, // 图片或icon变大,此时隐藏文字,支持变色
zoomout, // 图片或icon变大,并凸出显示,文字显示,支持变色
zoomoutonlypic, // 图片或icon变大,并凸出显示,文字隐藏
}
二:NavigationItem 搭建
对于 NavigationItem 因为计划有凸出效果展示,整体用了 Stack 来搭建,配合 AnimatedAlign 等具体的组件来共同搭建,因为 Item 中各种状态均可根据用户定义的样式进行传参,故所有字段前均需 @required。
class NavigationItem extends StatelessWidget {
final UniqueKey uniqueKey;
final textStr;
final textUnSelectedColor;
final textSelectedColor;
final icon;
final iconUnSelectedColor;
final iconSelectedColor;
final image;
final imageSelected;
final selected;
final ACEBottomNavigationBarType type;
final Function(UniqueKey uniqueKey) callbackFunction;
NavigationItem(
{@required this.uniqueKey,
@required this.selected,
@required this.textStr,
@required this.textSelectedColor,
@required this.textUnSelectedColor,
@required this.icon,
@required this.iconSelectedColor,
@required this.iconUnSelectedColor,
@required this.image,
@required this.imageSelected,
@required this.callbackFunction,
@required this.type});
@override
Widget build(BuildContext context) {
return Expanded(
child: Stack(children: [
Container(
alignment: Alignment.bottomCenter,
child: Opacity(
opacity: textOption(),
child: Padding(
padding: const EdgeInsets.all(6.0),
child: Text(textStr,
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: TextStyle(
fontWeight: FontWeight.w600,
color: selected
? textSelectedColor
: textUnSelectedColor))))),
Container(
child: AnimatedAlign(
duration: Duration(milliseconds: 0),
alignment: picZoomAlignment(),
child: childWid()))
]));
}
double picSize() {
var size;
if (type == ACEBottomNavigationBarType.normal) {
size = 30.0;
} else {
size = selected ? 50.0 : 30.0;
}
return size;
}
double textOption() {
var option;
if (type == ACEBottomNavigationBarType.zoom ||
type == ACEBottomNavigationBarType.zoomoutonlypic) {
option = selected ? 0.0 : 1.0;
} else if (type == ACEBottomNavigationBarType.zoomout) {
option = 1.0;
} else {
option = 1.0;
}
return option;
}
EdgeInsetsGeometry imagePadding() {
EdgeInsetsGeometry edge;
if (type == ACEBottomNavigationBarType.zoom) {
edge = selected
? EdgeInsets.only(top: 6.0, bottom: 6.0)
: EdgeInsets.only(bottom: 20.0);
} else if (type == ACEBottomNavigationBarType.zoomout ||
type == ACEBottomNavigationBarType.zoomoutonlypic) {
edge = selected
? EdgeInsets.only(bottom: 0.0)
: EdgeInsets.only(bottom: 20.0);
} else if (type == ACEBottomNavigationBarType.normal) {
edge = EdgeInsets.only(bottom: 20.0);
} else {
edge = EdgeInsets.only(bottom: 0.0);
}
return edge;
}
Widget childWid() {
Widget widget;
if (image != null) {
widget = GestureDetector(
child: Padding(
padding: imagePadding(),
child: Image(
image: (selected && imageSelected != null)
? imageSelected
: image,
width: picSize(),
height: picSize())),
onTap: () {
callbackFunction(uniqueKey);
});
} else {
widget = IconButton(
highlightColor: Colors.transparent,
splashColor: Colors.transparent,
padding: EdgeInsets.only(bottom: 24.0),
alignment: Alignment(0, 0),
icon: Icon(icon,
size: picSize(),
color: selected ? iconSelectedColor : iconUnSelectedColor),
onPressed: () {
callbackFunction(uniqueKey);
});
}
return widget;
}
}
三:ACEBottomNavigationBar 框架搭建
小菜自定义 ACEBottomNavigationBar 用来装载 Item 框架,若不设置单独 Item 时使用 ACEBottomNavigationBar 配置项,为公共效果,若两者同时设置,优先使用 NavigationItem 效果。
为了实现切换时可以对应相应的 Tab 页,需要设置 item key。
class ACEBottomNavigationBar extends StatefulWidget {
final Key key;
final List items;
final initSelectedIndex;
final bgColor;
final bgImage;
final Function(int position) onTabChangedListener;
final textStr;
final textUnSelectedColor;
final textSelectedColor;
final icon;
final iconUnSelectedColor;
final iconSelectedColor;
final image;
final imageSelected;
final ACEBottomNavigationBarType type;
ACEBottomNavigationBar(
{@required this.items,
@required this.onTabChangedListener,
ACEBottomNavigationBarType type,
this.key,
this.initSelectedIndex = 0,
this.textStr,
this.textSelectedColor,
this.textUnSelectedColor,
this.icon,
this.iconSelectedColor,
this.iconUnSelectedColor,
this.image,
this.imageSelected,
this.bgColor,
this.bgImage})
: assert(onTabChangedListener != null),
assert(items != null),
assert(items.length >= 1 && items.length <= 5),
type = type;
@override
_ACEBottomNavigationBar createState() => _ACEBottomNavigationBar();
}
class _ACEBottomNavigationBar extends State
with TickerProviderStateMixin, RouteAware {
var curSelectedIndex = 0;
var textSelectedColor;
var textUnSelectedColor;
var iconSelectedColor;
var iconUnSelectedColor;
@override
void initState() {
super.initState();
_setSelected(widget.items[widget.initSelectedIndex].key);
}
_setSelected(UniqueKey key) {
if (mounted) {
setState(() {
curSelectedIndex =
widget.items.indexWhere((tabData) => tabData.key == key);
});
}
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
textUnSelectedColor = (widget.textUnSelectedColor == null)
? (Theme.of(context).brightness == Brightness.dark)
? Colors.white
: Colors.black54
: widget.textUnSelectedColor;
textSelectedColor = (widget.textSelectedColor == null)
? (Theme.of(context).brightness == Brightness.dark)
? Colors.white
: Colors.black87
: widget.textSelectedColor;
iconUnSelectedColor = (widget.iconUnSelectedColor == null)
? (Theme.of(context).brightness == Brightness.dark)
? Colors.white
: Colors.black54
: widget.iconUnSelectedColor;
iconSelectedColor = (widget.iconSelectedColor == null)
? (Theme.of(context).brightness == Brightness.dark)
? Colors.white
: Colors.black87
: widget.iconSelectedColor;
}
@override
Widget build(BuildContext context) {
return Stack(alignment: Alignment.bottomCenter, children: [
Container(
height: 60.0,
decoration: navigationBarBg(),
child: Row(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: widget.items
.map((item) => NavigationItem(
uniqueKey: item.key,
selected: item.key == widget.items[curSelectedIndex].key,
icon: item.icon,
textStr: item.textStr,
textSelectedColor: (item.textSelectedColor == null)
? this.textSelectedColor
: item.textSelectedColor,
textUnSelectedColor: (item.textUnSelectedColor == null)
? this.textUnSelectedColor
: item.textUnSelectedColor,
iconSelectedColor: (item.iconSelectedColor == null)
? this.iconSelectedColor
: item.iconSelectedColor,
iconUnSelectedColor: (item.iconUnSelectedColor == null)
? this.iconUnSelectedColor
: item.iconUnSelectedColor,
type: widget.type != null
? widget.type
: ACEBottomNavigationBarType.normal,
image: item.image,
imageSelected: item.imageSelected,
callbackFunction: (uniqueKey) {
int selected = widget.items
.indexWhere((tabData) => tabData.key == uniqueKey);
widget.onTabChangedListener(selected);
_setSelected(uniqueKey);
}))
.toList()))
]);
}
BoxDecoration navigationBarBg() {
return widget.bgImage != null
? BoxDecoration(boxShadow: [
BoxShadow(
color: Colors.black12, offset: Offset(0, -1), blurRadius: 8)
], image: DecorationImage(fit: BoxFit.cover, image: widget.bgImage))
: BoxDecoration(
color: widget.bgColor != null ? widget.bgColor : Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black12, offset: Offset(0, -1), blurRadius: 8)
]);
}
}
注意事项
- ACEBottomNavigationBarType 为状态栏样式,默认为 nomal 类型,支持文字和图片/icon 颜色切换;
- 小菜尝试时对图片设置成图片和 icon 两种,icon 类型支持颜色绘制,而图片支持选中和未选中两张图切换;同时如果设置图片和 icon 两种,优先使用图片样式;同时用户对于两张图样式时可以只设置一张未选中状态图;同时支持图片和 icon 两种方式共存;
- 小菜设计 NavigationItem 中传递 image 图片,是为了支持本地图/网络图/内存图等多种图片格式;
- ACEBottomNavigationBar 中可以设置背景图或背景色,优先使用背景图效果,且背景图支持本地图或网络图。
小菜尝试过程中还有很多欠缺,下一步计划添加固定凸出 Item 位样式,并尝试发不成 Pub 插件,有不对的地方敬请指点!
小菜对细节地方介绍较少,希望各位朋友优先尝试效果。以下是小菜公众号,欢迎闲来吐槽~