浅学 View Transitions API
只需要通过 View Transitions API 切换不同的类名,即可实现流畅的切换动画
命名 view-transition-name
在 View Transitions 动画执行的过程中,默认会在页面根节点下自动创建一组伪元素:
::view-transition // 视图过渡根元素,包含所有视图过渡组,且位于其他页面内容的顶部
├─ ::view-transition-group(root) // 默认视图过渡组 (root)
└─ ::view-transition-image-pair(root) // 承载一个过渡中旧视图状态和新视图状态的容器
├─ ::view-transition-old(root) // 旧视图状态
└─ ::view-transition-new(root) // 新视图状态
通过调用 API,让浏览器为新旧两种不同视图分别捕获并建立了快照 (即 ::view-transition-old 旧快照 和 ::view-transition-new 新快照),而后新旧两快照在 ::view-transition-image-pair 容器中完成转场动画的过渡。动画结束后则删除其相关伪元素 (快照和容器)。
动画执行的基本过程如下图所示:
- 开发者通过 document.startViewTransition(callback) 启动转场动画,其中 callback
函数是用来更新 DOM 状态 (即更新为新视图状态) - 捕获当前状态为旧视图状态
- 暂停 DOM 树渲染
- 回调函数 callback 被调用,用来更新文档状态 (可以是异步函数,返回 Promise)
- 回调函数 callback 成功后,transition.updateCallbackDone 被执行 (即 promise is
resolved) - 恢复 DOM 树渲染,而后捕获当前状态为新视图状态
- 创建过渡伪元素 (即 ::view-transition-old、::view-transition-new ...等)
- 渲染未暂停,显示过渡伪元素
- transition.ready 被执行 (即 promise is resolved)
- 伪元素开始动画,直至动画完成
- 删除了过渡伪元素
- transition.finished 被执行 (即 promise is resolved)
若需要使某个元素执行过渡动画,需要给每个元素添加一个自定义属性:view-transition-name,且每个元素的 view-transition-name 必须唯一,即同一个页面上渲染的元素(display 非 none) view-transition-name 不同重复。
<!DOCTYPE html>
<html lang=zh-CN>
<head>
<meta charset=utf-8>
<meta http-equiv=X-UA-Compatible content="IE=edge">
<title></title>
<script>
function readly(){
const btn = document.querySelector('#btn')
btn.addEventListener('click', function(ev) {
document.documentElement.style.setProperty('--x', ev.clientX + 'px');
document.documentElement.style.setProperty('--y', ev.clientY + 'px');
if (document.startViewTransition) {
const vt = document.startViewTransition(() => {
document.documentElement.classList.toggle('dark');
});
console.log('View Transition Instance', vt);
vt.ready.then(() => {
console.log('Transition Ready');
});
vt.updateCallbackDone.then(() => {
console.log('Callback Done');
});
vt.finished.then(() => {
console.log('Transition Finished');
});
} else {
document.documentElement.classList.toggle('dark');
}
});
}
</script>
<style>
html,
body {
margin: 0;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
html {
background-color: #fff;
}
button {
padding: 5px 16px;
margin-top: 20px;
margin-left:auto;
background-color: transparent;
border-radius: 8px;
line-height: 1.4;
box-shadow: 0 2px #00000004;
cursor: pointer;
transition: .3s;
transform: scale(1);
border-color: transparent;
background-color: royalblue;
color: #fff;
text-shadow: 0 -1px 0 rgb(0 0 0 / 12%);
box-shadow: 0 2px #0000000b;
user-select: none;
}
button:hover {
filter: brightness(1.1);
}
.dark {
background-color: #111;
}
.list {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
margin-top: 20px;
}
.item {
width: 200px;
height: 150px;
background-color: royalblue;
border-radius: 10px;
}
::view-transition-old(*) {
animation: none;
}
::view-transition-new(*) {
animation: clip .5s ease-in-out;
}
@keyframes clip {
from {
clip-path: circle(0% at var(--x) var(--y));
}
to {
clip-path: circle(100% at var(--x) var(--y));
}
}
</style>
</head>
<body onload="readly()">
<button class="button" id="btn" href="javascript:;">切换</button>
<div class="list" id="list">
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
</div>
</body>
</html>