在 Flutter 中实现一个浮动导航栏

2023-11-18

此图与正文无关,只是为了好看

写在前面

这段时间一直在学习 Flutter,在 dribble 上看到一张导航栏设计图,就是下面这张,感觉很是喜欢,于是思考着如何在 Flutter 中实现这个效果。

设计图作者:Lukáš Straňák

经过一番研究,大体上算是实现了效果(有些地方还是需要改进的),如下:

这篇文章和大家分享一下实现过程,一起交流、学习。

重点阅读

实现这个效果主要用到了 AnimationControllerCustomPaint,切换导航时进行重新绘制。

首先搭建一下整个页面的骨架:

class FloatNavigator extends StatefulWidget {
  @override
  _FloatNavigatorState createState() => _FloatNavigatorState();
}
class _FloatNavigatorState extends State<FloatNavigator>
    with SingleTickerProviderStateMixin {
    
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Stack(children: [
        Scaffold(
          appBar: AppBar(
            backgroundColor: Colors.transparent,
            elevation: 0.0,
            title: Text('Float Navigator'),
            centerTitle: true,
          ),
          backgroundColor: Color(0xFFFF0035),
        ),
        Positioned(
          bottom: 0.0,
          child: Container(
            width: width,
            child: Stack(
              overflow: Overflow.visible,
              children: <Widget>[
                //浮动图标
                //所有图标
              ],
            ),
          ),
        )
      ]),
    );
  }
}    
复制代码

这里将图中的导航分成两个部分,一个是浮动图标,另一个是所有图标浮动图标在点击的时候会移动到所有图标中对应图标的位置,而所有图标上的圆弧状缺口也会一起移动。

接下来,在 _FloatNavigatorState 定义一些变量,以供使用:

  int _activeIndex = 0; //激活项
  double _height = 48.0; //导航栏高度
  double _floatRadius; //悬浮图标半径
  double _moveTween = 0.0; //移动补间
  double _padding = 10.0; //浮动图标与圆弧之间的间隙
  AnimationController _animationController; //动画控制器
  Animation<double> _moveAnimation; //移动动画
  List _navs = [
    Icons.search,
    Icons.ondemand_video,
    Icons.music_video,
    Icons.insert_comment,
    Icons.person
  ]; //导航项
复制代码

接着在 initState 中对一些变量做初始化:

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

这里我将悬浮图标的半径设置为导航栏高度的三分之二,动画时长设置为 400 毫秒,当然这里面的参数都是可以改动的。

接着,实现悬浮图标:

//悬浮图标
Positioned(
  top: _animationController.value <= 0.5
      ? (_animationController.value * _height * _padding / 2) -
          _floatRadius / 3 * 2
      : (1 - _animationController.value) *
              _height *
              _padding /
              2 -
          _floatRadius / 3 * 2,
  left: _moveTween * singleWidth +
      (singleWidth - _floatRadius) / 2 -
      _padding / 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(_navs[_activeIndex], color: Colors.black)),
  ),
)
复制代码

这里的 top 值看上去很复杂,但实际上并没什么特别的,只是为了让悬浮图标上下移动而已,_animationController 产生的值为 0.0 到 1.0,因此,这里判断如果小于等于 0.5,就让图标向下移动,大于 0.5 则向上移动(移动距离可以随意修改)。

left 做横向移动,这里使用的是 _moveTween,因为移动的距离是 singleWidth 的倍数(当然最终移动距离还要减去半径及间隙,这里的倍数是指列如从索引 0 移动到索引 3 这之间途径的导航项长度)。

再向下就是重头戏了,所有图标的绘制:

CustomPaint(
  child: SizedBox(
    height: _height,
    child: Row(
      mainAxisAlignment: MainAxisAlignment.spaceAround,
      crossAxisAlignment: CrossAxisAlignment.center,
      children: _navs
          .asMap()
          .map((i, v) => MapEntry(
              i,
              GestureDetector(
                child: Icon(v,
                    color: _activeIndex == i
                        ? Colors.transparent
                        : Colors.grey),
                onTap: () {
                  _switchNav(i);
                },
              )))
          .values
          .toList(),
    ),
  ),
  painter: ArcPainter(
      navCount: _navs.length,
      moveTween: _moveTween,
      padding: _padding),
)
复制代码

