flutter自定义底部导航栏

1,158 阅读3分钟

开始

想做个有意思的底部导航栏,不想用官方的,找了一些参考,发现这个比较有意思,还有动画。

github.com/tunitowen/t…

image.png

遗憾的但是拿过来这个并不能直接用

思路

在布局上来看 底部导航栏是一个横向的布局 我们用 Row 就可以了,每Bar切换是与页面绑定的,而且Bar的个数 是不固定的,并且每个Bar都要与对应的页面相绑定。

新建一个 Bar的类存储每个导航栏的信息

class TabItem {
  final String title;
  final IconData iconData;

  TabItem({
    required this.iconData,
    required this.title,
  });
}

当然你还可以扩充 比如设置这个加个颜色什么之类的 。

简单的底部导航栏

效果

4t4h9-f2mj6.gif

通过传入的导航,根据导航数量进行构建底部导航栏,barItems里添加每个导航的样式。


List<Widget> _buildBarItems() {
  List<Widget> barItems = [];
  for (int i = 0; i < widget.tabItem.length; i++) {
    barItems.add(Expanded(
      child: Stack(
        fit: StackFit.expand,
        children: [
          SizedBox(
            height: double.infinity,
            width: double.infinity,
            child:  Padding(
              padding: const EdgeInsets.all(8.0),
              child: Text(
                widget.tabItem[i].title,
                style: const TextStyle(fontWeight: FontWeight.w600),
              ),
            )
          ),
          SizedBox(
            height: double.infinity,
            width: double.infinity,
            child:IconButton(
              highlightColor: Colors.transparent,
              splashColor: Colors.transparent,
              padding: const EdgeInsets.all(0),
              alignment: const Alignment(-0.8, 1.5),
              icon: Icon(
                widget.tabItem[i].iconData,
                color: widget.iconBackgroundColor,
              ),
              onPressed: () {
                setState(() {
                  barIndex = i;
                  widget.onBarTap(barIndex);
                });
              },
            ),
          )

        ],
      ),
    ));
  }
  return barItems;
}

把导航栏这个放入Row 横向布局里即可

class EasyBar extends StatefulWidget {
  final Color? iconBackgroundColor;
  final Color? backgroundColor;
  final  List<TabItem> tabItem;
  final Function onBarTap;

  const EasyBar(
      {Key? key,
        this.iconBackgroundColor = Colors.deepPurple,
        required this.tabItem,
        required this.onBarTap,
        this.backgroundColor = Colors.white})
      : super(key: key);

  @override
  State<EasyBar> createState() => _EasyBarState();
}

class _EasyBarState extends State<EasyBar> with TickerProviderStateMixin {

  double fabIconAlpha = 1;

  ///是1的话在中间
  int barIndex = 1;

  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      clipBehavior: Clip.none,
      alignment: Alignment.topCenter,
      children: <Widget>[
        Container(
          height: 65,
          padding: const EdgeInsets.only(bottom: 18),
          decoration: BoxDecoration(
              color: widget.backgroundColor,
              boxShadow: const [BoxShadow(color: Colors.black12, offset: Offset(0, -1), blurRadius: 8)]),
          child: Row(
            mainAxisSize: MainAxisSize.max,
            crossAxisAlignment: CrossAxisAlignment.center,
            children: _buildBarItems(),
          ),
        ),
      ],
    );
  }

把点击事件暴露出来处理,最主要的是页面的index 要与导航的index所绑定

int selectedBarIndex = 1;
List<Widget> pages = [];

@override
void initState() {
  super.initState();
  pages
    ..add(Scaffold(
      body: Container(width: double.infinity, height: double.infinity, color: Colors.lightBlue,),
    ))
    ..add(Scaffold(body: Container(width: double.infinity, height: double.infinity, color: Colors.pinkAccent)))
    ..add(Scaffold(
      body: Container(width: double.infinity, height: double.infinity, color: Colors.amber,),
    ));
}

@override
Widget build(BuildContext context) {
  return Scaffold(
    body: pages[selectedBarIndex],
    bottomNavigationBar: EasyBar(
      backgroundColor: Colors.pink,
      tabItem: widget.barItems,
      onBarTap: (index) {
        setState(() {
          //底部的index和页面绑定
          selectedBarIndex = index;
        });
      },
    ),
  );
}

动画的底部导航栏

效果

ipw9c-o5lky.gif

通过传入的导航的数量构建底部导航栏还是大同小异,唯一的区别是,当选中的时候 icon变透明,以及title从底部移动上来的两个动画效果 使用了 AnimatedOpacity 控制透明度 AnimatedAlign 来控制标题的位置

