动画 - 让页面更加灵动
先看效果
案例1
案例2
案例3
怎么样? 是不是很丝滑, 还不错? 爱了爱了 ❤
实际是利用了 flip
技术
flip
flip 是一个
First
,Last
,Invert
,Paly
的缩写
-
First
元素的起始位置或者大小,可以使用getBoundingClientRect()
这个API
来处理 -
Last
从字面理解,就是元素的结束状态啦,也就是动画让元素停止的状态了 -
🚩Invert
计算元素第一个位置(First
)和最后一个位置(Last
)之间的位置变化, 让元素进行移动(通过transform
来改变元素的位置),从而创建它位于 初始位置 的一个错觉, 然后再添加transition
效果,自然而然的,过渡效果就来了 -
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
的效果其他动画方式也可以实现,但是有一点是特别的,是他可以很方便的实现动画,而不需要自己手动计算位置,大大简化了操作
一直以为样式,用户体验不重要,只要业务逻辑可以实现就可以了,其实不然。
动画写的好的,用户跑不了,自己看着也挺有意思,真的有趣 😎