Animejs-draggable 导航栏拖拽切换动画

1,294 阅读5分钟

有段时间没更文了,起初动画库选的 GSAP,奈何后面发现英文文档实在难以下咽,而且官方案例又很少,学习难度增大。

GSAP和Animejs实际用法上大差不差,Animejs属于后起之秀吧,写法上可能稍微比前者有所优化,所以我不会太惧怕🙄

20年刚入行开始学动画的时候也是选的 Animejs,可能当时还没崛起。很多功能并没有GSAP那么全面,不过现在看起来。Animejs才是真正的神,这几年社区发展的很快,很多功能也都赶上了,更重要的是有 中文文档,案例也都很完善,建议大家少走弯路,选它!选它!选它!

背景

前端现阶段也有很多组件库,但很多时候如果想要自定义动画效果,例如轮播图,导航栏,拖拽等效果就有点费劲了。

如果大家对这些没有过多要求的话,那你可以忽略这篇文章。因为下面只是为了单纯的让你了解anime一些特性,让你可以做出自定义的动画效果,实际应用上和市面上的组件库差不太多。

效果展示

anime-draggable.gif

以上是实机演示效果,其中包括3点:

  1. 头部导航点击定位
  2. 内容部分可拖拽定位
  3. 底部上一页,下一页点击定位

如果你对这个效果很感兴趣,那么请继续往下看,会针对以上3点进行功能拆解,当然你也可以直接到文章底部看 👉在线代码源码😎。

实现步骤

引入anime的方式很简单,你也可以直接参考 👉官方文档

引入animejs

npm install animejs
import { animate } from 'animejs';

创建拖拽容器

anime-draggable1.gif

引入animeDraggable 插件

import { createDraggable } from 'animejs';
div id="bounded-flick" class="flicker container margin">
      <ul class="carousel">
        <li class="draggable carousel-item">1</li>
        <li class="draggable carousel-item">2</li>
        <li class="draggable carousel-item">3</li>
        <li class="draggable carousel-item">4</li>
      </ul>
</div>
const boundedFlickWidth = 280 + 10; // 定义每个轮播项的宽度(280px内容 + 10px间距)
// 获取轮播项的数量(计算轮播内容的总宽度需要)
const boundedFlickLength = utils.$("#bounded-flick .carousel-item").length;
// 设置轮播容器的总宽度(所有轮播项宽度之和)
utils.set("#bounded-flick .carousel", {
  width: `${boundedFlickLength * boundedFlickWidth}`
});
// -10px间距,280px为每个item的真实宽高
utils.set([".container", ".carousel-item"], {
  width: boundedFlickWidth - 10,
  height: boundedFlickWidth - 10
});
// 创建可拖拽的轮播容器
const boundedFlicker = createDraggable("#bounded-flick .carousel", {
  // Array<Number> ([top, right, bottom, left]) 设置X轴可拖动范围
  container: [0, 0, 0, -boundedFlickWidth * (boundedFlickLength - 1)],
  y: false, // 禁止垂直拖动
  /**
   * 将两个轴或一个特定轴的最终值四舍五入到最接近的指定增量。
   * 如果提供一个 Array 作为增量,它将从数组中选择最接近的值
   */
  snap: boundedFlickWidth, // 拖动时以290px为单位吸附(实现分页效果)
  // 更新时,每次拖拽更新时就会执行此方法
  onUpdate: (e) => {
    ...
  }
});

创建头部nav导航

anime-draggable2.gif

引入animeUtils 插件

import { utils } from 'animejs';
<ul class="navigation">
    <li class="nav-item">1</li>
    <li class="nav-item">2</li>
    <li class="nav-item">3</li>
    <li class="nav-item">4</li>
