美文网首页Android开发经验谈一起来学Flutter~Flutter中文社区
Flutter 30: 图解自定义底部状态栏 ACEBottom

Flutter 30: 图解自定义底部状态栏 ACEBottom

作者: 阿策神奇 | 来源:发表于2019-01-28 20:17 被阅读12次

          小菜刚接触 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 插件,有不对的地方敬请指点!

          小菜对细节地方介绍较少,希望各位朋友优先尝试效果。以下是小菜公众号,欢迎闲来吐槽~


    公众号.jpg

    相关文章

      网友评论

        本文标题:Flutter 30: 图解自定义底部状态栏 ACEBottom

        本文链接:https://www.haomeiwen.com/subject/adcajqtx.html