3、CSSTransition实战:用CSSTransition实现常见入场/离场动画
好,咱们今天来点真格的。
上一章我们聊了React Transition Group的基本概念,说白了就是搭建了一个动画框架。但光有架子不行,得往里填东西。这一章,我们就用CSSTransition这个核心组件,把淡入淡出、滑动、缩放这些最常见的入场/离场动画一个个实现出来。
我个人习惯把CSSTransition叫做「CSS状态机」——它本质上就是在不同阶段往DOM元素上贴不同的class名,然后你通过CSS来控制这些class对应的样式变化。就这么简单,但威力不小。
3.1 先搭个基础架子
动手之前,先看看CSSTransition的基本用法。我直接上代码,你感受一下:
import { CSSTransition } from 'react-transition-group';
import { useState } from 'react';
function FadeDemo() {
const [show, setShow] = useState(true);
return (
<div>
<button onClick={() => setShow(!show)}>
{show ? '隐藏' : '显示'}
</button>
<CSSTransition
in={show}
timeout={300}
classNames="fade"
unmountOnExit
>
<div className="box">我是会淡入淡出的盒子</div>
</CSSTransition>
</div>
);
}
这里有几个关键属性,我挨个说一下:
- in:布尔值,控制显示还是隐藏。true就入场,false就离场。
- timeout:动画持续时间,单位毫秒。这个值要和CSS里的transition时长保持一致,不然会出现动画没播完就消失的尴尬情况。
- classNames:CSS类名的前缀。比如我写了
"fade",那组件就会自动生成fade-enter、fade-enter-active、fade-exit、fade-exit-active这些class。 - unmountOnExit:离场动画结束后,直接从DOM里移除元素。这个我强烈建议加上,不然隐藏的元素还占着位置,容易出问题。
timeout={{ enter: 300, exit: 200 }},这样入场和离场动画时长可以不一样。我在做弹窗组件时经常这么用——入场快一点,离场慢一点,用户体验更好。
3.2 淡入淡出动画
好,架子搭完了,咱们开始写CSS。淡入淡出是最基础的动画,说白了就是控制透明度从0到1,或者从1到0。
/* 入场前状态 */
.fade-enter {
opacity: 0;
}
/* 入场动画执行中 */
.fade-enter-active {
opacity: 1;
transition: opacity 300ms ease-in;
}
/* 离场前状态 */
.fade-exit {
opacity: 1;
}
/* 离场动画执行中 */
.fade-exit-active {
opacity: 0;
transition: opacity 300ms ease-out;
}
嗯,这里要注意一个细节。fade-enter是动画开始前的状态,fade-enter-active是动画执行中的状态。组件挂载时,会先加上fade-enter,然后在下一帧立刻换成fade-enter-active。这个「下一帧」的机制很关键,没有它,CSS transition就触发不了。
我曾经踩过一个坑:把opacity: 0和opacity: 1都写在fade-enter-active里,结果动画完全不生效。为什么?因为浏览器没检测到状态变化,自然不会触发过渡。记住,初始状态和结束状态必须分在两个class里。
3.3 滑动动画
淡入淡出太单调了?那咱们加点方向感。滑动动画,说白了就是让元素从某个方向「滑」进来。
我个人最常用的是从上往下滑,做通知栏或者下拉菜单时特别好使:
/* 从上方滑入 */
.slide-enter {
transform: translateY(-100%);
opacity: 0;
}
.slide-enter-active {
transform: translateY(0);
opacity: 1;
transition: all 300ms ease-out;
}
.slide-exit {
transform: translateY(0);
opacity: 1;
}
.slide-exit-active {
transform: translateY(-100%);
opacity: 0;
transition: all 300ms ease-in;
}
你想想看,这里我同时控制了transform和opacity,效果就是元素一边滑下来一边变清晰。比单纯的滑动要有质感得多。
如果想从右边滑入,把translateY改成translateX(100%)就行。从左边就是translateX(-100%)。这个套路很固定,你记住一个方向,其他方向就是改个参数的事。
transform而不是top/left。因为transform只触发合成层,不触发重排和重绘,性能好得多。我在移动端项目里测试过,用transform的帧率能稳定在60fps,用top直接掉到30fps以下。
3.4 缩放动画
缩放动画适合做弹窗、图片预览、或者那种「从一个小点炸开」的效果。实现起来也不复杂:
.scale-enter {
transform: scale(0);
opacity: 0;
}
.scale-enter-active {
transform: scale(1);
opacity: 1;
transition: all 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
.scale-exit {
transform: scale(1);
opacity: 1;
}
.scale-exit-active {
transform: scale(0);
opacity: 0;
transition: all 200ms ease-in;
}
注意看,我入场动画用了cubic-bezier(0.34, 1.56, 0.64, 1)。这个贝塞尔曲线有个特点——它会先超过终点再弹回来,产生一种「弹性」的感觉。说白了就是让动画更有活力,不那么死板。
离场动画我用了200ms,比入场短。为什么?因为用户关闭弹窗时通常希望它快点消失,别磨叽。这个设计细节是我从实际项目中总结出来的。
transform-origin,结果元素从左上角缩放,看起来特别奇怪。如果你想让元素从中心缩放,记得加上transform-origin: center center。弹窗类组件建议用这个,下拉菜单类组件可能从顶部缩放更自然。
3.5 组合动画:淡入 + 滑动 + 缩放
单个动画玩熟了,咱们可以组合起来用。比如做一个卡片入场效果,让它一边滑上来一边放大一边变清晰:
.card-enter {
transform: translateY(50px) scale(0.9);
opacity: 0;
}
.card-enter-active {
transform: translateY(0) scale(1);
opacity: 1;
transition: all 400ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
.card-exit {
transform: translateY(0) scale(1);
opacity: 1;
}
.card-exit-active {
transform: translateY(20px) scale(0.95);
opacity: 0;
transition: all 250ms ease-in;
}
这里我把translateY和scale写在了同一个transform里。注意顺序——先位移再缩放,效果更自然。反过来先缩放再位移,元素移动的路径会变,看起来怪怪的。
组合动画的关键是「统一节奏」。所有属性用同一个transition时长,或者至少时长相近。不然会出现透明度已经变完了,位置还没到位的情况,那画面就割裂了。
3.6 实际项目中的封装思路
每次写动画都重复这些class很烦,对吧?我一般会封装成一个Animate组件:
function Animate({ children, type = 'fade', timeout = 300, show }) {
const classMap = {
fade: 'fade',
slide: 'slide',
scale: 'scale',
card: 'card'
};
return (
<CSSTransition
in={show}
timeout={timeout}
classNames={classMap[type]}
unmountOnExit
>
{children}
</CSSTransition>
);
}
// 使用
<Animate type="slide" show={visible}>
<div>滑进来的内容</div>
</Animate>
这样项目里所有动画都统一管理,想改动画时长或者效果,改一个地方就行。我在一个中型后台项目里就是这么干的,维护起来特别省心。
3.7 常见问题与调试技巧
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 动画不执行 | timeout和CSS transition时长不一致 | 确保两者数值相同 |
| 元素闪一下才消失 | 忘记加unmountOnExit | 加上unmountOnExit属性 |
| 入场动画不触发 | 初始状态和结束状态写在了同一个class里 | 把初始状态写在-enter里,结束状态写在-enter-active里 |
| 动画卡顿 | 使用了会触发重排的属性(如top/left) | 改用transform和opacity |
调试动画时,我建议你在浏览器开发者工具里打开「动画」面板(Chrome DevTools -> More Tools -> Animations)。它能录制和回放动画,还能看到每一帧的变化。我曾经靠这个面板找到了一个因为z-index导致动画被遮挡的bug,省了我半天时间。
好了,这一章的内容就到这里。淡入淡出、滑动、缩放,再加上组合动画,你已经掌握了最常见的入场/离场动画写法。下一章我们会聊更高级的用法——列表动画和动态切换动画,到时候你会看到TransitionGroup和CSSTransition配合起来有多强大。