List<Widget> _buildBarItems() {
  List<Widget> barItems = [];
  for (int i = 0; i < widget.tabItem.length; i++) {
    barItems.add(Expanded(
      child: Stack(
        fit: StackFit.expand,
        children: [
          SizedBox(
            height: double.infinity,
            width: double.infinity,
            child: AnimatedAlign(
                // curve: Curves.easeIn,//动画曲线
                duration: Duration(milliseconds: widget.animationMilliseconds!),
                alignment: Alignment(0, (barIndex == i)? 3 : 8),//控制文字的位置
                child: Padding(
                  padding: const EdgeInsets.all(8.0),
                  child: Text(
                    widget.tabItem[i].title,
                    style: const TextStyle(fontWeight: FontWeight.w600),
                  ),
                )),
          ),
          SizedBox(
            height: double.infinity,
            width: double.infinity,
            child:AnimatedOpacity(
              duration: Duration(milliseconds: widget.animationMilliseconds!),//动画时长
              opacity: (barIndex == i) ? 0 : 1,////控制icon的透明度
              child: IconButton(
                highlightColor: Colors.transparent,
                splashColor: Colors.transparent,
                padding: const EdgeInsets.all(0),
                alignment: const Alignment(0, 0),
                icon: Icon(
                  widget.tabItem[i].iconData,
                  color: widget.iconBackgroundColor,
                ),
                onPressed: () {
                  setState(() {
                    barIndex = i;
                    widget.onBarTap(barIndex);
                    _positionTween.begin = _positionAnimation.value; //叠叠圆X轴开始位置
                    _positionTween.end = i - 1; //叠叠圆X轴结束的位置
                    _animationController.reset();
                    _fadeOutController.reset();
                    _animationController.forward();
                    _fadeOutController.forward();
                  });
                },
              ),
            ),
          )

        ],
      ),
    ));
  }
  return barItems;
}

image.png

这个圆我们观察他! 大圆叠小圆 上面还有个icon

叠叠圆

Positioned(
  top: -45,
  left: 0,
  right: 0,
  child: IgnorePointer(
    child: Container(
      decoration: const BoxDecoration(color: Colors.transparent),
      child: Align(
        heightFactor: 0.5,
        alignment: Alignment(_positionAnimation.value, -1),
        //_positionAnimation.value控制这个叠叠的圆的位置,根据点的导航修改圆的x轴的位置
        child: FractionallySizedBox(
          widthFactor: 1/3,//控制占的宽度
          child: Stack(
            alignment: Alignment.center,
            children: <Widget>[
              SizedBox(
                height: 90,
                width: 90,
                child: ClipRect(
                    clipper: HalfClipper(),//裁剪
                    child: Center(
                      child: Container(
                          width: 70,
                          height: 70,
                          decoration: BoxDecoration(
                              color: widget.backgroundColor,
                              shape: BoxShape.circle,
                              boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 8)])),
                    )),
              ),
              SizedBox(
                height: 60,
                width: 60,
                child: Container(
                  decoration: BoxDecoration(
                      shape: BoxShape.circle,
                      color: widget.iconBackgroundColor,
                      border: Border.all(color: Colors.white, width: 5, style: BorderStyle.none)),
                  child: Padding(
                    padding: const EdgeInsets.all(0.0),
                    child: Opacity(
                      opacity: fabIconAlpha,//白色图标切换时的动画
                      child: Icon(
                        widget.tabItem[barIndex].iconData,
                        color: Colors.white,
                      ),
                    ),
                  ),
                ),
              )
            ],
          ),
        ),
      ),
    ),
  ),
),

要想在stack中子组件超出父组件的约束 只需要stack的 clipBehavior属性设置 Clip.none

完整代码

import 'package:flutter/material.dart';
import 'package:vector_math/vector_math.dart' as vector;

class BottomBar extends StatefulWidget {
  final Color? iconBackgroundColor;
  final Color? backgroundColor;
  final  List<TabItem> tabItem;
  final Function onBarTap;
  final int? animationMilliseconds;

 const BottomBar(
      {Key? key,
      this.iconBackgroundColor = Colors.deepPurple,
      required this.tabItem,
      required this.onBarTap,
      this.animationMilliseconds = 300,
      this.backgroundColor = Colors.white})
      : super(key: key);

  @override
  State<BottomBar> createState() => _BottomBarState();
}

class _BottomBarState extends State<BottomBar> with TickerProviderStateMixin {
  late AnimationController _animationController;
  late Tween<double> _positionTween;
  late Animation<double> _positionAnimation;

  late AnimationController _fadeOutController;
  late Animation<double> _fadeFabOutAnimation;
  late Animation<double> _fadeFabInAnimation;

  double fabIconAlpha = 1;

  ///是1的话在中间
 int barIndex = 1;

