平滑的移动应用内导航动画已经存在一段时间了。然而,在网页上实现相同的效果一直是一个挑战,通常需要复杂和独特的方法,直到 View Transitions API,这已经是一个简单而直接的基于浏览器的解决方案。
在本文中,我们将解释这个 API 是什么以及它是如何工作的。我们将学习如何在状态和页面之间创建平滑而简单的过渡效果。我们还将探讨 View Transitions API 如何与 React 搭配起来。
什么是 View Transitions API
View Transitions API 提供了一种在两个文档(视图)之间创建动画过渡的方式,而无需在过渡中创建重叠。
View Transitions 通过允许您在状态之间创建过渡动画,使用快照视图,在不重叠的情况下使 DOM 发生变化,使这一过程变得简单而直观。
该 API 的当前实现针对单页面应用程序(SPA)。在本教程中,我们将解释如何在 ReactJS 中实现这一目标。
但在跨页面应用 MPA 的路由切换中,犹豫新状态承载于新页面,也就是新的 document 对象,所以过渡效果无法跨过页面之间的阻隔。
这个玩意很新,兼容性很差,浏览器版本都要求是很高的,注意使用。
view transition 初体验
在我们了解 view transition 原理之前,让我们尝试一些简单的操作,看看它实际上非常简单。
想象一下,当我们单击按钮时,我们希望在页面上显示图像。通常情况下,图像只会弹出,没有任何花哨的效果。现在,让我们看看如何使用这个 API 来使图像平滑过渡。
首先我们先来编写如下代码,如果你想查看完整的源代码,你可以通过 codesandbox 进行查看:
最终代码实现效果如下图所示:
使用 View Transitions API 非常简单。然而,在我们创建更多示例之前,让我们花点时间来了解其工作原理的基本机制。
view transitions 工作原理
View Transitions API 可能听起来有点神秘,但一旦你了解它的工作原理并自己创建了一些 demo,你就能很好地掌握它。现在,让我们窥探幕后,看看发生了什么。
当你触发 View Transitions,比如调用 document.startViewTransition()
时,浏览器会对当前页面状态进行快照。可以将其想象成是对当前屏幕上显示的内容的快速拍照。
然后,魔法开始了。你在 document.startViewTransition() 中提供的回调函数会被调用。在这里,你可以对网页上的内容进行更改。浏览器在此回调期间巧妙地暂停了渲染,以防止任何闪烁,并且这个过程非常快速。
一旦你的回调函数完成了它的任务,浏览器会再次进行快照,但这次是新的页面状态,也就是你刚刚修改过的状态。浏览器使用这些快照来创建一个特殊的结构,它就像是叠加在页面之上的一种覆盖层。这个结构包括旧的快照和新的快照,叠加在一起。
这个结构具有不同的层次,就像伪元素的树状结构一样。每一层都有其特定的用途,但现在我们不会深入讨论细节。重要的是,这个覆盖层位于页面上的其他所有内容之上。
ruby
::view-transition
└─ ::view-transition-group(root)
└─ ::view-transition-image-pair(root)
├─ ::view-transition-old(root)
└─ ::view-transition-new(root)
旧的快照(::view-transition-old)和新状态的实时表示(::view-transition-new)开始它们自己特殊的动画。旧图像淡出(就像将不透明度从 1 减小到 0),而新图像淡入(将不透明度从 0 增加到 1)。这就创建了那个熟悉的交叉淡化效果。
一旦这个动画完成,覆盖层就会被移除,显示出最终的页面状态。这个过程巧妙地设计,以确保旧内容和新内容不会同时存在,这有助于避免可访问性、可用性和布局方面的问题。
如下图所示,如果你想查看完整的源代码,你可以通过 codesandbox 进行查看:
现在到了真正简洁的部分,动画由 CSS 控制,你可以改变它的外观。
例如,您可以使交叉淡入淡出持续时间更长,如下所示:
css
::view-transition-group(root) {
animation-duration: 2s;
}
这就是 View Transitions API 如何发挥其魔力的方式!它捕捉快照,应用您的更改,通过交叉淡化进行动画处理,然后平稳地呈现最终结果。
如果您对这些伪元素的各自功能感兴趣,以下是每个伪元素的简要解释:
::view-transition-group
: 在两个状态之间动画调整大小和位置。::view-transition-image-pair
: 提供混合隔离,以便两个图像可以正确地交叉淡化。::view-transition-old
和::view-transition-new
: 要进行交叉淡化的视觉状态。
单页面应用 SPA
在单页应用程序中使用视图过渡与我们之前示例中的简单演示并没有太大的不同。在单页应用程序中,您使用 JavaScript 来动态更改 DOM 以实现新状态,可以通过添加或删除元素、更改类名、更改样式等方式来实现,但这一切都发生在单个函数中,因此您可以再次将 startViewTransition 包装在该函数周围。
如下代码所示:
js
function spaNavigate(data) {
// Fallback for browsers that don't support this API:
if (!document.startViewTransition) {
updateTheDOMSomehow(data);
return;
}
// With a transition:
document.startViewTransition(() => updateTheDOMSomehow(data));
}
现在,浏览器会查看初始页面,然后查看这个新页面并在两者之间进行动画处理。之后,它会将我们移至新页面。
到目前为止,您看到的大多数演示都是在同一个文档内进行的过渡,这是最初在浏览器中提出并实现的概念。然而,这个 API 的扩展称为"跨文档视图过渡",允许您在不同文档之间导航时添加过渡效果。换句话说,这意味着您也可以为多页应用程序添加过渡效果。
为了看到这个 API 的实际效果,让我们创建一个演示,我们将在本文中使用。我已经组建了一个网站,由多个网页组成,而且没有依赖于任何框架。这些只是纯 HTML 和 CSS。这里是网站的预览,您可以在 GitHub 上查看代码。
使用 CSS 动画自定义 view transitions
你可能并不总是想要默认的交叉淡化动画。幸运的是,View Transitions API 赋予了你权力,可以使用 CSS 中的伪元素来按照你的意愿塑造动画效果。
你将会使用::view-transition-old()来表示正在离开的状态,而::view-transition-new()表示正在进入的状态。
让我们对我们的演示进行一次改造,将动画效果从交叉淡化变为滑动效果:
css
@keyframes fade-in {
from {
opacity: 0;
}
}
@keyframes fade-out {
to {
opacity: 0;
}
}
@keyframes slide-from-bottom {
from {
transform: translateY(50px);
}
}
@keyframes slide-to-top {
to {
transform: translateY(-50px);
}
}
::view-transition-old(root) {
animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out, 300ms cubic-bezier(
0.4,
0,
0.2,
1
) both slide-to-top;
}
::view-transition-new(root) {
animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in, 300ms
cubic-bezier(0.4, 0, 0.2, 1) both slide-from-bottom;
}
这是我们的演示现在的样子:
现在,如果我们只想将这种魔力应用于页面的单个部分,而不是整个文档,该怎么办?
命名 view transitions
想象一下,你希望一个页面上的一个元素平滑地变形成另一个页面或状态上的另一个元素。要实现这个效果,你可以使用一个称为"named view transition"的东西。
View Transitions API 引入了一个名为 view-transition-name 属性的新工具。这个 CSS 属性允许你为这两个元素分配相同的特殊名称。这个名称是帮助你创建命名视图过渡的关键。
这对 SPA 和 MPA 应用程序都适用。让我们通过一个示例来详细说明。
在这个示例中,我们有一个小图像在一个页面上,我们希望它变形成另一个页面上的一个更大的图像。
css
/* 第一个页面 */
.image {
view-transition-name: my-image;
}
/* 第二个页面 */
.bigger-image {
view-transition-name: my-image;
}
这两个元素可以是同一个具有不同样式的元素,甚至可以是完全不同的 HTML 元素。重要的是它们共享相同的 view-transition-name 值,这会触发 view-transition 效果。
请记住,你可以选择任何你喜欢的名称,只是不能选择 none
。而且要确保每个页面上的名称是唯一的。如果同一页面上的两个元素在同一时间共享相同的 view-transition-name 值,过渡效果将不会发生。
还记得我们讨论过的伪元素树吗?当你使用 view-transition-name 添加另一个视图过渡时,你就创建了一个新的分支。现在让我们看看这棵树是什么样子的:
ruby
::view-transition
├─ ::view-transition-group(root)
│ └─ ::view-transition-image-pair(root)
│ ├─ ::view-transition-old(root)
│ └─ ::view-transition-new(root)
└─ ::view-transition-group(my-image)
└─ ::view-transition-image-pair(my-image)
├─ ::view-transition-old(my-image)
└─ ::view-transition-new(my-image)
如果你想查看完整的源代码,你可以通过 codesandbox 进行查看:
这意味着我们可以使用新的伪元素来自定义新的视图过渡效果。
现在,让我们在我们的演示中将视图过渡应用于不同的元素。到目前为止,我们为整个页面使用了单一的交叉淡化动画。
现在来我们开始上点有难度的动画,如果你想查看完整的源代码,你可以通过 codesandbox 进行查看:
这些动画之所以能有这样的效果,主要是有以下这些关键帧起效:
css
@keyframes fade-and-scale-in {
from {
opacity: 0;
transform: scale(0);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes fade-and-scale-out {
from {
opacity: 1;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(0);
}
}
在 React 中使用 View Transition API
首先我们创建一个初始化项目,执行如下命令:
bash
npx create-neat app
因为 React 会异步渲染状态变化,所以我们需要使用 flushSync 将状态设置函数包装起来,以强制将状态变化同步应用。
紧接着在 App.jsx 页面中添加如下代码:
jsx
import React from "react";
import { flushSync } from "react-dom";
import "./index.css";
const App = () => {
const [isThumbnail, setIsThumbnail] = React.useState(true);
const handleMove = () => {
document.startViewTransition(() => {
flushSync(() => {
setIsThumbnail((prev) => !prev);
});
});
};
return (
<div>
<div className="top-bar">
<div className="top-bar-content">
<h1>Move Cat</h1>
<button onClick={handleMove}>Move</button>
</div>
{isThumbnail && (
<img
src="https://res.cloudinary.com/djzsjzasg/image/upload/c_scale,w_300/v1678947391/malcolm-kee/meow_dtsn8h.png"
alt="cat"
className="cat-img thumbnail"
/>
)}
</div>
{!isThumbnail && (
<div className="cat-details">
<img
src="https://res.cloudinary.com/djzsjzasg/image/upload/c_scale,w_500/v1678947391/malcolm-kee/meow_dtsn8h.png"
alt="cat"
className="cat-img detailed-img"
/>
<div className="cat-desc">
<h2>Cat Details</h2>
</div>
</div>
)}
</div>
);
};
export default App;
并添加如下 css 代码:
css
.cat-img {
view-transition-name: meow-image;
}
.cat-details {
display: flex;
}
.hidden {
display: none;
}
.cat-desc {
flex: 1;
}
.top-bar {
display: flex;
justify-content: space-between;
align-items: start;
}
.top-bar-content {
display: flex;
align-items: center;
gap: 2rem;
}
.thumbnail {
width: 100px;
height: 100px;
}
.detailed-img {
width: 50vw;
height: auto;
}
最终效果如下图所示:
将 View Transition API 与 React Router 结合使用
使用 View Transition API 与 React Router 大致相同,只是您在 document.startViewTransition 的回调中调用钩子 useNavigate 来导航到新页面而不是 setState 。
具体代码如下所示:
jsx
import * as React from "react";
import { flushSync } from "react-dom";
import { createRoot } from "react-dom/client";
import {
useNavigate,
createBrowserRouter,
RouterProvider,
} from "react-router-dom";
import "./index.css";
const AnimatedLink = ({ to, children }) => {
const navigate = useNavigate();
return (
<a
href={to}
onClick={(ev) => {
ev.preventDefault();
document.startViewTransition(() => {
flushSync(() => {
navigate(to);
});
});
}}
>
{children}
</a>
);
};
const TopBar = ({ link, rightContent }) => (
<div className="top-bar">
<div className="top-bar-content">
<h1>Move Cat</h1>
{link}
</div>
{rightContent}
</div>
);
const router = createBrowserRouter([
{
index: true,
element: (
<div>
<TopBar
link={<AnimatedLink to="/details">Details</AnimatedLink>}
rightContent={
<img
src="https://res.cloudinary.com/djzsjzasg/image/upload/c_scale,w_300/v1678947391/malcolm-kee/meow_dtsn8h.png"
alt="cat"
className="cat-img thumbnail"
/>
}
/>
</div>
),
},
{
path: "/details",
element: (
<div>
<TopBar link={<AnimatedLink to="/">Home</AnimatedLink>} />
<div className="cat-details">
<img
src="https://res.cloudinary.com/djzsjzasg/image/upload/c_scale,w_500/v1678947391/malcolm-kee/meow_dtsn8h.png"
alt="cat"
className="cat-img detailed-img"
/>
<div className="cat-desc">
<h2>Cat Details</h2>
</div>
</div>
</div>
),
},
]);
createRoot(document.getElementById("root")).render(
<RouterProvider router={router} />
);
视频位置转换案例
现在来我们开始上点有难度的动画,如果你想查看完整的源代码,你可以通过 codesandbox 进行查看:
表单创建和删除案例
现在来我们开始上点有难度的动画,如果你想查看完整的源代码,你可以通过 codesandbox 进行查看:
如何禁止 view transitions
关闭视图转换的最佳方法是将元素的 view-transition-name 属性设置为 none。
当我们使用这个属性给元素分配一个名称时,它们就会获得视图过渡效果。这甚至适用于根元素,当我们在页面上激活 View Transitions API 时,根元素会自动设置为"root"。
css
:root {
view-transition-name: root;
}
这是用户代理的默认样式,我们可以通过将其设置为"none"来取消它,从而禁用其视图过渡,就像这样
css
:root {
view-transition-name: none;
}
/* or any elements */
.some-element {
view-transition-name: none;
}
参考文献
- Using View Transition API in React App
- Smooth and simple transitions with the View Transitions API
- View Transitions API & meta frameworks: a practical guide
总结
View Transition API 提供了一种简单的方式来为 Web 应用程序创建流畅的动画效果。通过在你的 React 项目中实现它并使用渐进增强,你可以覆盖更多的浏览器,同时提升用户体验。
最后分享两个我的两个开源项目,它们分别是:
这两个项目都会一直维护的,如果你也喜欢,欢迎 star 🥰🥰🥰