这里需要用到索引来确定每次点击的是第几个导航,所以用到了 asMapMapEntryArcPainter 就是用来绘制背景的,来看一下绘制背景的实现(不要慌,_switchNav 方法我会在后面解释的):

//绘制圆弧背景
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;
  }
}
复制代码

先将整个导航栏背景的外框绘制出来,再填充成白色,就能得到我们想要的带圆弧形缺口的形状。Flutter 中的绘制方法有两种(并不完全是这样,有的方法只有一种),拿 relativeLineTo 来说,与其对应的另一个方法是 lineTo。两者的区别在于,relativeLineTo 在绘制结束后,会将结束点作为新的坐标系原点(0,0),而 lineTo 的原点始终在左上角(这个说法不严谨,两个方法的原点都是左上角,这里的意思是,它不会移动)。我这里使用的 relative* 方法就是因为不用绘制一笔后还要考虑下一笔开始的位置,比较方便,我很喜欢。

这里最复杂(对我来说)的就是圆弧部分的绘制了,用到了三次贝塞尔曲线(自己手工在草稿纸上画了一下每个点的位置,没办法,就是这么菜),需要注意的是,在绘制完圆弧左半边后,原点移动到了圆弧最底部,因此绘制右半边圆弧的坐标与左半边是相反的,剩下的就直接画就行。

最后一步,实现 _FloatNavigatorState 中的动画控制方法 _switchNav:

//切换导航
_switchNav(int newIndex) {
    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();
    }
}
复制代码

这里每次点击切换导航的时候都重新给 _moveAnimationbeginend 赋值,来确定要移动的真正距离,当动画执行完成后,更新当前激活项。

还有一点,差点漏了,销毁动画控制器:

  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }
复制代码

至此,代码就写完了,看一下动态效果:

五个导航项

四个导航项

三个导航项

感觉导航项少一些似乎更好看,完整代码请点这里

最后叨叨

只能说大体上实现了这个效果,但还是有一些不足:

  • 圆弧在移动的时候,途径的导航项图标没有隐藏
  • 悬浮图标中的图标是在动画执行结束后才切换的新图标

这些不足还是会让最终效果不那么完美,但现已足够。大家有什么好的想法或建议可以交流,畅所欲言。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

