简评:这篇文章将介绍五种可选方式来创建 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>
);
}
}
1.我们创建了一个 focused 状态并设为 false。我们将用这个状态出发更新我们动画化的组件。
2.在 componentDidMount 中,我们添加了两个监听器,一个监听 blur,一个监听 focus。两个监听器都能够调用 focus 方法。注意到我们正在引用 this.input,这是因为我们使用 ref 方法创建了一个引用,然后把它设置为一个类属性。我们在 componentDidMount 中做这些因为在 componentWillMount 时我们还没有进入 dom。
3.focus 方法会检查上个 focused 状态的值,并基于他的值来触发。
4.在 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,
}
}
1.初始化一个 disabled 状态,设为 true。
2.onChange 方法绑定了 input,我们会检查输入了多少个字符。如果有 4 个或以上,我们将 disabled 设为 false,否则它还没被设为 true 的话那就设为 true。
3.按钮元素的样式属性将会决定添加动画类 buttonEnabled 与否,取决于 this.state.disabled的值。
4.按钮的样式有一个 .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',
},
}
1.我们从 react-motion 中导入了 Motion 和 spring。
2.将 height 状态初始化为 38. 我们将会用它来动画化菜单的高度。
3.animate 方法会检查当前高度值,如果是 38 就改为 250,否则将它重置为 38.
4.在 render 中,我们使用 Motion 组件包裹了一个 p 标签列表。我们设置了 Motion 样式属性,传递了 this.state.height 作为高度值。现在,高度将在 Motion 组件的回调中返回。我们可以在回调中用这个高度来设置包裹着列表的 div 样式。
5.当按钮点击时,调用了 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>
);
}
}
1.从 animated 中导入 Animated 和 Easing。注意到我们没有直接导入整个库,但我们实际上直接引入了 react-dom 和 Easing APIs。
2.创建了一个 animatedValue 类属性,通过调用 new Animated.Value(0) 设为 0.
3.创建了一个 animated 方法。这个方法控制动画的发生,我们稍后将使用这个动画值并使用 interpolate 方法创建其他动画值。在这个方法中,我们通过调用 this.animatedValue.setValue(0) 将动画值设为 0,这样每次这个函数被调用时都能触发动画。然后调用了 Animated.timing, 传递动画值作为第一个参数(this.animatedValued),第二个参数是一个配置对象。这个配置对象有个 toValue 属性,将成为最终的动画值。duration 是动画的时长,easing 属性将声明动画的类型(我们选择了 Elastic)。
4.在我们的 render 方法中,我们首先通过使用 interpolate 方法创建了一个可动画化的值叫 marginLeft。interpolate 接受一个配置对象包含 inputRange 数组和一个 outputRange 数组,将会基于输入和输出创建一个新值。我们用这个值来设置 UI 中消息的 marginLeft 属性。
5.用 Animated.div 取代常规的 div。
6.我们用 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',
}
}
1.从 velocity-react 中导入 VelocityComponent。
2.我们创建了一个可以重用的组件来保存每个要动画化的字符。
3.在这个组件中,我们设置动画的 opacity 为 1,marginTop 为 0. 子组件会根据我们传入的值重写这些值。这个例子中,<p> 的初始 opacity 为 0, marginTop 为 100. 当组件被创建时,我们将 opacity 从 0 设为 1,将 marginTop 从 100 设为 0. 我们痛死后设置了时长为 500 毫秒,以及一个 runOnMount 属性,声明我们想让动画在组件被安装或者创建时运行。
4.在 render 中 input 元素回调了一个 onChange 方法。onChange 将会从 input 中得到每个字符,并使用上面的 VelocityLetter 组件创建了一个新的数组。
5.在 render 中,我们用这个数组来渲染字符到 UI 中。
总结
总体来说,我会适应 JS 样式动画来做基础动画,React Motion 来做任何 Web 上疯狂的东西。至于 React Native,我坚持使用 Animated。尽管我现在正在开始享受使用 React Motion,一旦 Animated 变得更加成熟,我可能在 web 上也会切换到 Animated!
原文链接:React Animations in Depth
推荐阅读:教你用 Web Speech API 和 Node.js 来创建一个简单的 AI 聊天机器人