前言
对我们前端来说,用户体验是我们在开发面向客户的应用时必须做好的一点,怎样实现良好的交互过渡特效也是我们需要掌握的一个技术。笔者用过vue
和react
,这两个框架都为我们开发应用时处理UI的过渡动画提供了组件,我们不需要关心底层的实现,只需要简单配置组件的api
就能解决绝大部分的过渡场景。但是我最近在开发一个组件时,需要一些特别的过渡动画,这些组件功能就有点局限,所以我学习实践了一下上述组件使用到的过渡技术,FLIP
。
为啥要用FLIP
举个例子,假如现在页面上存在4个标签[tag1, tag2, tag3, tag4]
,我现在需要把新增一个标签tag5
,会变成[tag5, tag1, tag2, tag3, tag4]
,我想要让一开始存在的4个标签可以有整体向右移动的过渡动画,你有什么思路?
如果是没有学过FLIP
之前的我,我会考虑给这4个标签都加一个过渡的css
,然后改变transform
让这些标签向右移动一段距离,或者使用js
去移动这些标签向右移动。
这种方式存在几个比较麻烦的问题,向右移动一段距离,如果每个元素的宽高都一致,计算这个距离还好,但是如果每个元素的宽高都不一致,那这个距离怎么计算?如果一行排满了,还需要换行,高度怎么计算?还有一个问题,我们现在用的前端框架都是使用数据来控制视图,什么时候去把这个新的tag1
加入到数据中?
先加数据,再控制标签移动,页面会重新渲染,标签的位置会变化,这时候再控制标签向右移动其实位置已经错误了。
先移动,再加数据的话,如果是css控制的移动,按过渡时间来,经常会因为js
的定时不精准出现跳帧的现象。如果是js控制的移动,一般需要等待所有元素的移动回调完成再去加数据。
FLIP实现原理
FLIP
其实是四个单词的缩写,First
,Last
,Invert
,Play
。我不太喜欢在掘文里写具体的概念,大家来看博客都是为了学技术,具体概念啥的有兴趣的就自行去了解一下吧,我在这里主要介绍一下FLIP
的实现的思路。
FLIP
其实是一个反向的实现过渡思路,一般的过渡思路是我需要把一个元素从A点移动到B点,我就需要一点点修改这个元素的transform
,让它到达B点。而FLIP
是直接让这个元素到达B点的位置,然后计算A点和B点的坐标的距离,设置transform
,让它从A点移动到translate(0px, 0px)
,也就是自身的位置。
FLIP
的大体流程是
- 1.记录原来的dom节点的坐标
- 2.数据更改,修改dom页面(新增,删除,重新排序)
- 3.记录新的dom点的坐标
- 4.根据新和老的坐标,得出两个坐标之间的距离
- 5.给已经重新渲染后的dom添加初始样式,让它有类似过渡的动画
预览
我仿照vue
官网的过渡组件中的例子,写了一个示例的页面。因为FLIP
只是一种实现动画的思想,跟框架无关,所以我在demo页面中是使用的原生js
。
实现细节
这里介绍我写的这个示例页面的实现过程以及细节。首先是布局,布局很简单,就是一个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);
};
这里有一个比较关键的点,如果在vue
和react
中使用需要注意,就是页面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
来更方便跟好的帮助你解决问题。
感谢
如果本文对你有所帮助,请帮忙点个赞,感谢大家!