flutter 底部动画导航栏

更多文章请查看 lutter从入门 到精通

实现底部导航栏并点击切换页面可简述为有三种方式

  • TabBar + TabBarView
  • BottomNavigationBar + BottomNavigationBarItem
  • 自定义 BottomAppBar
  • 自定义绘制布局

这里实现的是自定义绘制布局

在这里插入图片描述


class MoveBottomMenu extends StatefulWidget {
  @override
  _MoveBottomMenuState createState() => _MoveBottomMenuState();
}

class _MoveBottomMenuState extends State<MoveBottomMenu>
    with SingleTickerProviderStateMixin {
  //当前选中的页面
  int _activeIndex = 0;

  //导航栏高度
  double _height = 52.0;

  //悬浮图标半径
  double _floatRadius;

  //移动补间
  double _moveTween = 0.0;

  //浮动图标与圆弧之间的间隙
  double _padding = 10.0;

  //动画控制器
  AnimationController _animationController;

  //移动动画
  Animation<double> _moveAnimation;

  //导航栏使用到的图标
  List _icons = [
    Icons.home,
    Icons.ondemand_video,
    Icons.add,
    Icons.insert_comment,
    Icons.person
  ];

  //点击导航项是要显示的页面
  final _pages = [
    ChildItemView("首页"),
    ChildItemView("发现"),
    ChildItemView("添加"),
    ChildItemView("动态"),
    ChildItemView("我的")
  ];

  //导航栏使用到的标题
  final _titls = ["首页", "发现", "添加", "动态", "我的"];

  @override
  void initState() {
    _floatRadius = _height * 2 / 3;
    _animationController =
        AnimationController(vsync: this, duration: Duration(milliseconds: 400));
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    final double bottomPadding = MediaQuery.of(context).padding.bottom;
    double width = MediaQuery.of(context).size.width;
    double singleWidth = width / _icons.length;
    double posTop = _animationController.value <= 0.5
        ? (_animationController.value * _height * _padding / 2) -
            _floatRadius / 3 * 2
        : (1 - _animationController.value) * _height * _padding / 2 -
            _floatRadius / 3 * 2;
    print("posTop " + posTop.toString());
    if (posTop > 0) {
      posTop = 0;
    }
    print("posTop--- " + posTop.toString());
    print("bottomPadding "+bottomPadding.toString());
    return Container(
      padding: EdgeInsets.only(bottom: bottomPadding),
      child: Stack(children: [
        Scaffold(
          //对应的页面
          body: _pages[_activeIndex],
          appBar: AppBar(
            backgroundColor: Colors.red,
            elevation: 0.5,
            title: Text('动画底部菜单栏'),
            centerTitle: true,
          ),
          backgroundColor: Color(0xFFEEEEEE),
        ),
        Positioned(
          bottom: 0.0,
          child: Container(
            width: width,
            child: Stack(
              overflow: Overflow.visible,
              children: <Widget>[
                //浮动图标
                Positioned(
                  top: posTop,
                  left: _moveTween * singleWidth +
                      (singleWidth - _floatRadius) / 2 -
                      _padding / 2 -
                      2,
                  child: DecoratedBox(
                    decoration:
                    ShapeDecoration(shape: CircleBorder(), shadows: [
                      BoxShadow(
                          blurRadius: _padding / 2,
                          offset: Offset(0, _padding / 2),
                          spreadRadius: 0,
                          color: Colors.black26),
                    ]),
                    child: CircleAvatar(
                        radius: _floatRadius - _padding,
                        //浮动图标和圆弧之间设置10pixel间隙
                        backgroundColor: Colors.white,
                        child: Icon(_icons[_activeIndex], color: Colors.blue)),
                  ),
                ),
                //所有图标
                CustomPaint(
                  child: SizedBox(
                    height: _height,
                    child: Row(
                      mainAxisAlignment: MainAxisAlignment.spaceAround,
                      crossAxisAlignment: CrossAxisAlignment.center,
                      children: _icons
                          .asMap()
                          .map((i, v) => MapEntry(
                          i,
                          GestureDetector(
                            child: buildBotomItem(
                                _activeIndex, i, v, _titls[i]),
                            onTap: () {
                              _switchNav(i);
                            },
                          )))
                          .values
                          .toList(),
                    ),
                  ),
                  painter: ArcPainter(
                      navCount: _icons.length,
                      moveTween: _moveTween,
                      padding: _padding),
                )
              ],
            ),
          ),
        )
      ]),
    );
  }

  //切换导航
  _switchNav(int newIndex) {
    print("switch " + newIndex.toString());
    double oldPosition = _activeIndex.toDouble();
    double newPosition = newIndex.toDouble();
    if (oldPosition != newPosition &&
        _animationController.status != AnimationStatus.forward) {
      _animationController.reset();
      _moveAnimation = Tween(begin: oldPosition, end: newPosition).animate(
          CurvedAnimation(
              parent: _animationController, curve: Curves.easeInCubic))
        ..addListener(() {
          setState(() {
            _moveTween = _moveAnimation.value;
          });
        })
        ..addStatusListener((AnimationStatus status) {
          if (status == AnimationStatus.completed) {
            setState(() {
              _activeIndex = newIndex;
            });
          }
        });
      _animationController.forward();
    }
  }

  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }

  buildBotomItem(int selectIndex, int index, IconData iconData, String title) {
    //未选中状态的样式
    TextStyle textStyle = TextStyle(fontSize: 12.0, color: Colors.grey);
    Color iconColor = Colors.grey;
    Color bgColor = Colors.white;
    EdgeInsetsGeometry padding = EdgeInsets.only(top: 8.0);

    if (selectIndex == index) {
      //选中状态的文字样式
      textStyle = TextStyle(fontSize: 0.0, color: Colors.transparent);
      //选中状态的按钮样式
      iconColor = Colors.transparent;
      bgColor = Colors.transparent;
      padding = EdgeInsets.only(top: 0.0);
    }
    Widget padItem = SizedBox();
    if (iconData != null) {
      padItem = Padding(
        padding: padding,
        child: Container(
          color: bgColor,
          child: Center(
            child: Column(
              children: <Widget>[
                Icon(
                  iconData,
                  color: iconColor,
                ),
                Text(
                  title,
                  style: textStyle,
                )
              ],
            ),
          ),
        ),
      );
    }
    return SizedBox(
      height: 52,
      child: padItem,
    );
  }
}