在 Flutter 中实现一个浮动导航栏 的相关文章

  • 一些大厂的开源平台

    百度 http fex baidu com http efe baidu com 饿了么 https fe ele me 腾讯 http www alloyteam com 美团 https tech meituan com 滴滴 http
  • Qt5学习之路(vs2012下创建一个QT应用程序)2013-10-14

    刚开始学习QT在网上找的资料基本都是使用QT Create进行开发的 VS下开发的学习资料感觉很少很难找的到 视频教程也基本没看到过貌似 因为我们研发中心是使用MFC进行开发开发工具是VS2010 使用QT开发的话基本我们不会再使用QT C
  • UE4命令行使用,解释

    命令行在外部 从命令行运行编辑项目 1 导航到您的 LauncherInstall VersionNumber Engine Binaries Win64 目录中 2 右键单击上 UE4Editor exe 的可执行文件 并选择创建快捷方式
  • 【前端】Vue+Element UI案例:通用后台管理系统-项目总结

    文章目录 相关链接 前言 效果 登录页 首页 管理员的首页 xiaoxiao的首页 用户管理 总结 项目搭建 左侧 CommonAside 上侧 CommonHeader和CommonTag 首页 Home vue 用户管理 User vu
  • ​第一本 Compose 图书上市,联想大咖教你学会 Android 全新 UI 编程

    朱江 现任联想 北京 有限公司 Android 开发工程师 从事 Android 开发工作多年 有丰富的项目经验 负责和参与开发过多款移动应用程序 同时还是多个开源项目的作者 2017 年开始在 CSDN 发表 Android 技术相关博文
  • Qt的基本语法及其使用(一)

    Qt的概念 Qt是通用的C 开发界面框架 C 图形用户界面 应用程序开发框架 既可以开发GUI程序也可以开发开发非GUI程序 Qt是面向对象的框架 使用特殊的代码生成扩展 Qt的历史 1991由QT公司研发 2008年被诺基亚收购 2012
  • iOS 自定义弹出框

    2019独角兽企业重金招聘Python工程师标准 gt gt gt 在iOS中 系统再带的弹出窗体不好扩展 开发时候不如自定义一个弹出窗体 附加上显示和消失的动画 弹出窗体父类如下 具体效果直接往上面添加控件就行 ViewControlle
  • Qt 信号与槽

    Qt 信号与槽 在这章节里 我们学习 Qt 的信号与槽 这里分一个章节来学习这个 Qt 的信号与槽 可见 这个信号与槽有多么重要 在学习 Qt 的过程中 信号与槽是必不可少的部分 也是 Qt 编程的 基础 是 Qt 编程的一大创新 其实与
  • 数理统计知识整理——回归分析与方差分析

    题记 时值我的北科研究生第一年下 选学 统计优化 课程 备考促学 成此笔记 以谨记 1 线性回归 1 1 原理分析 要研究最大积雪深度x与灌溉面积y之间的关系 测试得到近10年的数据如下表 使用线性回归的方法可以估计x与y之间的线性关系 线
  • ios -Unity3D的EasyAR集成到已经有项目中。

    近期 在做AR这一块 用EasyAR集成到iOS端 由于现在到项目已经上线 下一版本要做一个AR功能 于是迫于需求需要 自己研究和翻阅读好多集成到资料 通过整理分出几个重要到模块 其中在这里指出Xcode9版本确实好坑 建议弃坑 该用稍微好
  • Linux宝塔面板命令大全,快速学会

    cd www server panel python tools py panel 123456 查看宝塔日志 cat tmp panelBoot pl 查看软件安装日志 cat tmp panelExec log 站点配置文件位置 www
  • 移动端H5开发遇到的问题

    移动端开发必会出现的问题和解决方案 H5开发过程中难免会遇到一些兼容性等爬过坑的问题 移动端 H5 相关问题汇总 1px 问题 响应式布局 iOS 滑动不流畅 iOS 上拉边界下拉出现白色空白 页面件放大或缩小不确定性行为 click 点击
  • UI自动化测试的正确姿势 —— Airtest设备连接&API详解第一篇

    一 背景 Airtest作为一款优秀的自动化测试工具 有着强大的API功能 处理日常自动化测试过程中需要的各类操作 今天就给大家逐一介绍关于设备连接和常用API部分 结合自动化测试中的各类需求 看看如何通过使用Airtest来快速实现 二
  • UI自动化测试方案

    2024软件测试面试刷题 这个小程序 永久刷题 靠它快速找到工作了 刷题APP的天花板 CSDN博客 文章浏览阅读1 3k次 点赞60次 收藏8次 你知不知道有这么一个软件测试面试的刷题小程序 里面包含了面试常问的软件测试基础题 web自动
  • app测试必掌握的核心测试:UI、功能测试!

    一 UI测试 UI即User Interface 用户界面 的简称 UI 设计则是指对软件的人机交互 操作逻辑 界面美观的整体设计 好的UI设计不仅是让软件变得有个性有品味 还要让软件的操作变得舒适 简单 自由 充分体现软件的定位和特点 手
  • 鸿蒙开发之页面路由(router)

    页面路由 router 页面路由指在应用程序中实现不同页面之间的跳转和数据传递 HarmonyOS提供了Router模块 通过不同的url地址 可以方便地进行页面路由 轻松地访问不同的页面 本文将从 页面跳转 页面返回 和 页面返回前增加一
  • 独立搭建UI自动化测试框架分享

    今天给大家分享一个selenium testng maven ant的UI自动化 可以用于功能测试 也可按复杂的业务流程编写测试用例 今天此篇文章不过多讲解如何实现CI CD 只讲解自己能独立搭建UI框架 如果有其他好的框架也可以联系我 分
  • element ui弹窗在别的弹窗下方,优先级不高的问题

    在 弹窗 的标签中加入append to body即可解决该问题
  • 界面控件DevExpress WPF属性网格 - 让应用轻松显示编辑各种属性事件

    DevExpress WPF Property Grid 属性网格 灵感来自于Visual Studio Visual Studio启发的属性窗口 对象检查器 让在WPF应用程序显示和编辑任何对象的属性和事件变得更容易 P S DevExp
  • 一文让你了解UI自动化测试

    测试都起什么作用 是项目的保险 但不是项目的救命草 测试无实际产出 但作用远大于实际产出 测试是从项目维度保证质量 而不是测试阶段 UI自动化 下面简称自动化 基于UI进行自动功能测试 以Web端作为例子 一般的UI功能自动化都是基于HTM

随机推荐