  @override
  void initState() {
    super.initState();

    _animationController =
        AnimationController(vsync: this, duration: Duration(milliseconds: widget.animationMilliseconds!));
    _fadeOutController =
        AnimationController(vsync: this, duration: Duration(milliseconds: (widget.animationMilliseconds! ~/ 5)));

    _positionTween = Tween<double>(begin: 0, end: 0);
    _positionAnimation = _positionTween.animate(CurvedAnimation(parent: _animationController, curve: Curves.easeOut))
      ..addListener(() {
        setState(() {});
      });

    _fadeFabOutAnimation =
        Tween<double>(begin: 1, end: 0).animate(CurvedAnimation(parent: _fadeOutController, curve: Curves.easeOut))
          ..addListener(() {
            setState(() {
              fabIconAlpha = _fadeFabOutAnimation.value;
            });
          })
          ..addStatusListener((AnimationStatus status) {
            if (status == AnimationStatus.completed) {
              setState(() {});
            }
          });

    _fadeFabInAnimation = Tween<double>(begin: 0, end: 1)
        .animate(CurvedAnimation(parent: _animationController, curve: const Interval(0.8, 1, curve: Curves.easeOut)))
      ..addListener(() {
        setState(() {
          fabIconAlpha = _fadeFabInAnimation.value;
        });
      });
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      clipBehavior: Clip.none,
      alignment: Alignment.topCenter,
      children: <Widget>[
        Container(
          height: 65,
          padding: const EdgeInsets.only(bottom: 18),
          decoration: BoxDecoration(
              color: widget.backgroundColor,
              boxShadow: const [BoxShadow(color: Colors.black12, offset: Offset(0, -1), blurRadius: 8)]),
          child: Row(
            mainAxisSize: MainAxisSize.max,
            crossAxisAlignment: CrossAxisAlignment.center,
            children: _buildBarItems(),
          ),
        ),
        Positioned(
          top: -45,
          left: 0,
          right: 0,
          child: IgnorePointer(
            child: Container(
              decoration: const BoxDecoration(color: Colors.transparent),
              child: Align(
                heightFactor: 0.5,
                alignment: Alignment(_positionAnimation.value, -1),//_positionAnimation.value控制这个叠叠的圆的位置,根据点的导航修改圆的x轴的位置
                child: FractionallySizedBox(
                  widthFactor: 1/3,//控制占的宽度
                  child: Stack(
                    alignment: Alignment.center,
                    children: <Widget>[
                      SizedBox(
                        height: 90,
                        width: 90,
                        child: ClipRect(
                            clipper: HalfClipper(),//裁剪
                            child: Center(
                              child: Container(
                                  width: 70,
                                  height: 70,
                                  decoration: BoxDecoration(
                                      color: widget.backgroundColor,
                                      shape: BoxShape.circle,
                                      boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 8)])),
                            )),
                      ),
                      SizedBox(
                        height: 60,
                        width: 60,
                        child: Container(
                          decoration: BoxDecoration(
                              shape: BoxShape.circle,
                              color: widget.iconBackgroundColor,
                              border: Border.all(color: Colors.white, width: 5, style: BorderStyle.none)),
                          child: Padding(
                            padding: const EdgeInsets.all(0.0),
                            child: Opacity(
                              opacity: fabIconAlpha,//白色图标切换时的动画
                              child: Icon(
                                widget.tabItem[barIndex].iconData,
                                color: Colors.white,
                              ),
                            ),
                          ),
                        ),
                      )
                    ],
                  ),
                ),
              ),
            ),
          ),
        ),
      ],
    );
  }

  List<Widget> _buildBarItems() {
    List<Widget> barItems = [];
    for (int i = 0; i < widget.tabItem.length; i++) {
      barItems.add(Expanded(
        child: Stack(
          fit: StackFit.expand,
          children: [
            SizedBox(
              height: double.infinity,
              width: double.infinity,
              child: AnimatedAlign(
                  // curve: Curves.easeIn,//动画曲线
                  duration: Duration(milliseconds: widget.animationMilliseconds!),
                  alignment: Alignment(0, (barIndex == i)? 3 : 8),//控制文字的位置
                  child: Padding(
                    padding: const EdgeInsets.all(8.0),
                    child: Text(
                      widget.tabItem[i].title,
                      style: const TextStyle(fontWeight: FontWeight.w600),
                    ),
                  )),
            ),
            SizedBox(
              height: double.infinity,
              width: double.infinity,
              child:AnimatedOpacity(
                duration: Duration(milliseconds: widget.animationMilliseconds!),//动画时长
                opacity: (barIndex == i) ? 0 : 1,////控制icon的透明度
                child: IconButton(
                  highlightColor: Colors.transparent,
                  splashColor: Colors.transparent,
                  padding: const EdgeInsets.all(0),
                  alignment: const Alignment(0, 0),
                  icon: Icon(
                    widget.tabItem[i].iconData,
                    color: widget.iconBackgroundColor,
                  ),
                  onPressed: () {
                    setState(() {
                      barIndex = i;
                      widget.onBarTap(barIndex);
                      _positionTween.begin = _positionAnimation.value; //圆开始位置
                      _positionTween.end = i - 1; //圆结束的位置
                      _animationController.reset();
                      _fadeOutController.reset();
                      _animationController.forward();
                      _fadeOutController.forward();
                    });
                  },
                ),
              ),
            )

          ],
        ),
      ));
    }
    return barItems;
  }
}

//裁剪成一半
class HalfClipper extends CustomClipper<Rect> {
  @override
  Rect getClip(Size size) {
    final rect = Rect.fromLTWH(0, 0, size.width, size.height / 2);
    return rect;
  }

  @override
  bool shouldReclip(CustomClipper<Rect> oldClipper) {
    return true;
  }
}

class TabItem {
  final String title;
  final IconData iconData;

  TabItem({
    required this.iconData,
    required this.title,
  });
}

ENDING 动画真好玩~