教你怎么更优雅的去实现交互过渡动效

前言

对咱们前端来讲,用户体验是咱们在开发面向客户的应用时必须作好的一点,怎样实现良好的交互过渡特效也是咱们须要掌握的一个技术。笔者用过vuereact,这两个框架都为咱们开发应用时处理UI的过渡动画提供了组件,咱们不须要关心底层的实现,只须要简单配置组件的api就能解决绝大部分的过渡场景。可是我最近在开发一个组件时,须要一些特别的过渡动画,这些组件功能就有点局限,因此我学习实践了一下上述组件使用到的过渡技术,FLIPcss

为啥要用FLIP

举个例子,假如如今页面上存在4个标签[tag1, tag2, tag3, tag4],我如今须要把新增一个标签tag5,会变成[tag5, tag1, tag2, tag3, tag4],我想要让一开始存在的4个标签能够有总体向右移动的过渡动画,你有什么思路?前端

若是是没有学过FLIP以前的我,我会考虑给这4个标签都加一个过渡的css,而后改变transform让这些标签向右移动一段距离,或者使用js去移动这些标签向右移动。vue

这种方式存在几个比较麻烦的问题,向右移动一段距离,若是每一个元素的宽高都一致,计算这个距离还好,可是若是每一个元素的宽高都不一致,那这个距离怎么计算?若是一行排满了,还须要换行,高度怎么计算?还有一个问题,咱们如今用的前端框架都是使用数据来控制视图,何时去把这个新的tag1加入到数据中?react

先加数据,再控制标签移动,页面会从新渲染,标签的位置会变化,这时候再控制标签向右移动其实位置已经错误了。web

先移动,再加数据的话,若是是css控制的移动,按过渡时间来,常常会由于js的定时不精准出现跳帧的现象。若是是js控制的移动,通常须要等待全部元素的移动回调完成再去加数据。api

FLIP实现原理

FLIP实际上是四个单词的缩写,FirstLastInvertPlay。我不太喜欢在掘文里写具体的概念,你们来看博客都是为了学技术,具体概念啥的有兴趣的就自行去了解一下吧,我在这里主要介绍一下FLIP的实现的思路。前端框架

FLIP实际上是一个反向的实现过渡思路,通常的过渡思路是我须要把一个元素从A点移动到B点,我就须要一点点修改这个元素的transform,让它到达B点。而FLIP是直接让这个元素到达B点的位置,而后计算A点和B点的坐标的距离,设置transform,让它从A点移动到translate(0px, 0px),也就是自身的位置。markdown

FLIP的大致流程是app

  • 1.记录原来的dom节点的坐标
  • 2.数据更改,修改dom页面(新增,删除,从新排序)
  • 3.记录新的dom点的坐标
  • 4.根据新和老的坐标,得出两个坐标之间的距离
  • 5.给已经从新渲染后的dom添加初始样式,让它有相似过渡的动画

预览

我仿照vue官网的过渡组件中的例子,写了一个示例的页面。由于FLIP只是一种实现动画的思想,跟框架无关,因此我在demo页面中是使用的原生js框架

123.gif

DEMO页面

实现细节

这里介绍我写的这个示例页面的实现过程以及细节。首先是布局,布局很简单,就是一个flex容器。

.item_box {
    display: flex;
    flex-wrap: wrap;
    ...
}
.item {
    ...
}

// 数据列表,每次修改页面元素,都会同步修改这个数据
const itemList = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1];
// 初始化,渲染item列表
const itemInit = () => {
  const fragment = document.createDocumentFragment();
  for (let i = 0; i < itemList.length; i ++) {
    const dom = document.createElement('div');
    // 这里的后一个item是为了后续找到相应的dom元素
    dom.className = `item item${itemList[i]}`;
    dom.innerHTML = itemList[i];
    fragment.appendChild(dom);
  }
  document.querySelector('.item_box').appendChild(fragment);
};

itemInit();
复制代码

咱们拿新增元素来举例,先实现FLIP的第一步,记录原来的dom节点的坐标

const getLeftOrTops = () => {
  const rectList = [];
  // 遍历数据列表
  for (let i = 0; i < itemList.length; i ++) {
    // 计算这些节点的left和top数据
    const { left, top } = document.querySelector(`.item${itemList[i]}`).getBoundingClientRect()
    rectList.push({ left, top });
  }
  return rectList;
};
复制代码

而后进行第二步,数据更改,修改dom页面,以及第三步,记录新的dom点的坐标。

let count = 10;
// 新增dom节点的方法
const itemAdd = () => {
  // 这是记录原来的dom节点坐标的方法 
  const oldRects = getLeftOrTops();
  const curIndex = count++;
  // 给数据列表新推入一个元素
  itemList.unshift(curIndex);
  // 在页面中插入这个新节点,修改dom页面
  $box.insertBefore(createItem(curIndex), $box.childNodes[0]);
  // 第三步,记录新的dom点的坐标
  // 获取新的数据列表中dom节点的坐标
  // 新加入的dom节点不须要添加过渡条件,其余新旧dom节点须要计算新旧坐标的差
  const newRects = getLeftOrTops().slice(1);
};
复制代码

这里有一个比较关键的点,若是在vuereact中使用须要注意,就是页面dom修改和获取新的dom节点的坐标的顺序。若是在vue中,修改dom页面其实就是修改数据,vue会把这个修改页面dom的操做存入nextTick,等到当前宏任务运行完,再去微任务中运行。因此,获取新的dom节点的坐标的方法,也必须写在nextTick中,保证会在dom元素修改完成以后再去获取(获取时页面其实还未从新渲染,可是dom节点已经被修改)。在react中推荐使用useLayoutEffect在数据变动,dom修改以后去获取新的dom节点的坐标。

而后咱们继续FLIP的流程

const itemAdd = () => {
   // 上面的内容省略
   ...
    for (let i = 0; i < oldRects.length; i ++) {
        // 根据新和老的坐标,得出两个坐标之间的距离
        const left = oldRects[i].left - newRects[i].left;
        const top = oldRects[i].top - newRects[i].top;
        const move = [
          { transform: `translate(${left}px, ${top}px)` },
          { transform: "translate(0)" },
        ];
        const dom = document.querySelector(`.item${oldRects[i].key}`);
        // 这里使用web api的animate方法,让元素移动
        // animate的兼容可使用polyfill,或者使用其它的js动画库
        // 给已经从新渲染后的dom添加初始样式,让它有相似过渡的动画
        dom && dom.animate(move, {
          duration: 300,
          easing: "cubic-bezier(0,0,0.4,1)",
        });
      }
};
复制代码

就这样,咱们就是完成了FLIP的整个流程,你的新增元素已经有了好看的过渡动画。

新增,删除,从新排列的完整代码都在上面的示例页面中,你们有兴趣能够细看。

总结

FLIP只是一种实现过渡动画的思路,不但在上文这种文档流场景下可使用,它在绝对定位的布局中依然能够用。并且它是一种很是灵活的方法,并非只局限于本文中的内容,在实现一些交互动效的时候,能够多思考看看可否使用FLIP来更方便跟好的帮助你解决问题。

感谢

若是本文对你有所帮助,请帮忙点个赞,感谢你们!

相关文章
相关标签/搜索