简介
FLIP 是一种用于实现结构变化动画的方案,核心流程首字母组成FLIP:
- First 计算初始位置
- Last 计算变换之后的位置[页面无变化]
- Invert 反转[页面无变化]
- Play 播放动画[页面变化]
使用场景
- 拖拽排序
- 洗牌
Flip使用的知识点
- 浏览器的渲染操作不在事件循环的队列中,而是在额外的渲染队列中;
- 渲染队列中会存在多条渲染相关代码,对于相同的样式属性,会将其计算后合并执行,不会逐个执行;
- 渲染队列会在主线程微小空闲期间批量执行【本次任务已结束,下个任务未开始|队列空】;
利用 渲染操作非逐行执行的特性
在 Last 步骤移动元素并计算移动的距离
在 Invert 步骤增加 transform 使元素还原到 First 步骤时位置
在 Play 步骤中设置 transition 过滤和删除 Invert 步骤中的 transform 属性
实现 First ---> Last --transform-> First --removeTransform-> Last
Flip 简单核心案例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
align-items: flex-start;
}
.list {
border-radius: 10px;
border: 1px solid #ccc;
padding: 15px 30px;
list-style: none;
margin-top: 10px;
margin-left: 30px;
}
.list-item {
background: #7ebf50;
border: 2px solid #4d782e;
color: #fff;
padding: 0 30px;
border-radius: 25px;
margin: 10px 0;
height: 50px;
line-height: 50px;
padding-right: 60px;
letter-spacing: 3px;
user-select: none;
position: relative;
}
button {
border: none;
outline: none;
background: #409eff;
padding: 12px 20px;
color: #fff;
border-radius: 7px;
font-size: 16px;
margin-top: 30px;
margin-left: 30px;
cursor: pointer;
}
button:hover {
background: #66b1ff;
}
button:active {
background: #3a8ee6;
}
</style>
</head>
<body>
<button>改变第一个元素的位置</button>
<ul class="list">
<li class="list-item" style="background: #e75723; border-color: #a12d02">
HTML + CSS
</li>
<li class="list-item">JavaScript</li>
<li class="list-item">网络</li>
<li class="list-item">工程化</li>
<li class="list-item">框架</li>
<li class="list-item">移动端</li>
<li class="list-item">NodeJS</li>
</ul>
<script>
const btn = document.querySelector("button");
const list = document.querySelector(".list");
const firstItem = document.querySelector(".list-item:first-child");
const lastItem = document.querySelector(".list-item:last-child");
function getLocation() {
const rect = firstItem.getBoundingClientRect();
return rect.top;
}
const start = getLocation();
console.log("First:", start);
btn.onclick = () => {
list.insertBefore(firstItem, null);
const end = getLocation();
console.log("Last:", end);
const dis = start - end;
firstItem.style.transform = `translateY(${dis}px)`;
console.log("Invert:", dis);
raf(() => {
firstItem.style.transition = "transform 1s";
firstItem.style.removeProperty("transform");
console.log("play");
});
};
function raf(callback) {
requestAnimationFrame(() => {
requestAnimationFrame(callback);
});
}
function delay(duration = 1000) {
const start = Date.now();
while (Date.now() - start < duration) {}
}
</script>
</body>
</html>
Flip 列表中的使用封装
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<link rel="stylesheet" href="./font/iconfont.css" />
<link rel="stylesheet" href="./css/index.css" />
</head>
<body>
<button>排序</button>
<ul class="list">
<li class="list-item">HTML + CSS</li>
<li class="list-item">JavaScript</li>
<li class="list-item">网络</li>
<li class="list-item">工程化</li>
<li class="list-item">框架</li>
<li class="list-item">移动端</li>
<li class="list-item">NodeJS</li>
</ul>
<script src="./js/flip.js"></script>
<script src="./js/index.js"></script>
</body>
</html>
//index.js
var listContainer = document.querySelector(".list");
var btn = document.querySelector("button");
btn.onclick = function () {
var f = new Flip(listContainer.children);
function shuffle(listContainer) {
var children = Array.from(listContainer.children); // 将子元素转换为数组
var len = children.length;
for (var i = len - 1; i > 0; i--) {
// 生成一个随机索引,范围从 0 到 i
var j = Math.floor(Math.random() * (i + 1));
// 交换 children[i] 和 children[j]
var temp = children[i];
children[i] = children[j];
children[j] = temp;
}
// 将乱序后的子元素重新插入到容器中
listContainer.innerHTML = ""; // 清空容器
children.forEach(function (child) {
listContainer.appendChild(child);
});
}
// 调用函数
shuffle(listContainer);
f.play();
};
上面,我们实现了一个一键洗牌的列表,下面给列表添加 shuffle 的动画
const Flip = (function () {
class FlipDom {
constructor(dom, duration = 0.5) {
this.dom = dom;
this.transition =
typeof duration === "number" ? `${duration}s` : duration;
this.firstPosition = {
x: null,
y: null,
};
}
getDomPosition() {
const rect = this.dom.getBoundingClientRect();
return {
x: rect.left,
y: rect.top,
};
}
recordFirst(firstPosition) {
if (!firstPosition) {
firstPosition = this.getDomPosition();
}
this.firstPosition.x = firstPosition.x;
this.firstPosition.y = firstPosition.y;
}
play() {
this.dom.style.transition = "none";
const lastPosition = this.getDomPosition();
const dis = {
x: lastPosition.x - this.firstPosition.x,
y: lastPosition.y - this.firstPosition.y,
};
if (!dis.x && !dis.y) {
console.log("No displacement, no animation needed.");
return;
}
this.dom.style.transform = `translate(${-dis.x}px, ${-dis.y}px)`;
raf(() => {
this.dom.style.transition = `transform ${this.transition}`;
this.dom.style.transform = `none`;
});
}
}
function raf(callback) {
requestAnimationFrame(() => {
requestAnimationFrame(callback);
});
}
class Flip {
constructor(doms, duration = 0.5) {
this.flipDoms = [...doms].map((it) => new FlipDom(it, duration));
this.duration = duration;
this.flipDoms.forEach((it) => it.recordFirst());
}
play() {
this.flipDoms.forEach((it) => it.play());
}
}
return Flip;
})();
对上面代码使用生成器函数进行优化
//flip.js
const Flip = (function () {
class FlipDom {
constructor(dom, duration = 0.5) {
this.dom = dom;
this.transition =
typeof duration === "number" ? `${duration}s` : duration;
this.firstPosition = {
x: null,
y: null,
};
this.isPlaying = false;
this.transitionEndHandler = () => {
this.isPlaying = false;
this.recordFirst();
};
}
getDomPosition() {
const rect = this.dom.getBoundingClientRect();
return {
x: rect.left,
y: rect.top,
};
}
recordFirst(firstPosition) {
if (!firstPosition) {
firstPosition = this.getDomPosition();
}
this.firstPosition.x = firstPosition.x;
this.firstPosition.y = firstPosition.y;
}
*play() {
if (!this.isPlaying) {
this.dom.style.transition = "none";
const lastPosition = this.getDomPosition();
const dis = {
x: lastPosition.x - this.firstPosition.x,
y: lastPosition.y - this.firstPosition.y,
};
if (!dis.x && !dis.y) {
return;
}
this.dom.style.transform = `translate(${-dis.x}px, ${-dis.y}px)`;
yield "moveToFirst";
this.isPlaying = true;
}
this.dom.style.transition = this.transition;
this.dom.style.transform = `none`;
this.dom.removeEventListener("transitionend", this.transitionEndHandler);
this.dom.addEventListener("transitionend", this.transitionEndHandler);
}
}
class Flip {
constructor(doms, duration = 0.5) {
this.flipDoms = [...doms].map((it) => new FlipDom(it, duration));
this.flipDoms = new Set(this.flipDoms);
this.duration = duration;
this.flipDoms.forEach((it) => it.recordFirst());
}
addDom(dom, firstPosition) {
const flipDom = new FlipDom(dom, this.duration);
this.flipDoms.add(flipDom);
flipDom.recordFirst(firstPosition);
}
play() {
let gs = [...this.flipDoms]
.map((it) => {
const generator = it.play();
return {
generator,
iteratorResult: generator.next(),
};
})
.filter((g) => !g.iteratorResult.done);
while (gs.length > 0) {
document.body.clientWidth;
gs = gs
.map((g) => {
g.iteratorResult = g.generator.next();
return g;
})
.filter((g) => !g.iteratorResult.done);
}
}
}
return Flip;
})();