感谢 @枫上雾棋 的投稿,本文翻译自 React Animations in Depth。
以前一直投入在 React Native 中,写动画的时候不是用 CSS 中的 transitions / animations,就是依赖像 GreenSock 这样的库,最近转向 Web,在 Tweet 得到很多大佬关于React Web 动画 的回应,于是决定分享给大家,如有其他见解,非常欢迎在下面评论中交流
以下便是本文要分享的创建 React 动画 的几种方式
-
CSS animation
-
JS Style
-
React Motion
-
Animated
-
Velocity React
下面,勒次个特斯大特一特
CSS animation
给元素添加 class 是最简单,最常见的书写方式。如果你的 app 正在使用 CSS,那么这将是你最愉快的选择
赞同者: 我们只需修改 opacity 和 transform 这样的属性,就可构建基本的动画,而且,在组件中,我们可以非常容易地通过 state 去更新这些值
反对者:这种方式并不跨平台,在 React Native 中就不适用,而且,对于较复杂的动画,这种方式难以控制
接下来,我们通过一个实例来体验一下这种创建方式:当 input focus 的时候,我们增加它的宽度
首先,我们要创建两个 input 要用到的 class
.input {
width: 150px;
padding: 10px;
font-size: 20px;
border: none;
border-radius: 4px;
background-color: #dddddd;
transition: width .35s linear;
outline: none;
}
.input-focused {
width: 240px;
}
一个是它原始的样式,一个是它 focus 后的样式
下面,我们就开始书写我们的 React 组件
在此,推荐一个 在线的 React VS Code IDE,真的很强大,读者不想构建自己的 React app,可以在其中检验以下代码的正确性
class App extends Component {
state = {
focused: false,
}
componentDidMount() {
this
._input.addEventListener('focus', this.focus);
this._input.addEventListener('blur', this.focus);
}
focus = () => {
this.setState(prevState => ({
focused: !prevState.focused,
}));
}
render() {
return (
<div className="App">
<div className="container">
<input
ref={input => this
._input = input}
className={['input', this.state.focused && 'input-focused'].join(' ')}
/>
div>
div>
);
}
}
-
我们有一个 focused 的 state,初始值为 false,我们通过更新该值来创建我们的动画
-
在 componentDidMount 中,我们添加两个监听器,一个 focus,一个 blur,指定的回调函数都是 focus
-
focus 方法会获取之前 focused 的值,并负责切换该值
-
在 render 中,我们通过 state 来改变 input 的 classNames,从而实现我们的动画
JS Style
JavaScipt styles 跟 CSS 中的 classes 类似,在 JS 文件中,我们就可以拥有所有逻辑
赞同者:跟 CSS动画 一样,且它的表现更加清晰。它也不失为一个好方法,可以不必依赖任何 CSS
反对者:跟 CSS动画 一样,也是不跨平台的,且动画一旦复杂,也难以控制
在下面的实例中,我们将创建一个 input,当用户输入时,我们将一个 button 从 disable 转变为 enable
class App
extends Component {
state = {
disabled: true,
}
onChange = (e) => {
const length = e.target.value.length;
if (length > 0) {
this.setState({ disabled: false });
} else {
this.setState({ disabled: true });
}
}
render() {
const { disabled } = this.state;
const label = disabled ? 'Disabled' : 'Submit';
return (
<div style={styles.App}>
<input
style={styles.input}
onChange={this.onChange}
/>
<button
style={Object.assign({},
styles.button,
!this.state.disabled && styles.buttonEnabled
)}
disabled={disabled}
>
{label}
button>
div>
);
}
}
const styles = {
App: {
display: 'flex',
justifyContent: 'left',
},
input: {
marginRight
: 10,
padding: 10,
width: 190,
fontSize: 20,
border
: 'none',
backgroundColor: '#ddd',
outline: 'none',
},
button: {
width
: 90,
height: 43,
fontSize: 17,
border: 'none',
borderRadius:
4,
transition: '.25s all',
cursor: 'pointer',
},
buttonEnabled: {
width
: 120,
backgroundColor: '#ffc107',
}
}
-
我们有一个 disabled 的 state,初始值为 true
-
onChange 方法会获取用户的输入,当输入非空时,就切换 disabled 的值
-
根据 disabled 的值,确定是否将 buttonEnabled 添加到 button 中
React Motion
React Motion 是 Cheng Lou 书写的一个非常不错的开源项目。它的思想是你可以对Motion 组件 进行简单的样式设置,然后你就可以在回调函数中通过这些值,享受动画带来的乐趣
对于绝大多数的动画组件,我们往往不希望对动画属性(宽高、颜色等)的变化时间做硬编码处理,react-motion 提供的 spring 函数就是用来解决这一需求的,它可以逼真地模仿真实的物理效果,也就是我们常见的各类缓动效果
下面是一个森破的示例
<
Motion style={{ x: spring(this.state.x) }}>
{
({ x }) =>
<div style={{ transform: `translateX(${x}px)` }} />
}
Motion>
这是官方提供的几个 demo,真的可以是不看不知道,一看吓一跳
-
Chat Heads
-
Draggable Balls
-
TodoMVC List Transition
-
Water Ripples
-
Draggable List
赞同者:React Motion 可以在 React Web 中使用,也可以在 React Native 中使用,因为它是跨平台的。其中的 spring 概念最开始对我来说感觉挺陌生,然而上手之后,发现它真的很神奇,并且,它有很详细的 API
反对者:在某些情况下,他不如纯 CSS / JS 动画,虽然它有不错的 API,容易上手,但也需要学习成本
为了使用它,首先我们要用 yarn 或 npm 安装它
yarn add react
-motion
在下面的实例中,我们将创建一个 dropdown 菜单,当点击按钮时,下拉菜单友好展开
class App extends Component {
state = {
height
: 38,
}
animate = () => {
this.setState((state) => ({ height: state.height === 233 ? 38 : 233 }));
}
render() {
return (
<div className="App">
<div style={styles.button} onClick={this.animate}>Animatediv>
<Motion
style={{ height: spring(this.state.height) }}
>
{
({ height }) =>
<div style={Object.assign({}, styles.menu, { height } )}>
<p style={styles.selection}>Selection 1p>
<
p style={styles.selection}>Selection 2p>
<p style={styles.selection}>Selection 3p>
<p style={styles.selection}>Selection 4p>
<
p style={styles.selection}>Selection 5p>
<p style={styles.selection}>Selection 6p>
div>
}
Motion>
div>
);
}
}
const styles = {
menu: {
marginTop
: 20,
width: 300,
border: '2px solid #ddd',
overflow: 'hidden',
},
button: {
display: 'flex',
width: 200,
height
: 45,
justifyContent: 'center',
alignItems: 'center',
border: 'none',
borderRadius
: 4,
backgroundColor: '#ffc107',
cursor: 'pointer',
},
selection: {
margin: 0,
padding: 10,
borderBottom
: '1px solid #ededed',
},
}
-
我们从 react-motion 中 import Motion 和 spring
-
我们有一个 height 的 state,初始值为 38,代表 menu 的高度
-
animate 方法设置 menu 的 height,如果 原 height 为 38,则设置新 height 为 233,如果 原 height 为 233,则设置 新 height 为 38
-
在 render 中,我们使用 Motion 组件 包装整个 p 标签 列表,将 this.state.height 的当前值设为组件的 height,然后在组件的回调函数中使用该值作为整个下拉的高度
-
当按钮被点击时,我们通过 this.animate 切换下拉的高度
Animated
Animated 是基于 React Native 使用的同一个动画库建立起来的
它背后的思想是创建声明式动画,通过传递配置对象来控制动画
赞同者:跨平台,它在 React Native 中已经非常稳定,如果你在 React Native 中使用过,那么你将不用再重复学习。其中的 interpolate 是一个神奇的插值函数,我们将在下面看到
反对者:基于 Twitter 的交流,它目前貌似不是 100% 的稳定,在老的浏览器中的,存在前缀和性能的问题,而且,它也有学习成本
为了使用 Animated,我们首先还是要用 yarn 或 npm 安装它
yarn add animated
在下面的实例中,我们将模拟在提交表单成功后显示的动画 message
import
Animated from 'animated/lib/targets/react-dom';
import Easing from 'animated/lib/Easing';
class AnimatedApp extends Component {
animatedValue
= new Animated.Value(0);
animate = () => {
this.animatedValue.setValue(0);
Animated.timing(
this.animatedValue,
{
toValue: 1,
duration: 1000,
easing
: Easing.elastic(1),
}
).start();
}
render() {
const marginLeft = this.animatedValue.interpolate({
inputRange: [0, 1],
outputRange: [-120, 0],
});
return (
<div className="App">
<div style={styles.button} onClick={this.animate}>Animatediv>
<Animated.div
style={
Object.assign(
{},
styles
.box,
{ opacity: this.animatedValue, marginLeft })}
>
<p>Thanks for your submission!p>
Animated.div>
div>- 我们将 `animatedValue`
和 `marginLeft` 作为 `Animated.div ` 的 `style` 属性, );
}
}
const styles
= {
button: {
display: 'flex',
width
: 125,
height: 50,
justifyContent: 'center',
alignItems
: 'center',
border: 'none',
borderRadius: 4,
backgroundColor
: '#ffc107',
cursor: 'pointer',
},
box: