外观
第25章—用react-transition-group和react-spring做过渡动画
3842字约13分钟
2024-09-19
前面我们学了用 react-spring 做属性变化的动画,以及和 use-gesture 手势库结合做一些交互触发的动画。
其实还有一种动画类型没有涉及,就是过渡动画。
过渡动画是当元素进入、离开的时候(也就是添加到 dom 和从 dom 移除的时候)触发的动画。
比如这个:
有同学说,上节不是做过这个么?
不一样,上节我们是一个列表里多个元素根据 index 计算 x,多个元素同时存在。
而这个是只存在一个,切换的时候其余元素会添加到 dom 和从 dom 中移除,从而触发进入、离开的过渡动画。
我们来写一下就知道了。
创建个项目:
安装 react-spring 的包:
npm install --save @react-spring/web
改下 App.tsx:
import React, { useState, CSSProperties } from 'react'
import { useTransition, animated, AnimatedProps } from '@react-spring/web'
import './App.css';
interface PageItem {
(props: AnimatedProps<{ style: CSSProperties }>): React.ReactElement
}
const pages: Array<PageItem> = [
({ style }) => <animated.div style={{ ...style, background: 'lightpink' }}>A</animated.div>,
({ style }) => <animated.div style={{ ...style, background: 'lightblue' }}>B</animated.div>,
({ style }) => <animated.div style={{ ...style, background: 'lightgreen' }}>C</animated.div>,
]
export default function App() {
const [index, set] = useState(0);
const onClick = () => set(state => (state + 1) % 3);
const transitions = useTransition(index, {
from: { transform: 'translate3d(100%,0,0)' },
enter: { transform: 'translate3d(0%,0,0)' },
leave: { transform: 'translate3d(-100%,0,0)' },
})
return (
<div className='container' onClick={onClick}>
{transitions((style, i) => {
const Page = pages[i]
return <Page style={style} />
})}
</div>
)
}
从上往下来看。
pages 是一个数组,接收 style 作为参数,返回 ReactElement。
这个 style 是被 react-spring 处理过后的 style,所以是 AnimatedProps 类型:
不是都用 ReactNode 来表示 JSX 么,为什么这里用 ReactElement?
讲 react 的 ts 类型的时候,讲过 ReactNode 包括 ReactElement 类型还包括 string、number 等类型:
但是这里要把 pages 的元素作为 ReactElement 来用,而 string、number 等都是不可以的,所以不能写 ReactNode:
然后我们声明了一个 index 的 state,在点击的时候修改了它:
而渲染的时候只渲染这一个,之前渲染的组件就会被销毁,从而触发过渡动画:
这是和上节那个类似效果的区别,上节那个并不是同时只保留一个,所以不是过渡动画。
然后这里用到了 useTransition,它的第一个参数就是会变化的状态,当状态变化的时候就会触发进入、离开动画:
分别指定了初始状态(from),进入的时候(enter),离开的时候(leave)会变化的 style。
这样,当 index 变化的时候,这些 style 就会变,从而触发动画:
在 App.css 写下样式:
.container > div {
position: absolute;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
color: white;
font-weight: 800;
font-size: 300px;
}
跑一下:
npm run start
没啥问题。
但这只是一个元素的过渡动画,如果多个元素呢?
新建 App2.tsx
import React, { useState } from "react";
import "./App2.css";
import { useTransition, animated } from '@react-spring/web'
export default function App() {
const [items, setItems] = useState([
{ id: 1, text: "guang" },
{ id: 2, text: "guang" },
]);
const transitions = useTransition(items, {
from: { transform: 'translate3d(100%,0,0)', opacity: 0 },
enter: { transform: 'translate3d(0%,0,0)', opacity: 1 },
leave: { transform: 'translate3d(-100%,0,0)', opacity: 0 },
});
return (
<div>
<div className="item-box">
{transitions((style, i) => {
return <animated.div className="item" style={style}>
<span
className="del-btn"
onClick={() => {
setItems(items.filter((item) => item.id !== i.id));
}}
>
x
</span>
{i.text}
</animated.div>
})}
</div>
<div
className="btn"
onClick={() => {
setItems([...items, { id: Date.now(), text: 'guang' }]);
}}
>
Add
</div>
</div>
);
}
还有 App2.css
.item-box {
width: 300px;
margin: 20px auto;
}
.item {
margin: 4px 0;
padding: 10px 0;
border-radius: 4px;
background: lightblue;
}
.del-btn {
padding: 0 10px;
cursor: pointer;
user-select: none;
}
.btn {
color: #fff;
background-color: #0069d9;
border-color: #0062cc;
padding: 10px 20px;
border-radius: 4px;
width: fit-content;
cursor: pointer;
margin: 20px auto;
}
引入这个组件跑一下:
可以看到,每个元素都加上了过渡动画。
回过头来看下代码:
列表项的增删就不详述了。
useTransition 多个元素的时候,你会发现和之前一模一样:
useTransition 传单个数据就是单个元素的过渡动画、传数组就是多个元素的过渡动画,写法一样。
此外,现在是刚开始所有元素会做一次动画:
如果我只是想在增删元素的时候才有动画呢?
设置下 initial 时的样式就可以了:
const transitions = useTransition(items, {
initial: { transform: 'translate3d(0%,0,0)', opacity: 1 },
from: { transform: 'translate3d(100%,0,0)', opacity: 0 },
enter: { transform: 'translate3d(0%,0,0)', opacity: 1 },
leave: { transform: 'translate3d(-100%,0,0)', opacity: 0 },
});
这样最开始就不会做一次动画,只有在增删元素的时候会触发过渡动画:
这就是用 react-spring 的 useTransition 做过渡动画的方式。
此外,最好加上 keys,react-spring 会根据这个来添加 key,从而识别出元素的增删:
其实提到过渡动画,还有一个老牌的库 react-transition-group。
这个库是 react 官方出的,而且用的也很多。
看 npm 包的周下载量:
react-transition-group 的周下载量:
@react-spring/web 的周下载量:
好家伙,10 倍的差距。
这并不是说 react-transition-group 做过渡动画更优秀,只是它出现的更早更加流行。
我们来试一下:
安装 react-transition-group:
npm install --save react-transition-group
npm install --save-dev @types/react-transition-group
不同于 react-spring 是在组件 enter、leave 的时候直接修改 style。
react-transition-group 是通过改变 className 来给组件加上的过渡效果的。
创建 App3.tsx:
import { useEffect, useState } from 'react';
import { CSSTransition } from 'react-transition-group';
import './App3.css';
function App() {
const [flag, setFlag] = useState(false);
useEffect(() => {
setTimeout(() => {
setFlag(true);
}, 3000);
}, []);
return <CSSTransition
in={flag}
timeout={1000}
>
<div id="box"></div>
</CSSTransition>
}
export default App;
App3.css
#box {
width: 300px;
height: 50px;
background: lightblue;
margin: 200px auto;
}
.enter {
transform: translateX(-100%);
opacity: 0;
}
.enter-active {
transform: translateX(0);
opacity: 1;
transition: all 1s ease;
}
.enter-done {
border: 5px solid #000;
}
参数 in 设置为 true 就是触发进入的动画,设置为 false 就是触发离开的动画。
跑一下:
可以看到,当 in 变为 true 的时候,会触发进入的动画,依次给元素加上 .enter、.enter-active、.enter-done 的 className。
这样,我们就把过渡的样式加到这三个 className 里就可以了。
而且不同于 react-spring 会自己控制动画效果,react-transition-group 只是加上 className 不负责动画效果,需要添加 transition 来实现动画。
CSSTransition 组件会先设置 enter,再设置 enter-active,这样就触发动画了。
然后到了 timeout 参数的时间,就会设置 enter-done 的 className。
反之,如果 in 的参数改为 false,就会触发离开动画:
className 会先设置 exit,再设置 exit-active 来触发动画,到了 timeout 的时间会设置为 exit-done。
连起来,就可以实现 enter 和 exit 的动画:
import { useEffect, useState } from 'react';
import { CSSTransition } from 'react-transition-group';
import './App3.css';
function App() {
const [flag, setFlag] = useState(false);
return <div>
<CSSTransition
in={flag}
timeout={1000}
>
<div id="box"></div>
</CSSTransition>
<button onClick={() => setFlag(!flag)}>{!flag ? '进入' : '离开'}</button>
</div>
}
export default App;
#box {
width: 300px;
height: 50px;
background: lightblue;
margin: 100px auto;
}
button {
margin: 0 auto;
display: block;
}
.enter {
transform: translateX(-100%);
opacity: 0;
}
.enter-active {
transform: translateX(0);
opacity: 1;
transition: all 1s ease;
}
.enter-done {
border: 5px solid #000;
}
.exit {
transform: translateX(0%);
opacity: 1;
}
.exit-active {
transform: translateX(100%);
opacity: 0;
transition: all 1s ease;
}
.exit-done {
}
可以看到,通过 className 从 enter 到 enter-active 到 enter-done 的变化,以及从 exit 到 exit-active 到 exit-done 的变化,就实现了进入和离开的动画。
不知道大家有没有发现,最开始出现的时候是没有动画的,之后后来切换 in 的 props 的时候,才有动画。
如果想最开始出现的时候就做一次动画呢?
这就需要设置 appear 的 props 了:
.appear {
transform: scale(0);
}
.appear-active {
transform: scale(1);
transition: all 1s ease;
}
.appear-done {
}
可以看到,最开始还有一个 appear、appear-active、appear-done 的 className 变化,并且还会添加 enter-done。
这个只会在刚出现的时候设置一次。
也就是一共可以有 appear、enter、exit 3 种过渡动画。
这个在 react-spring 里也是一样,它默认是有 appear 动画的:
我们当时是设置了 initial 时的样式,然后就没有 apear 动画了:
也就是说用 react-spring 和 react-transition-group 都能实现一样的过渡动画的功能。
而且 react-transition-group 只是添加 className 要自己用 transition 来做动画,而 react-spring 是自带动画效果。
继续看 react-transition-group,现在是我们自己设置 in 的 props 来触发进入和离开动画的,如果是列表的多个 child,都想加动画呢?
这时候就用 TransitionGrop 组件。
创建 App4.tsx
import React, { useState } from "react";
import { CSSTransition, TransitionGroup } from "react-transition-group";
import "./App4.css";
export default function App() {
const [items, setItems] = useState([
{ id: 1, text: "guang" },
{ id: 2, text: "guang" },
]);
return (
<div>
<TransitionGroup className="item-box">
{items.map(({ id, text }) => (
<CSSTransition key={id} timeout={1000}>
<div className="item">
<span
className="del-btn"
onClick={() => {
setItems(items.filter((item) => item.id !== id));
}}
>
x
</span>
{text}
</div>
</CSSTransition>
))}
</TransitionGroup>
<div
className="btn"
onClick={() => {
setItems([...items, { id: Date.now(), text: 'guang' }]);
}}
>
Add
</div>
</div>
);
}
就是用 TransitionGroup 包裹下,
.item-box {
width: 300px;
margin: 20px auto;
}
.item {
margin: 4px 0;
padding: 10px 0;
border-radius: 4px;
background: lightblue;
}
.del-btn {
padding: 0 10px;
cursor: pointer;
user-select: none;
}
.enter {
opacity: 0;
transform: translateX(-100%);
background: lightblue;
}
.enter-active {
opacity: 1;
transform: translateX(0%);
background: lightblue;
transition: all 1s ease;
}
.enter-done {
}
.exit {
opacity: 1;
transform: translateX(0%);
background: red;
}
.exit-active {
opacity: 0;
transform: translateX(100%);
background: red;
transition: all 1s ease;
}
.btn {
color: #fff;
background-color: #0069d9;
border-color: #0062cc;
padding: 10px 20px;
border-radius: 4px;
width: fit-content;
cursor: pointer;
margin: 20px auto;
}
效果就是前面用 react-spring 实现过一遍的那个:
用 CSSTransition 的时候,我们需要自己设置 in 的 props 来触发进入和离开动画。
而现在只需要设置 key,TransitionGroup 会在 children 变化的时候对比新旧 item,来自动设置 in,触发动画。
这就是 react-transition-group 的常用功能。
此外,它还有两个组件,Transition 和 SwitchTransition:
把 CSSTransition 换成 Transition,然后打印下 status:
可以看到,status 最开始是从 entering 到 entered,从 exiting 到 exited 变化,但是不会设置 className:
我们可以根据 status 的变化自己设置 className。
其实,CSSTransition 就是基于 Transition 封装的。
一般我们用 CSSTransition 就好了。
再就是 SwithTransition,先看下效果:
包裹一层 SwitchTransition,然后设置下 key。
当 mode 为 in-out 时:
当 mode 为 out-in 时:
这个组件就是用来控制两个组件切换时的进入、离开动画的顺序的。
这样,react-transition-group 的 4 个组件: Transition、CSSTransition、TransitionGroup、SwitchTransition 我们就都过了一遍。
案例代码上传了小册仓库
总结
当组件进入 dom 和从 dom 中移除的时候,发生的动画就叫做过渡动画。
react-spring 有 useTransition 这个 hook 来实现过渡动画,我们也可以用 react-trasition-group 这个包来实现。
这两个包能实现一样的功能,但是思路不同。
react-spring 有内置的动画效果,所以只要用 useTransition 设置 from、enter、leave 时的 style,它就会在数据变化的时候触发过渡动画。
而 react-transition-group 是通过 className 的修改来实现过渡动画,而且要自己用 transition 的 css 来控制动画效果:
进入的时候会触发 enter、enter-active、enter-done 的 className 切换
离开的时候是 exit、exit-active、exit-done 的切换
如果设置了 appear 参数,刚出现的时候,还会有 appear、appear-active、appear-done 的切换。
它有 Transition、CSSTransition、TransitionGroup、SwitchTransition 这 4 个组件。
常用的就是 CSSTransition 和 TransitionGroup,这俩是用来做单个元素的过渡动画和多个元素的过渡动画的。
而在 react-spring 里,单个元素的过渡动画和多个元素的过渡动画写法没区别。
具体用哪种方案来实现过渡动画都行,都是很流行的方案。