flip - 更好玩的动画 ❤

3,873 阅读3分钟

动画 - 让页面更加灵动

先看效果

案例1

动画.gif

案例2

动画.gif

案例3

动画.gif

怎么样? 是不是很丝滑, 还不错? 爱了爱了 ❤
实际是利用了 flip 技术

flip

flip 是一个First,Last,Invert,Paly 的缩写

  1. First
    元素的起始位置或者大小,可以使用 getBoundingClientRect()这个 API来处理

  2. Last
    从字面理解,就是元素的结束状态啦,也就是动画让元素停止的状态了

  3. 🚩Invert
    计算元素第一个位置(First)和最后一个位置(Last)之间的位置变化, 让元素进行移动(通过 transform来改变元素的位置),从而创建它位于 初始位置 的一个错觉, 然后再添加 transition效果,自然而然的,过渡效果就来了

  4. Play 万事俱备,只要让动画动起来就好了.


实现图片增删动画

⚠ tip: 使用的 vue3,理解思路即可 注意

1. First记录初始位置

const prevImgs = Array.from(imgRefs.value);
const prevSrcRectMap = createSrcRectMap(prevImgs);

createSrcRectMap 用来记录 元素的初始位置
传入DomList,返回带位置信息的数据结构

    function createSrcRectMap(imgs: HTMLImageElement[]) {
     return imgs.reduce((prev, img) => {
       const rect = img.getBoundingClientRect();
       const { left, top } = rect;
       prev[img.src] = { left, top, img };
       return prev;
     }, {});
   }

最后的数据结构是这样的

    [{
        图片的url:{
            left:图片的.left,
            top:图片的.top,
            img:图片自身
        }
    }]

其中imgs 是图中的一个一个的图片,使用getBoundingClientRect 记录他们的位置

2. Last 找到最后的位置

然后 用户点击 新增图片 / 删除等 其他对DOM 结构有所改动的操作时,需要再次记录

使用的是Vue,获取最新的 Dom是在 nextTick 之后

⚠此时浏览器并没有开始执行渲染,如果开始渲染就不会有动画了(浏览器是16.6ms刷新一次页面)

 const newData = 新增的图片;
 imgs.value = newData.concat(imgs.value);
 // Dom 发生变化后,记录最新的位置信息
 // Dom 更新
 nextTick()
 const currentSrcRectMap = createSrcRectMap(prevImgs);

3. Invert 找到他们的差值

知道了开始的位置,知道了结束位置,可以很简单的找到差值

假装元素位置还没有发生变化

/* prevSrcRectMap 没有变化之前的
 currentSrcRectMap 变化之后的
  [{
        图片的url:{
           left:图片的.left,
            top:图片的.top,
            img:图片自身
        }
    }]
  */  
Object.keys(prevSrcRectMap).forEach((src) => {
   const currentRect = currentSrcRectMap[src];
   const prevRect = prevSrcRectMap[src];
   const invert = {
     left: prevRect.left - currentRect.left,
     top: prevRect.top - currentRect.top,
   };
 }

由于src 是唯一的, 可以遍历以前的老元素,找到每个元素的差值,开始动起来就好了

4. Paly 让动画动起来

调用元素的 animate方法让元素运动起来哦

    const keyframes = [
     {
       transform: `translate(${invert.left}px, ${invert.top}px)`,
     },
     { transform: "translate(0, 0)" },
   ];
   const options = {
     duration: 300,
     easing: "cubic-bezier(0,0,0.32,1)",
   };
   currentRect.img.animate(keyframes, options);

🚀 核心代码

只需理解 add 方法,其他删除/乱序是一样的

核心就一个方法scheduleAnimation

// 添加 add
const add = async () => {
// 新增的图片
const newData = initialGetSister();
 scheduleAnimation(() => {
    imgs.value = newData.concat(imgs.value);
 });
}

 async function scheduleAnimation(update: Function) {
     // 记录原始结构
     const prevImgs = Array.from(imgRefs.value);
     const prevSrcRectMap = createSrcRectMap(prevImgs);
     
     // 执行传递的函数
     update();
     
     // dom 已经改变但是视图还未发生变化
     await nextTick(); 

   // 记录Dom 变化后的 结构
     const currentSrcRectMap = createSrcRectMap(prevImgs);
     
     // 找到 差异
     Object.keys(prevSrcRectMap).forEach((src) => {
       const currentRect = currentSrcRectMap[src];
       const prevRect = prevSrcRectMap[src];
       const invert = {
         left: prevRect.left - currentRect.left,
         top: prevRect.top - currentRect.top,
       };

       const keyframes = [
         {
           transform: `translate(${invert.left}px, ${invert.top}px)`,
         },
         { transform: "translate(0, 0)" },
       ];
       const options = {
         duration: 300,
         easing: "cubic-bezier(0,0,0.32,1)",
       };
       currentRect.img.animate(keyframes, options);
     })
 }   

// 记录元素的位置信息
function createSrcRectMap(imgs: HTMLImageElement[]) {
 return imgs.reduce((prev, img) => {
   const rect = img.getBoundingClientRect();
   const { left, top } = rect;
   prev[img.src] = { left, top, img };
   return prev;
 }, {});
}

总结

其实filp的效果其他动画方式也可以实现,但是有一点是特别的,是他可以很方便的实现动画,而不需要自己手动计算位置,大大简化了操作

一直以为样式,用户体验不重要,只要业务逻辑可以实现就可以了,其实不然。
动画写的好的,用户跑不了,自己看着也挺有意思,真的有趣 😎

源码地址