首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【Flutter 专题】30 图解自定义底部状态栏 ACEBottomNavigationBar (一)

【Flutter 专题】30 图解自定义底部状态栏 ACEBottomNavigationBar (一)

作者头像
阿策小和尚
发布2019-08-12 16:22:28
1.1K0
发布2019-08-12 16:22:28
举报
文章被收录于专栏:阿策小和尚阿策小和尚

和尚刚接触 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)
              ]);
  }
}

注意事项

  1. ACEBottomNavigationBarType 为状态栏样式,默认为 nomal 类型,支持文字和图片/icon 颜色切换;
  2. 和尚尝试时对图片设置成图片和 icon 两种,icon 类型支持颜色绘制,而图片支持选中和未选中两张图切换;同时如果设置图片和 icon 两种,优先使用图片样式;同时用户对于两张图样式时可以只设置一张未选中状态图;同时支持图片和 icon 两种方式共存;
  3. 和尚设计 NavigationItem 中传递 image 图片,是为了支持本地图/网络图/内存图等多种图片格式;
  4. ACEBottomNavigationBar 中可以设置背景图或背景色,优先使用背景图效果,且背景图支持本地图或网络图。

和尚尝试过程中还有很多欠缺,下一步计划添加固定凸出 Item 位样式,并尝试发不成 Pub 插件,有不对的地方敬请指点!

和尚对细节地方介绍较少,希望各位朋友优先尝试效果。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-01-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 阿策小和尚 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 设计尝试
    • 一:类型确定
      • 二:NavigationItem 搭建
      • 三:ACEBottomNavigationBar 框架搭建
      • 注意事项
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档