Flutter完全自定义底部导航

倒叙手法,先上效果图:


自定义底部导航

最初的想法是使用系统自带的BottomNavigationBar来实现,做到一半发现完全无法满足这奇葩合理的需求:

  • 背景模糊效果
  • 凸起按钮
  • 非标准高度
  • indicator 动画

起初也曾想到过想要封装一个相对通用的组件,然而需求太过非标,不可避免地需要重复造车轮子,只能将其中一些思路整理出来。

框架结构

@override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        alignment: AlignmentDirectional.bottomStart,
        children: [
                    // 页面
          Container(
            child: PageView(),
          ),
                    // 底部导航
          LATabBar(),
          // indicator动画
          Transform.translate(),
        ],
      ),
    );
  }

我们使用Stack 来放置子视图,简单粗暴,从底部开始层叠:

  • Container:子页面,使用PageView 进行页面切换
  • LATabBar:自定义底部导航
  • Transform.translate():indicator动画组件

出于尽可能解耦的原因,这里把indicator 放到底部导外。

接下来,我们将重点关注底部导航的构建。

全局常量

定义全局常量 ,以针对不同设备进行适配。

Flutter定义全局常量

底部导航

考虑到这里有一个模糊效果,使用Stack来层叠backgrounditem ,首先来定义相关属性。

class LATabBar extends StatefulWidget {
    final double height;
  final Color backgroundColor;
  final Widget background;
  final List items;

    ...
}
  • height:底部导航视图高度
  • backgroundColor:背景颜色(考虑后续组件化设计)
  • background:自定义背景组件(模糊+凸起效果)
  • items:自定义NavigationItem

自定义背景

先前说过,此方法很难封装成一个通用的组件。因为根据需求,需要自定义的东西太多,标准组件所需默认值也多。因此,只能将其中的思路记录下来。

绘制不规则图形

在iOS开发中,此类不规则背景,只需使用一个UIImageView 加载带透明度图片,随后使用view.mask 即可实现不规则背景。

而在Flutter 中,并无相应API来渲染(我太蔡了),因此,需要使用到自定义裁切ClipPath 来绘制相应不规则图形。

首先使用贝塞尔曲线,自定义一个clipper:

class RaisedClipper extends CustomClipper {

  @override
  Path getClip(Size size) {
    /**凸起高度*/
    double raisedHeight = 18;
    /**凸起宽度*/
    double raisedWidth = 80;
    /**凸起右边距*/
    double paddingTrailing = 25;

    
    Path path = Path();
    path.lineTo(0, raisedHeight);
    double cubicStartX = size.width - paddingTrailing - raisedWidth - 10;
    // 贝塞尔曲线起始位置
    path.lineTo(cubicStartX, raisedHeight);
    // 绘制曲线
    path.cubicTo(
        cubicStartX + raisedWidth * 0.1, raisedHeight,
        cubicStartX + raisedWidth * 0.25, 0,
        cubicStartX + raisedWidth * 0.5, 0);
    path.cubicTo(
        cubicStartX + raisedWidth * 0.75, 0,
        cubicStartX + raisedWidth * 0.85, raisedHeight,
        size.width - paddingTrailing, raisedHeight);

    path.lineTo(size.width, raisedHeight);
    path.lineTo(size.width, size.height);
    path.lineTo(0, size.height);

    return path;
  }

关于贝塞尔曲线,这里不再赘述,分享一个在线预览工具,方便调试。

模糊效果

Flutter提供了BackdropFilter 来作为高斯模糊的组件,配合使用ImageFilter 来制作模糊效果。

class BlurBackground extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ClipPath(
      clipper: RaisedClipper(),
      child: BackdropFilter(
        filter: ImageFilter.blur(sigmaX: 3, sigmaY: 3),
        child: Container(
          color: Colors.white.withOpacity(0.5),
        ),
      ),
    );
  }
}

这样就完成了自定义底部导航背景视图的编码。

导航切换按钮

先上代码:

enum LATabItemStyle {
  normal,
  titleOnly,
  iconOnly,
}

class LATabItem extends StatefulWidget {
  final double width;
  final String iconNormal;
  final String iconSelected;
  final EdgeInsets iconInsets;
  final Alignment iconAlignment;
  final Color titleNormalColor;
  final Color titleSelectedColor;
  final EdgeInsets titleInsets;
  final Alignment titleAlignment;
  final String title;
  final GestureTapCallback onTap;
  final bool selected;
  final LATabItemStyle style;

  const LATabItem({
    Key key,
    this.width,
    this.iconNormal,
    this.iconSelected,
    this.iconInsets = EdgeInsets.zero,
    this.iconAlignment = Alignment.center,
    this.titleNormalColor,
    this.titleSelectedColor,
    this.titleInsets = EdgeInsets.zero,
    this.titleAlignment = Alignment.center,
    this.title,
    this.onTap,
    this.selected = false,
    this.style = LATabItemStyle.normal}) : super(key: key);

  @override
  State createState() {
    return LATabItemState();
  }
}

class LATabItemState extends State {
  @override
  Widget build(BuildContext context) {
    Widget content;
    switch (this.widget.style) {
      case LATabItemStyle.titleOnly:

        break;

      case LATabItemStyle.iconOnly:
        content = Container(
          width: this.widget.width,
          alignment: this.widget.iconAlignment,
          padding: this.widget.iconInsets,
          child: Image.asset(this.widget.selected ? this.widget.iconSelected : this.widget.iconNormal),
        );
        break;

      default:

        break;
    }

    if (this.widget.width == null || this.widget.width <= 0) {
      return Expanded(
        child: GestureDetector(
          onTap: this.widget.onTap,
          child: content,
          behavior: HitTestBehavior.opaque,
        ),
      );
    } else {
      return GestureDetector(
        onTap: this.widget.onTap,
        child: content,
      );
    }
  }
}

导航按钮完全可以使用自定义的widget 而不必拘泥于形式,可以是自定义的文字、图片、绘制的图形各种。只要将其作为GestureDetectorchild 即可响应点击事件。

需要注意的是,如果使用自适应大小的Expanded ,则需要指定GestureDetectorbehavior: HitTestBehavior.opaque

indicator动画

简单动画,直接使用AnimationControllerTween 来制作动画。

  • 初始化:
_animationController = AnimationController(duration: Duration(milliseconds: 100), vsync: this);
_tween = Tween(begin: leading, end: leading);
_animation = _tween.animate(_animationController)
  ..addListener(() {
    setState(() {});
  });
  • Translation容器:
Transform.translate(
        offset: Offset(_animation.value, 0),
        child: Container(
        margin: EdgeInsets.fromLTRB(0, 0, 0, Global.tabBarHeight + Global.paddingBottom - Global.tabBarRaisedHeight - 4),
        width: 20,
        height: 4,
        decoration: BoxDecoration(
        borderRadius: BorderRadius.all(Radius.circular(2)),
        color: Color(0xFFB60005),
     ),
   ),
 ),
  • 切换视图时,执行动画:
void setIndicator(int index) {
    _tween.begin = _tween.end;
    _animationController.reset();
    _tween.end = unitWidth * index + leading;
    _animationController.forward();
}

到这里,自定义底部导航完成。

最后放上demo。

你可能感兴趣的:(Flutter完全自定义底部导航)