简评:这篇文章将介绍五种可选方式来创建 React Web 动画,其中有一些是跨平台的(可以支持 React Native )
1. 基于 React 组件状态的 CSS 动画
对于我来说最基础也是最显然的来创建动画就是使用 CSS 类的属性并通过添加或删除他们来展现动画。如果在你的应用中已经使用了 CSS,这是种很好的方式来实现基础动画。
缺点:不是跨平台的(不支持 React Native),依赖于 CSS 和 DOM,如果需要实现复杂的效果,这种方式会变得难以控制。
优点:高性能。关于 CSS 动画,有一条已知的规则:除了透明度和变换意外,不要改变任何属性,通常会有很棒的性能。基于状态更新这些值非常简单,而且只要简单地重新渲染我们的组件就能达到平滑变换的效果。
看个例子:我们将会基于 React 组件使用 CSS 动画来动画化一个 input 组件。
首先我们要创建两个类关连上我们的 input:
.input {
transition: width .35s linear;
outline: none;
border: none;
border-radius: 4px;
padding: 10px;
font-size: 20px;
width: 150px;
background-color: #dddddd;
}
.input-focused {
width: 240px;
}
我们有一些基础的属性,并且我们设置了 width .35 linear 的变换,给动画一些属性。
同时 input-focused 类将把宽度从 150 px 改动到 240 px。
现在在我们的 React 应用中把他们用起来:
class App extends Component {
state = {
focused: false
}
componentDidMount() {
this.input.addEventListener('focus', this.focus);
this.input.addEventListener('blur', this.focus);
}
focus = () => {
this.setState((state) => ({ focused: !state.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 状态并设为 false。我们将用这个状态出发更新我们动画化的组件。
在 componentDidMount 中,我们添加了两个监听器,一个监听 blur,一个监听 focus。两个监听器都能够调用 focus 方法。注意到我们正在引用 this.input,这是因为我们使用 ref 方法创建了一个引用,然后把它设置为一个类属性。我们在 componentDidMount 中做这些因为在 componentWillMount 时我们还没有进入 dom。
focus 方法会检查上个 focused 状态的值,并基于他的值来触发。
在 render 中,主要注意的是我们给 input 设置了 classNames。我们检查 this.state.focused 是否为 true,如果是,我们会加入 input-focused 类。我们创建了一个数组,并调用 .join('') 作为一个可用的 className。
2. 基于 React 组件状态的 JS 样式动画
用 JS 样式来创建动画的方式和用 CSS 类有点相似。好处是你可以获得相同的性能,但你不用依赖 CSS 类,你可以在 JS 文件中写上所有的逻辑。
优点:像 CSS 动画,好处是性能杠杠的。同样也是种很好的方式,因为你不需要依赖于任何 CSS 文件。
缺点:同样和 CSS 动画一样,不是跨平台的(不支持 React Native),依赖于 CSS 和 DOM,如果要创造复杂的动画,会变得难以控制。
这个例子中,我们会创建一个输入框,当用户输入时,会变成可点击和不可点击的状态,给予用户反馈。
class App extends Component {
state = {
disabled: true,
}
onChange = (e) => {
const length = e.target.value.length;
if (length >= 4) {
this.setState(() => ({ disabled: false }))
} else if (!this.state.disabled) {
this.setState(() => ({ disabled: true }))
}
}
render() {
const label = this.state.disabled ? 'Disabled' : 'Submit';
return (
<div className="App">
<button
style={Object.assign({}, styles.button, !this.state.disabled && styles.buttonEnabled)}
disabled={this.state.disabled}
>{label}</button>
<input
style={styles.input}
onChange={this.onChange}
/>
</div>
);
}
}
const styles = {
input: {
width: 200,
outline: 'none',
fontSize: 20,
padding: 10,
border: 'none',
backgroundColor: '#ddd',
marginTop: 10,
},
button: {
width: 180,
height: 50,
border: 'none',
borderRadius: 4,
fontSize: 20,
cursor: 'pointer',
transition: '.25s all',
},
buttonEnabled: {
backgroundColor: '#ffc107',
width: 220,
}
}
初始化一个 disabled 状态,设为 true。
onChange 方法绑定了 input,我们会检查输入了多少个字符。如果有 4 个或以上,我们将 disabled 设为 false,否则它还没被设为 true 的话那就设为 true。
按钮元素的样式属性将会决定添加动画类 buttonEnabled 与否,取决于 this.state.disabled的值。
按钮的样式有一个 .25s all 的变换,因为我们想让 backgroundColor 和 width 属性同时动画化。
3. React Motion
React Motion 是 Cheng Lou(华裔 FB 大神,不确定国籍)写的很棒的库,他在动画方面工作超过 2 年了,包括 React Web 和 React Native。他在 2015 年的 React Europe 上发表了一个很棒的关于讨论动画的演讲。
React Motion 背后的思想是它将 API 引用的内容作为 “Spring”,这是一个非常稳定的基础动画配置,在大多数情况下工作良好,同时也是可配置的。它不依赖于时间,所以当你想要取消/停止/撤销一个动画或者在你的应用中使用可变维度的时候会更好用。
React Motion 的用法是你在一个 React Motion 组件中设置一个样式配置,然后你会收到一个包含这些样式值的回调函数。基础的例子看起来是这样的:
<Motion style={{ x: spring(this.state.x) }}>
{
({ x }) =>
<div style={{ transform: `translateX(${x}px)` }} />
}
</Motion>
优点:React Motion 是跨平台的。spring 的概念一开始觉得很奇怪,但在真正使用后会觉得它是个天才的想法,并且将所有的东西都处理得非常好。同时 API 设计的也很棒!
缺点:我注意到在某些情况下它的性能不如纯 CSS/JS 样式动画。尽管 API 很容易上手,但你还是要花时间去学习。
要使用这个库,你可以通过 npm 或者 yarn 安装:
yarn add react-motion
这个例子中,我们将创建一个下拉菜单,按钮按下会触发菜单展开动画。
import React, { Component } from 'react';
import {Motion, spring} from 'react-motion';
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}>Animate</div>
<Motion style={{ height: spring(this.state.height) }}>
{
({ height }) => <div style={Object.assign({}, styles.menu, { height } )}>
<p style={styles.selection}>Selection 1</p>
<p style={styles.selection}>Selection 2</p>
<p style={styles.selection}>Selection 3</p>
<p style={styles.selection}>Selection 4</p>
<p style={styles.selection}>Selection 5</p>
<p style={styles.selection}>Selection 6</p>
</div>
}
</Motion>
</div>
);
}
}
const styles = {
menu: {
overflow: 'hidden',
border: '2px solid #ddd',
width: 300,
marginTop: 20,
},
selection: {
padding: 10,
margin: 0,
borderBottom: '1px solid #ededed'
},
button: {
justifyContent: 'center',
alignItems: 'center',
display: 'flex',
cursor: 'pointer',
width: 200,
height: 45,
border: 'none',
borderRadius: 4,
backgroundColor: '#ffc107',
},
}
我们从 react-motion 中导入了 Motion 和 spring。
将 height 状态初始化为 38. 我们将会用它来动画化菜单的高度。
animate 方法会检查当前高度值,如果是 38 就改为 250,否则将它重置为 38.
在 render 中,我们使用 Motion 组件包裹了一个 p 标签列表。我们设置了 Motion 样式属性,传递了 this.state.height 作为高度值。现在,高度将在 Motion 组件的回调中返回。我们可以在回调中用这个高度来设置包裹着列表的 div 样式。
当按钮点击时,调用了 this.animate 触发高度属性变化。
4. Animated
Animated 库基于在 React Native 中使用的同名动画库。
Animated 的基本思想是你可以创建声明式动画,并传递配置对象来控制在动画中发生的事情。
优点:跨平台。在 React Native 中也非常稳定,所以如果你在 Web 中学习了就不用再学一次了。Animated 允许我们通过 interpolate 方法插入一个单一的值到多个样式中。我们还可以利用多个 Easing 属性的优势,开箱即用。
缺点:根据我通过 Twitter 的交流,看起来这个库在 Web 上还没有达到 100% 稳定,像为老版本浏览器自动添加前缀的问题及一些性能问题。如果你还没有从 React Native 中学过,同样需要花费时间学习。
可以通过 npm 或 yarn 安装:
yarn add animated
在这个例子中,我们将模仿点击订阅后弹出一条消息。
import Animated from 'animated/lib/targets/react-dom';
import Easing from 'animated/lib/Easing';
class App 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}>Animate</div>
<Animated.div
style={
Object.assign(
{},
styles.box,
{ opacity: this.animatedValue, marginLeft })}>
<p>Thanks for your submission!</p>
</Animated.div>
</div>
);
}
}
从 animated 中导入 Animated 和 Easing。注意到我们没有直接导入整个库,但我们实际上直接引入了 react-dom 和 Easing APIs。
创建了一个 animatedValue 类属性,通过调用 *new Animated.Value(0) *设为 0.
创建了一个 animated 方法。这个方法控制动画的发生,我们稍后将使用这个动画值并使用 interpolate 方法创建其他动画值。在这个方法中,我们通过调用 this.animatedValue.setValue(0) 将动画值设为 0,这样每次这个函数被调用时都能触发动画。然后调用了 Animated.timing, 传递动画值作为第一个参数(this.animatedValued),第二个参数是一个配置对象。这个配置对象有个 toValue 属性,将成为最终的动画值。duration 是动画的时长,easing 属性将声明动画的类型(我们选择了 Elastic)。
在我们的 render 方法中,我们首先通过使用 interpolate 方法创建了一个可动画化的值叫 marginLeft。interpolate 接受一个配置对象包含 inputRange 数组和一个 outputRange 数组,将会基于输入和输出创建一个新值。我们用这个值来设置 UI 中消息的 marginLeft 属性。
用 Animated.div 取代常规的 div。
我们用 animatedValue 和 marginLeft 属性为* Animated.div* 添加样式,用 animatedValue 设置 opacity,marginLeft 设置 marginLeft。
5. Velocity React
Velocity React 基于已有的 Velocity DOM 库。
用过之后,我的感觉是它的 API 像 Animated 和 React Motion 的结合体。总体来说,他看起来是一个有趣的库,我会在 web 上做动画的时候想到它,但我想的比较多的是 React Motion 和 Animated。
优点:非常容易上手。API 相当简单明了,比 React Motion 更容易掌握。
缺点:学它的时候有几个瑕疵必须要克服,包括不在 componentDidMount 中运行动画,而是必须声明 runOnMount 属性。同样不是跨平台的。
基础的 API 看起来像这样:
<VelocityComponent
animation={{ opacity: this.state.showSubComponent ? 1 : 0 }}
duration={500}
>
<MySubComponent/>
</VelocityComponent>
可以通过 npm 或 yarn 来安装:
yarn add velocity-react
在这个例子中我们会创建一个很酷的输入动画:
import { VelocityComponent } from 'velocity-react';
const VelocityLetter = ({ letter }) => (
<VelocityComponent
runOnMount
animation={{ opacity: 1, marginTop: 0 }}
duration={500}
>
<p style={styles.letter}>{letter}</p>
</VelocityComponent>
)
class App extends Component {
state = {
letters: [],
}
onChange = (e) => {
const letters = e.target.value.split('');
const arr = []
letters.forEach((l, i) => {
arr.push(<VelocityLetter letter={l} />)
})
this.setState(() => ({ letters: arr }))
}
render() {
return (
<div className="App">
<div className="container">
<input onChange={this.onChange} style={styles.input} />
<div style={styles.letters}>
{
this.state.letters
}
</div>
</div>
</div>
);
}
}
const styles = {
input: {
height: 40,
backgroundColor: '#ddd',
width: 200,
border: 'none',
outline: 'none',
marginBottom: 20,
fontSize: 22,
padding: 8,
},
letters: {
display: 'flex',
height: 140,
},
letter: {
opacity: 0,
marginTop: 100,
fontSize: 22,
whiteSpace: 'pre',
}
}
从 velocity-react 中导入 VelocityComponent。
我们创建了一个可以重用的组件来保存每个要动画化的字符。
在这个组件中,我们设置动画的 opacity 为 1,marginTop 为 0. 子组件会根据我们传入的值重写这些值。这个例子中,<p> 的初始 opacity 为 0, marginTop 为 100. 当组件被创建时,我们将 opacity 从 0 设为 1,将 marginTop 从 100 设为 0. 我们同时设置了时长为 500 毫秒,以及一个 runOnMount 属性,声明我们想让动画在组件被安装或者创建时运行。
在 render 中 input 元素回调了一个 onChange 方法。onChange 将会从 input 中得到每个字符,并使用上面的 VelocityLetter 组件创建了一个新的数组。
在 render 中,我们用这个数组来渲染字符到 UI 中。
总结
总体来说,我会适应 JS 样式动画来做基础动画,React Motion 来做任何 Web 上疯狂的东西。至于 React Native,我坚持使用 Animated。尽管我现在正在开始享受使用 React Motion,一旦 Animated 变得更加成熟,我可能在 web 上也会切换到 Animated!
原文链接:React Animations in Depth
推荐阅读:教你用 Web Speech API 和 Node.js 来创建一个简单的 AI 聊天机器人