FLIP 动画
FLIP 是 First, Last, Invert, Play 的缩写,是一种用于创建流畅动画的动画技术。Vue 中的 transition
组件就是基于 FLIP 实现的。
原理
FLIP 动画的原理是:
- First: 记录元素的初始状态(位置、大小等)
- Last: 记录元素的最终状态
- Invert: 计算初始状态和最终状态之间的差异,并应用反向变换
- Play: 移除反向变换,让元素自然过渡到最终状态
实现步骤
- First: 使用
getBoundingClientRect()
获取元素的初始位置和尺寸 - Last: 触发 DOM 变化后,再次获取元素的位置和尺寸
- Invert: 计算差异并应用 transform 属性
- Play: 使用 CSS transition 或 Web Animations API 实现动画
代码示例
function flipAnimation(element) {
const list = document.getElementById("list");
// 1. First 获取初始状态
const first = element.getBoundingClientRect();
// 触发 DOM 变化
element.classList.add("active");
// 移动元素到最终位置
// 这里可以写各种自定义的逻辑来改变元素的位置
list.appendChild(element);
// 2. last 获取最终状态
const last = element.getBoundingClientRect();
// 3. Invert 计算差异并应用反向变换
const deltaX = first.left - last.left;
const deltaY = first.top - last.top;
const deltaWidth = first.width / last.width;
const deltaHeight = first.height / last.height;
// 应用反向变换,将元素移动到初始位置和大小
element.style.transform = `translate(${deltaX}px, ${deltaY}px) scale(${deltaWidth}, ${deltaHeight})`;
/** 注意⚠️:代码执行到这里,用户是看不到样式变化的,因为浏览器还没有重绘,所以需要等待下一帧 */
requestAnimationFrame(() => {
// 4. Play: 移除反向变换,让元素自然过渡
element.style.transition = "transform 0.5s ease-out";
element.style.transform = "";
// 动画结束后清理样式
item1.addEventListener(
"transitionend",
() => {
item1.style.transition = "";
item1.classList.remove("moving");
},
{ once: true }
);
});
}
优点
- 性能优化:使用 transform 和 opacity 进行动画,避免重排
- 流畅性:通过反向变换实现更自然的动画效果
- 灵活性:可以应用于各种 DOM 变化场景
应用场景
- 列表重排序
- 元素位置变化
- 模态框打开/关闭
- 图片画廊切换
注意事项
- 确保使用
requestAnimationFrame
来优化性能 - 动画结束后记得清理样式
- 考虑使用
will-change
属性来提示浏览器优化 - 注意处理动画中断的情况
FLIP 动画示例
<p>点击按钮,"项目 1"会随机移动到其他位置,并带有动画效果。</p>
<button class="button">随机移动项目 1</button>
<div class="list-container">
<ul class="list" id="list">
<li class="list-item item-1">项目 1</li>
<li class="list-item">项目 2</li>
<li class="list-item">项目 3</li>
<li class="list-item">项目 4</li>
<li class="list-item">项目 5</li>
</ul>
</div>
function moveItemRandomly() {
const list = document.getElementById("list");
const item1 = document.querySelector(".item-1");
// 1. First: 记录初始状态
const first = item1.getBoundingClientRect();
// 添加移动类
item1.classList.add("moving");
// 获取所有项目并找到当前项目的位置
const items = Array.from(list.children);
const currentIndex = items.indexOf(item1);
// 生成一个不包含当前位置的可用位置数组
const availablePositions = items.map((_, index) => index).filter((index) => index !== currentIndex);
// 从可用位置中随机选择一个
const newIndex = availablePositions[Math.floor(Math.random() * availablePositions.length)];
// 先移除项目
item1.remove();
// 根据新位置插入项目
if (newIndex === items.length - 1) {
list.appendChild(item1);
} else {
const targetNode = list.children[newIndex];
list.insertBefore(item1, targetNode);
}
// 2. Last: 记录最终状态
const last = item1.getBoundingClientRect();
// 3. Invert: 计算差异并应用反向变换
let deltaX = first.left - last.left;
let deltaY = first.top - last.top;
// 应用反向变换,将元素移动到初始位置
item1.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
/** 注意⚠️:代码执行到这里,用户是看不到样式变化的,因为浏览器还没有重绘,所以需要等待下一帧 */
// 等待一帧以确保 DOM 更新
// 其实这里用一层 requestAnimationFrame 就够了,但是为了保险起见,用两层
// 第一层:确保 DOM 更新
// 第二层:确保样式更新
requestAnimationFrame(() => {
// 等待下一帧
requestAnimationFrame(() => {
// 4. Play: 移除反向变换,让元素自然过渡
item1.style.transition = "transform 0.5s cubic-bezier(0.4, 0, 0.2, 1)";
item1.style.transform = "";
// 动画结束后清理样式
item1.addEventListener(
"transitionend",
() => {
item1.style.transition = "";
item1.classList.remove("moving");
},
{ once: true }
);
});
});
}
document.querySelector(".button").addEventListener("click", moveItemRandomly);
.list-container {
display: flex;
gap: 20px;
margin-top: 20px;
}
.list {
list-style: none;
padding: 0;
margin: 0;
flex: 1;
}
.list-item {
padding: 15px;
margin-bottom: 10px;
background-color: #f0f0f0;
border-radius: 4px;
will-change: transform;
position: relative;
}
.item-1 {
background-color: #ffcccc;
}
.moving {
position: relative;
z-index: 1;
}
.button {
padding: 10px 20px;
background-color: #4caf50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
margin-bottom: 20px;
}
.button:hover {
background-color: #45a049;
}