</ul>
const boundedFlicker = createDraggable("#bounded-flick .carousel", {
   ...
  // 更新时,每次拖拽更新时就会执行此方法
  onUpdate: (e) => {
    // 牵涉到 activeIndex 下标位置,因此在每次更新时需要获取最新得下标,以及相关样式更新
    getActiveIndex(e.x);
  }
});
// 创建nav导航点击事件 index:当前下标 isInit:是否为初始化
const clickNavItem = (index, isInit = false) => {
  // 根据坐标计算当前位置下标,同步拖拽容器得动画执行位置
  animate(boundedFlicker, {
    x: -index * boundedFlickWidth, //x轴偏移位置
    duration: isInit ? 0 : 500,
    ease: "out(4)"
  });
};
...
// 初始化下标位置
let activeIndex = 2;
clickNavItem(activeIndex, true);

// 获取当前位置下标
const getActiveIndex = (x) => {
  const index = utils.round(x / -boundedFlickWidth, 0);
  activeIndex = index;
  // updateActiveIndexClass({ index, x }); //此处是用于更新相关样式,不影响正常运行
};
// 通过utils工具获取nav元素,并添加点击事件监听
const navs = utils.$(".nav-item");
navs.forEach(($el, index) => {
  $el.addEventListener("click", () => {
    clickNavItem(index);
  });
});

创建底部前后切换定位

anime-draggable3.gif

    <div class="controls">
      <button class="control-button prve">Prev</button>
      <button class="control-button next">Next</button>
    </div>
// 创建底部导航控制方法
const slideTo = (targetIndex) => {
  // 控制切换时最大最小下标
  const clampedIndex = Math.max(0,Math.min(targetIndex, boundedFlickLength - 1));
  // 根据坐标计算当前位置下标,同步拖拽容器得动画执行位置
  animate(boundedFlicker, {
    x: -clampedIndex * boundedFlickWidth,
    duration: 500,
    ease: "out(4)"
  });
};
...
// 通过utils工具获取切换元素,并添加点击事件监听
const [$prev, $next] = utils.$(".control-button");
$prev.addEventListener("click", () => slideTo(activeIndex - 1));
$next.addEventListener("click", () => slideTo(activeIndex + 1));

大功告成🎉

了解以上拆分得步骤后,基本可以满足需求了。

动画提升

以下纯纯动画提神醒脑,想多了解的可以看看,实际功能是已经实现的了。

.seek()时间轴关联

这里值得一提的是anime的 .seek() 更新动画的 currentTime 并将其推进到特定时间

anime-draggable4.gif

image.png

应用到当前项目中,就是在拖拽时更新nav导航底部进度条.plan的样式,类似下面这样:

anime-draggable5.gif

<div class="plan"></div>
// 创建进度条动画
utils.set(".plan", { width: 0, transformOrigin: "left" });
const planAnimation = animate(".plan", {
  autoplay: false,
  width: "100%",
  ease: "linear" //如果想要等比动画,这里一定要设置匀速!!!!
});
...
// 根据当前下标移除并添加样式
const updateActiveIndexClass = ({ index, x }) => {
  ...
  // 根据当前拖拽距离同步进度条动画的播放时间,默认动画时间为1000ms
  planAnimation.seek(
    (x / (-boundedFlickWidth * boundedFlickLength)) * 1000 + 1000 / boundedFlickLength
  ); // 每个carousel-item偏移时所需要的平均时间 + 补偿时间(拖拽位置x为0时,实际的头部进度条应该到达1的位置,所以需要 1000/4=250ms 的补偿)
};

遇到的问题

  1. 这里你可能想到用scaleX属性,但是实际应用却不生效🕵️‍♀️(我没做过多深究所以就转用width控制宽度了);
  2. 为何要重新在js里控制下width:0,这个官方也有解释,大致意思就是anime不会解析css中的样式,如果想要变换尽量.set()下才能监听到

image.png

3.ease: "linear"如果想要同步关联进度条动画,这里切记要用匀速,否则拖拽时头部的.plan样式无法同步;

在线代码

以上为在线代码,需要在vue3中引用的,可以👉参考这里

希望有帮到你,愿将来你能成为一个anime动画小能手💪,敬us!