小菜刚接触 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: <Widget>[
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<NavigationItemBean> 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<ACEBottomNavigationBar> 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: <Widget>[
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 中可以设置背景图或背景色,优先使用背景图效果,且背景图支持本地图或网络图。
作者:阿策神奇
共同學習,寫下你的評論
評論加載中...
作者其他優質文章


