博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
在 Flutter 中实现一个浮动导航栏
阅读量:6548 次
发布时间:2019-06-24

本文共 6773 字,大约阅读时间需要 22 分钟。

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

写在前面

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

设计图作者:

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

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

重点阅读

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

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

class FloatNavigator extends StatefulWidget {  @override  _FloatNavigatorState createState() => _FloatNavigatorState();}class _FloatNavigatorState extends State
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:
[ //浮动图标 //所有图标 ], ), ), ) ]), ); }} 复制代码

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

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

int _activeIndex = 0; //激活项  double _height = 48.0; //导航栏高度  double _floatRadius; //悬浮图标半径  double _moveTween = 0.0; //移动补间  double _padding = 10.0; //浮动图标与圆弧之间的间隙  AnimationController _animationController; //动画控制器  Animation
_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();  }复制代码

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

五个导航项

四个导航项

三个导航项

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

最后叨叨

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

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

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

转载地址:http://iegdo.baihongyu.com/

你可能感兴趣的文章
实战CGLib系列之proxy篇(一):方法拦截MethodInterceptor
查看>>
php 字符串截取
查看>>
ttcn-3
查看>>
00.java虚拟机的基本结构概念
查看>>
ThreadLocal使用出现的问题
查看>>
连接池并发的实现原理
查看>>
创建Pch预编译文件
查看>>
阿里云Centos配置iptables防火墙
查看>>
UML类图几种关系的总结
查看>>
PHP面试题汇总
查看>>
LeetCode (11): Container With Most Water
查看>>
【技巧】easyUI的datagrid,如何在翻页以后仍能记录被选中的行
查看>>
经过强制类型转换以后,变量a, b的值分别为( )short a = 128; byte b = (byte) a;
查看>>
ubuntu下msmtp+mutt的安装和配置
查看>>
QLabel显示图片,图片可以自适应label的大小
查看>>
BZOJ3994:[SDOI2015]约数个数和——题解
查看>>
3、EJB3.0开发第一个无会话Bean和客户端(jboss4.2.3)
查看>>
git fetch & pull详解
查看>>
boost_1.63.0编译VS2013
查看>>
jQuery 插件-(初体验一)
查看>>