flutter 底部导航栏动画效果

import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:vnm_version/config/app_colors.dart';

typedef CallBack = void Function(int);

class GoCodeNavigationBarWidget extends StatefulWidget {
  ///选中了第几个导航按钮,总计五个,依次是:Home,Casino,Live,More,Me
  CallBack callBack;

  GoCodeNavigationBarWidget({required this.callBack, super.key});

  @override
  State<StatefulWidget> createState() {
    return _GoCodeNavigationBarState();
  }
}

class _GoCodeNavigationBarState extends State<GoCodeNavigationBarWidget>
    with TickerProviderStateMixin {
  ///动画控制器
  late AnimationController controller;

  ///组件移动的动画
  late Animation<Offset> animationHorizontal;
  Offset? lastOffsetEndHorizontal;

  ///组件向上移动的动画
  late Animation<Offset> animationUp;

  ///组件向下移动的动画()
  late Animation<Offset> animationDown;

  // ///组件向上移动时候的颜色渐变动画
  // late Animation<Offset> animationColorUp;
  //
  // ///组件向下移动时候的颜色渐变动画()
  // late Animation<Offset> animationColorDown;

  ///动画每移动一个区间耗费的时间
  int timeConsumedForEachInterval = 2;

  // ///用户初始化选中的图标,默认是中间的LIVE
  // int defaultSelectedIndex = 2;

  ///用户上次点击选中的图标,默认是中间的LIVE,即2
  int lastSelectedIndex = 2;

  ///用户本次选中的图标
  int thisSelectedIndex = -1;

  ///动画是否初始化
  bool _initializedAnimation = false;

  ///图片大小的变化动画
  var pictureSizeChange;
  var pictureSizeChangeController;

  ///是否在播放动画,播放动画的时候禁止点击
  bool isPlayingAnimation = false;

  String coverIconPath = "assets/images/navigation_bar_move_bg_normal.png";

  ///启动动画的效果,参数为当前tab的索引
  void _animation(int locationIndex) {
    ///移动区间的个数
    int intervals = locationIndex - lastSelectedIndex;
    if (intervals == 0) {
      return;
    }
    _initializedAnimation = true;

    ///注意,千万不要添加addStatusListener监听,更不能在status == AnimationStatus.completed的时候赋值selectedIndex = locationIndex,我就被这句代码坑惨了
    controller = AnimationController(
        duration:
            Duration(seconds: intervals.abs() * timeConsumedForEachInterval),
        vsync: this)
      ..addListener(() {
        ///计算距离左右边缘多少距离的时候改变图片
        bool isNearEdge = ScreenUtil().screenWidth / 5 * intervals.abs() -
                ScreenUtil().screenWidth /
                    5 *
                    intervals.abs() *
                    controller.value <
            30.w;
        bool isFarEdge =
            ScreenUtil().screenWidth / 5 * intervals.abs() * controller.value >
                30.w;
        if (locationIndex == 0 && isNearEdge) {
          coverIconPath = "assets/images/navigation_bar_move_bg_left.png";
        } else if (locationIndex == 4 && isNearEdge) {
          coverIconPath = "assets/images/navigation_bar_move_bg_right.png";
        } else {
          if (lastSelectedIndex == 0 && !isFarEdge) {
            coverIconPath = "assets/images/navigation_bar_move_bg_left.png";
          } else if (lastSelectedIndex == 4 && !isFarEdge) {
            coverIconPath = "assets/images/navigation_bar_move_bg_right.png";
          } else {
            coverIconPath = "assets/images/navigation_bar_move_bg_normal.png";
          }
        }
        setState(() {});
      })
      ..addStatusListener((status) {
        if (status == AnimationStatus.forward) {
          isPlayingAnimation = true;
        }
        if (status == AnimationStatus.completed) {
          isPlayingAnimation = false;
          setState(() {
            lastSelectedIndex = locationIndex;
            animationUp = Tween(begin: Offset(0.w, 0.h), end: Offset(0.w, 0.h))
                .animate(controller);
            animationDown =
                Tween(begin: Offset(0.w, 0.h), end: Offset(0.w, 0.h))
                    .animate(controller);
          });
        }
      });
    Offset nowOffsetHorizontal = Offset(
        (locationIndex - 2) * (ScreenUtil().screenWidth / 5 / 104.w), 0.h);
    animationHorizontal = Tween(
            begin: lastOffsetEndHorizontal ?? Offset(0.w, 0.h),
            end: nowOffsetHorizontal)
        .animate(controller);
    lastOffsetEndHorizontal = nowOffsetHorizontal;
    animationUp = Tween(begin: Offset(0.w, 0.h), end: Offset(0.w, -0.44.h))
        .animate(controller);
    animationDown = Tween(begin: Offset(0.w, 0.h), end: Offset(0.w, 0.3548.h))
        .animate(controller);
    pictureSizeChange = Tween(begin: 0.0, end: 1.0);
    controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
        width: double.infinity,
        // height: 486.h,
        decoration: const BoxDecoration(
            image: DecorationImage(
                image: AssetImage("assets/images/main_navigation_bg.png"),
                fit: BoxFit.fitWidth,
                alignment: Alignment.bottomCenter)),
        child: Stack(
          children: [
            ///左右移动的图片要和上下移动的图片在不同的层级,这样可以保证在动画过程中左右移动的图片肯定会被5种item图片覆盖
            backgroundWidget(),
            Positioned.fill(
                child: Row(
              crossAxisAlignment: CrossAxisAlignment.end,
              children: List.generate(5, (index) => tab(index)),
            ))
          ],
        ));
  }

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

  Expanded tab(int locationIndex) {
    return Expanded(
        child: InkWell(
            onTap: isPlayingAnimation
                ? null
                : () {
                    // coverIconPath =
                    //     "assets/images/navigation_bar_move_bg_normal.png";
                    _animation(locationIndex);
                    widget.callBack(locationIndex);
                    thisSelectedIndex = locationIndex;
                  },
            child: SingleChildScrollView(
              child: Column(
                mainAxisSize: MainAxisSize.min,
                mainAxisAlignment: MainAxisAlignment.end,
                children: [
                  showNavigationBarIcon(locationIndex),
                  SizedBox(
                      height: lastSelectedIndex == locationIndex ? 5.h : 11.h),
                  _showText(locationIndex),
                  SizedBox(height: 3.h),
                ],
              ),
            )));
  }

  SizedBox _showText(int locationIndex) {
    String text = "";
    switch (locationIndex) {
      case 0:
        text = AppLocalizations.of(context)?.homeTab ?? "Home";
        break;
      case 1:
        text = AppLocalizations.of(context)?.casinoTab ?? "Casino";
        break;
      case 2:
        text = AppLocalizations.of(context)?.liveTab ?? "LIVE";
        break;
      case 3:
        text = AppLocalizations.of(context)?.moreTab ?? "More";
        break;
      case 4:
        text = AppLocalizations.of(context)?.meTab ?? "Me";
        break;
      default:
        text = AppLocalizations.of(context)?.liveTab ?? "LIVE";
        break;
    }
    return SizedBox(
      height: 15.h,
      child: Center(child: Text(text, style: _getTextStyle(locationIndex))),
    );
  }

  TextStyle _getTextStyle(int nowIndex) {
    return lastSelectedIndex == nowIndex
        ? TextStyle(
            fontSize: 11.sp,
            color: AppColors.termsText0,
            fontWeight: FontWeight.bold)
        : TextStyle(fontSize: 12.sp, color: AppColors.passwordUnselected);
  }

  Widget showNavigationBarIcon(int locationIndex) {
    if (lastSelectedIndex == locationIndex) {
      return SlideTransition(
          position: _initializedAnimation
              ? animationDown
              : const AlwaysStoppedAnimation<Offset>(Offset.zero),
          child: Container(
              margin: EdgeInsets.only(bottom: 20.h),
              width: !_initializedAnimation
                  ? 31.w
                  : 31.w -
                      (31.w - 25.w) * pictureSizeChange.evaluate(controller),
              height: !_initializedAnimation
                  ? 31.h
                  : 31.h -
                      (31.h - 25.h) * pictureSizeChange.evaluate(controller),
              child: Image.asset(
                getIconPath(locationIndex),
              )));
    } else if (thisSelectedIndex == locationIndex) {
      return SlideTransition(
        position: _initializedAnimation
            ? animationUp
            : const AlwaysStoppedAnimation<Offset>(Offset.zero),
        child: getUpImage(getIconPath(locationIndex)),
      );
    } else {
      return getNormalImage(getIconPath(locationIndex));
    }
  }

  Image getUpImage(String iconPath) {
    return Image.asset(iconPath,
        width: !_initializedAnimation
            ? 25.w
            : 25.w + (31.w - 25.w) * pictureSizeChange.evaluate(controller),
        height: _initializedAnimation
            ? 25.h
            : 25.h + (31.h - 25.h) * pictureSizeChange.evaluate(controller));
  }

  Image getNormalImage(String iconPath) {
    return Image.asset(iconPath, width: 25.w, height: 25.h);
  }

  Row backgroundWidget() {
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [tabBg()],
    );
  }

  Stack tabBg() {
    return Stack(children: [
      SlideTransition(
        position: _initializedAnimation
            ? animationHorizontal
            : const AlwaysStoppedAnimation<Offset>(Offset.zero),
        child: Container(
            width: 104.w,
            margin: EdgeInsets.only(bottom: 22.h),

            ///去除切换图片时候的闪烁效果
            child: FadeInImage(
              placeholder: AssetImage(coverIconPath),
              image: AssetImage(coverIconPath),
            )),
      )
    ]);
  }

  String getIconPath(int locationIndex) {
    String iconPath = "";
    switch (locationIndex) {
      case 0:
        iconPath = locationIndex == lastSelectedIndex
            ? "assets/images/home_selected.png"
            : "assets/images/home_unselected.png";
        break;
      case 1:
        iconPath = locationIndex == lastSelectedIndex
            ? "assets/images/casino_selected.png"
            : "assets/images/casino_unselected.png";
        break;
      case 2:
        iconPath = locationIndex == lastSelectedIndex
            ? "assets/images/live_selected.png"
            : "assets/images/live_unselected.png";
        break;
      case 3:
        iconPath = locationIndex == lastSelectedIndex
            ? "assets/images/more_selected.png"
            : "assets/images/more_unselected.png";
        break;
      case 4:
        iconPath = locationIndex == lastSelectedIndex
            ? "assets/images/me_selected.png"
            : "assets/images/me_unselected.png";
        break;
      default:
        iconPath = locationIndex == lastSelectedIndex
            ? "assets/images/home_selected.png"
            : "assets/images/home_unselected.png";
        break;
    }
    return iconPath;
  }
}

看下效果(视频效果和图片资源已上传)

flutter底部导航栏的动画

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值