//绘制圆弧背景
class ArcPainter extends CustomPainter {
  final int navCount; //导航总数
  final double moveTween; //移动补间
  final double padding; //间隙
  ArcPainter({this.navCount, this.moveTween, this.padding});

  @override
  void paint(Canvas canvas, Size size) {
    Paint paint = Paint()
      ..color = (Colors.white)
      ..style = PaintingStyle.stroke; //画笔
    double width = size.width; //导航栏总宽度,即canvas宽度
    double singleWidth = width / navCount; //单个导航项宽度
    double height = size.height; //导航栏高度,即canvas高度
    double arcRadius = height * 2 / 3; //圆弧半径
    double restSpace = (singleWidth - arcRadius * 2) / 2; //单个导航项减去圆弧直径后单边剩余宽度

    Path path = Path() //路径
      ..relativeLineTo(moveTween * singleWidth, 0)
      ..relativeCubicTo(restSpace + padding, 0, restSpace + padding / 2,
          arcRadius, singleWidth / 2, arcRadius) //圆弧左半边
      ..relativeCubicTo(arcRadius, 0, arcRadius - padding, -arcRadius,
          restSpace + arcRadius, -arcRadius) //圆弧右半边
      ..relativeLineTo(width - (moveTween + 1) * singleWidth, 0)
      ..relativeLineTo(0, height)
      ..relativeLineTo(-width, 0)
      ..relativeLineTo(0, -height)
      ..close();
    paint.style = PaintingStyle.fill;
    canvas.drawPath(path, paint);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return true;
  }
}

子页面widget

class ChildItemView extends StatefulWidget {
  String _title;

  ChildItemView(this._title);

  @override
  _ChildItemViewState createState() => _ChildItemViewState();
}

class _ChildItemViewState extends State<ChildItemView> {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Center(child: Text(widget._title)),
    );
  }
}

思路基础参考

早起的年轻人 CSDN认证博客专家 移动开发 项目管理 Java
只要用心去做,每一件事情还是有可能成功的,当然成功是没有界限的,只不过是达到自己心里的那个目标,公众号:我的大前端生涯,一个爱喝茶的程序员,通常会搞搞SpringBoot 、Herbinate、Mybatiys、Android、iOS、Flutter、Vue、小程序等.
©️2020 CSDN 皮肤主题: 代码科技 设计师:Amelia_0503 返回首页