FLIP动画实现思路
如果让你实现下面的这种动画效果你会怎么做?
可能很多人第一想法就是使用绝对定位进行布局,当顺序发生变化后,计算出变化后的位置,然后通过动画过渡到指定位置。这是一种很常见的实现方式,但存在几个问题:
- 需要维护每个节点的位置信息
- 顺序变化后,需要计算每个DOM的目标位置
- 使用绝对定位的方式,每行显示的小方块个数是固定的,不能自适应容器的变化。
过程分析
无论多么复杂的动画,都可以拆解成多个动画的组合。对于上面的效果,就可以看成是每个小方块的变化,这里只涉及到了位置的变化,当然还可能存在大小、颜色等变化。
从整个动画过程提取几个最重要的信息:
- 开始状态信息:黄绿色、宽50px、高50px、位置坐标0,0
- 结束状态信息:橙色、宽50px、高50px、位置坐标100,0
- 发生变化的属性:
- 颜色:黄绿色=》橙色
- 位置:left 0 => 100
要实现上面的过渡效果,我们通常可以使用 animation
、transition
和 requestAnimationFrame
来实现。下面就用代码来实现上面的效果。
animation
@keyframes identifier {
from {
left:100px;
background: yellowgreen;
}
to {
left:400px;
background: rgb(255, 123, 0);
}
}
.animation-dom {
position: absolute;
display: inline-block;
height: 50px;
width: 50px;
background: yellowgreen;
animation: identifier 3s infinite;
-webkit-animation:identifier 3s infinite;
}
transition
使用 transition
设置属性变化时具有过渡效果,为开始和结束状态创建两个不同的类名,分别设置其状态的属性。通过改变类名来实现过渡效果。
.transition-dom {
transition: all 1s;
&.start {
left: 0;
background: yellowgreen;
}
&.end {
left: 100px;
background: rgb(255, 123, 0);
}
}
requestAnimationFrame
多用于受控属性的控制,如位置、大小等信息。这种方式不太适用于一般的动画效果开发,需要自己去计算每个时刻的状态,并且对颜色这种过度无能为力。
FLIP
FLIP
分别是 First
、Last
、Invert
、 Play
四个单词的缩写;
First
元素的起始状态,例如位置、大小、形状、颜色等信息
Last
元素运动后的终止状态
Invert
元素的变化过程,也就是最终状态相对于其实状态,有哪些属性发生了改变。例如:位置向右移动了100px,颜色从黄色变为了橙色。将元素所有发生了变化的属性全部统计出来。
Play
执行动画过程,将所有发生变化的属性,从其实状态过渡到结束状态。可以设置过渡的时间、过渡方式等。可以通过上述的几种方式来实现这个过程。
实现思路
为了使得我们的动画灵活性更高,开发成本更低,首先就排除掉使用 绝对定位
的方式。如果不通过计算的方式,我们如何知道动画结束时元素的属性呢?
这里就要提出一个很重要的知识点:DOM元素的属性发生变化时,会被集中收集到浏览器的下一帧进行统一渲染。也就是说会存在这么一个时间段,DOM的元素属性已经发生改变,而浏览器还没来得及渲染,此时我们依然是可以拿到DOM更新后的属性的。
知道了元素的终止状态,就可以来实现过渡动画了。最好是使用 animation
的方式来实现,好处是不会在DOM元素上添加任何的 CSS。
具体实现
以 React
为例
// 以此列表进行循环渲染的数据源
const [dataList, setDataList] = useState<any[]>([0, 1, 2, 3, 4, 5]);
// 容器元素
const wrapperRef = useRef<HTMLDivElement>(null);
return (
<div className='flip-demo'>
<Space>
<Button>
新增
</Button>
<Button>
乱序
</Button>
</Space>
<div className='list' ref={wrapperRef}>
{dataList.map(item => (
<div
key={item}
className="item"
>
{item}
</div>
))}
</div>
</div>
);
.list {
display: flex;
flex-wrap: wrap;
width: 550px;
}
.item {
display: flex;
align-items: center;
justify-content: center;
width: 80px;
height: 80px;
border: 1px solid #eee;
}
第一步:记录元素的起始状态
将每个元素的起始状态进行记录保存,这里建议使用 Map 来进行存储,有两个好处:
- 方便取值,不像数组那样必须要保证顺序。
- 可以使用 DOM 节点作为 KEY 值,即使 DOM 的属性发生了变化,DOM 的引用是不会变的。
// 用于存储最后一次状态
const lastRectRef = useRef<Map<HTMLElement, DOMRect>>(new Map());
// 将容器下面所有的子元素存储到MAP中
function createChildrenElementMap(wrapperNode: HTMLElement | null) {
if (!wrapperNode) {
return new Map();
}
// 获取到所有的子元素
const childNodes = Array.from(wrapperNode.childNodes) as HTMLElement[];
// 原元素作为KEY,其属性值作为VALUE
const result = new Map(childNodes.map(node => [node, node.getBoundingClientRect()]));
return result;
}
// 只要 dataList 发生了变化,就需要更新状态
useEffect(() => {
const currentRectMap = createChildrenElementMap(wrapperRef.current);
lastRectRef.current = currentRectMap;
}, [dataList]);
知识点: 一个n * 2 的二位数组,将其转为 Map 时,数组的 arr[n][0]
会作为Map 的key , arr[n][1]
为相应的值。
第二步:实现新增和乱序功能
新增和乱序比较简单,就是改变数据源而已。
import { shuffle } from 'lodash';
// 乱序,使用lodash的shuffle方法
function handleShuffle() {
setDataList(shuffle);
}
// 添加
function addItem() {
setDataList((list) => [list.length, ...list]);
}
第三步:获取最终状态,计算变化属性值,执行动画
本案例中我们明确知道只有元素的位置信息发生了变化。
useLayoutEffect(() => {
// 这里获取到的是最新的状态信息,也就是最终状态
const currentRectMap = createChildrenElementMap(wrapperRef.current);
// 对保存的上次状态的元素进行遍历
lastRectRef.current.forEach((prevNode, node) => {
// 由于DOM的属性变化后其引用是不会发生改变的,因此可以在currentRectMap中获取到其最终状态
const currentRect = currentRectMap.get(node);
// 计算位置信息的变化
const invert = {
left: prevNode.left - currentRect?.left,
top: prevNode.top - currentRect?.top,
};
// 设置动画过程
const keyframes = [
{
transform: `translate(${invert.left}px, ${invert.top}px)`
},
{
transform: `translate(0, 0)`
}
];
// 执行动画
node.animate(keyframes, {
duration: 800,
easing: 'cubic-bezier(0.25, 0.8, 0.25, 1)',
})
});
lastRectRef.current = currentRectMap;
}, [dataList]);
动画调试
我们在开发动画时,为了追求更好的过渡效果,就需要更加深入的分析动画的整个过程。但往往动画的过程都是比较快的,肉眼很难看到细节。
既然太快了,我们就把动画的时间调慢些,慢到我们可以看到为止。这种方式的确很直接很实用,但是需要手动的去修改代码,调试完成后还需要手动改回来。
浏览器的开发者工具支持动画的调试,具体位置如下图:
点击 10%
可以将动画的过程放慢10倍,并且可以在下方看到所有发生了动画的元素。
点击快照列表可以重复执行动画,在界面中也会重新执行。
你甚至可以拖动下面的时间线,来单独控制某个元素的执行时间: