Code前端首页关于Code前端联系我们

在 Flutter 中实现浮动导航栏

terry 2年前 (2023-09-23) 阅读数 70 #移动小程序

在学习 Flutter 时,我看到了下面的 dribble 上的导航栏设计。我实在是太喜欢了,所以就想着如何在Flutter中实现这样的效果。 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
  ]; //导航项
复制代码

然后在te:in中初始化一些变量这就是我要浮动图标的位置半径是高度的三分之二导航栏,动画的持续时间为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,图标会向上移动(移动距离可根据需要调整)。

这里对于水平移动,我们使用_moveTween函数,因为移动距离的倍数是最终半径❙❙❙❙❙间隙的,这里的列之间的导航是指元素的长度(例如从索引 0 到索引 3)。下一步是突出显示。 MapEntryArcPainter 用于绘制背景。我们来看看如何绘制背景(别慌,后面我会讲解_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*方法就在这里,因为一笔划完之后不需要考虑下一笔的起始位置,比较方便,我很喜欢。

这里(对我来说)最复杂的部分是绘制弯曲部分,它使用了三次贝塞尔曲线(我在草图纸上手动绘制了每个点的位置,但我没办法,就这么简单) ),需要注意的是,左边的圆弧画完一半后,的原点移动到圆弧的底部,所以画右半圆弧的坐标与左半部分,剩下的直接画就可以了。

最后一步是应用动画控制方法_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();
    }
}
复制代码

每次点击切换导航时,都会重新启动。分配beginend值的动画来确定真实距离。当动画完成时,当前激活元素将被更新。

还有一点,我差点错过了,销毁动画控制器:

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

至此代码就完成了,我们来看看动态效果:Flutter 中实现一个浮动导航栏

五个导航元素Flutter 中实现一个浮动导航栏

四个导航元素Flutter 中实现一个浮动导航栏

三个看来导航元素越少越好看。点击这里查看完整代码

最后

我只能说这个效果总体来说是成功的,但是还是有差距:

  • 当圆弧移动时,导航元素的图标路径没有隐藏
  • 浮动图标中的图标是动画执行后切换的新图标

作者:MeFelixWang
链接:https://juejin.im/post/5cece3ae6fb9a07ee来源:掘金
版权属于作者。商业转载请联系作者获得许可。非商业转载请注明来源。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门