簡評:這篇文章將介紹五種可選方式來創(chuàng)建 React Web 動畫,其中有一些是跨平臺的(可以支持 React Native )
1. 基于 React 組件狀態(tài)的 CSS 動畫
對于我來說最基礎(chǔ)也是最顯然的來創(chuàng)建動畫就是使用 CSS 類的屬性并通過添加或刪除他們來展現(xiàn)動畫眶俩。如果在你的應(yīng)用中已經(jīng)使用了 CSS测蹲,這是種很好的方式來實現(xiàn)基礎(chǔ)動畫赃额。
缺點:不是跨平臺的(不支持 React Native)通危,依賴于 CSS 和 DOM沈撞,如果需要實現(xiàn)復雜的效果励幼,這種方式會變得難以控制汰寓。
優(yōu)點:高性能。關(guān)于 CSS 動畫苹粟,有一條已知的規(guī)則:除了透明度和變換意外有滑,不要改變?nèi)魏螌傩裕ǔ泻馨舻男阅芮断鳌毛好;跔顟B(tài)更新這些值非常簡單,而且只要簡單地重新渲染我們的組件就能達到平滑變換的效果苛秕。
看個例子:我們將會基于 React 組件使用 CSS 動畫來動畫化一個 input 組件肌访。
首先我們要創(chuàng)建兩個類關(guān)連上我們的 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;
}
我們有一些基礎(chǔ)的屬性,并且我們設(shè)置了 width .35 linear 的變換艇劫,給動畫一些屬性吼驶。
同時 input-focused 類將把寬度從 150 px 改動到 240 px。
現(xiàn)在在我們的 React 應(yīng)用中把他們用起來:
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>
);
}
}
我們創(chuàng)建了一個 focused 狀態(tài)并設(shè)為 false。我們將用這個狀態(tài)出發(fā)更新我們動畫化的組件蟹演。
在 componentDidMount 中风钻,我們添加了兩個監(jiān)聽器,一個監(jiān)聽 blur酒请,一個監(jiān)聽 focus骡技。兩個監(jiān)聽器都能夠調(diào)用 focus 方法。注意到我們正在引用 this.input羞反,這是因為我們使用 ref 方法創(chuàng)建了一個引用布朦,然后把它設(shè)置為一個類屬性。我們在 componentDidMount 中做這些因為在 componentWillMount 時我們還沒有進入 dom苟弛。
focus 方法會檢查上個 focused 狀態(tài)的值喝滞,并基于他的值來觸發(fā)阁将。
在 render 中膏秫,主要注意的是我們給 input 設(shè)置了 classNames。我們檢查 this.state.focused 是否為 true做盅,如果是缤削,我們會加入 input-focused 類。我們創(chuàng)建了一個數(shù)組吹榴,并調(diào)用 .join('') 作為一個可用的 className亭敢。
2. 基于 React 組件狀態(tài)的 JS 樣式動畫
用 JS 樣式來創(chuàng)建動畫的方式和用 CSS 類有點相似。好處是你可以獲得相同的性能图筹,但你不用依賴 CSS 類帅刀,你可以在 JS 文件中寫上所有的邏輯。
優(yōu)點:像 CSS 動畫远剩,好處是性能杠杠的扣溺。同樣也是種很好的方式,因為你不需要依賴于任何 CSS 文件瓜晤。
缺點:同樣和 CSS 動畫一樣锥余,不是跨平臺的(不支持 React Native),依賴于 CSS 和 DOM痢掠,如果要創(chuàng)造復雜的動畫驱犹,會變得難以控制。
這個例子中足画,我們會創(chuàng)建一個輸入框雄驹,當用戶輸入時,會變成可點擊和不可點擊的狀態(tài)淹辞,給予用戶反饋荠医。
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 狀態(tài),設(shè)為 true。
onChange 方法綁定了 input彬向,我們會檢查輸入了多少個字符兼贡。如果有 4 個或以上,我們將 disabled 設(shè)為 false娃胆,否則它還沒被設(shè)為 true 的話那就設(shè)為 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 上發(fā)表了一個很棒的關(guān)于討論動畫的演講漂洋。
React Motion 背后的思想是它將 API 引用的內(nèi)容作為 “Spring”,這是一個非常穩(wěn)定的基礎(chǔ)動畫配置力喷,在大多數(shù)情況下工作良好刽漂,同時也是可配置的。它不依賴于時間弟孟,所以當你想要取消/停止/撤銷一個動畫或者在你的應(yīng)用中使用可變維度的時候會更好用贝咙。
React Motion 的用法是你在一個 React Motion 組件中設(shè)置一個樣式配置,然后你會收到一個包含這些樣式值的回調(diào)函數(shù)拂募⊥バ桑基礎(chǔ)的例子看起來是這樣的:
<Motion style={{ x: spring(this.state.x) }}>
{
({ x }) =>
<div style={{ transform: `translateX(${x}px)` }} />
}
</Motion>
優(yōu)點:React Motion 是跨平臺的。spring 的概念一開始覺得很奇怪陈症,但在真正使用后會覺得它是個天才的想法蔼水,并且將所有的東西都處理得非常好。同時 API 設(shè)計的也很棒爬凑!
缺點:我注意到在某些情況下它的性能不如純 CSS/JS 樣式動畫徙缴。盡管 API 很容易上手,但你還是要花時間去學習嘁信。
要使用這個庫于样,你可以通過 npm 或者 yarn 安裝:
yarn add react-motion
這個例子中,我們將創(chuàng)建一個下拉菜單潘靖,按鈕按下會觸發(fā)菜單展開動畫穿剖。
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 狀態(tài)初始化為 38. 我們將會用它來動畫化菜單的高度卦溢。
animate 方法會檢查當前高度值糊余,如果是 38 就改為 250秀又,否則將它重置為 38.
在 render 中,我們使用 Motion 組件包裹了一個 p 標簽列表贬芥。我們設(shè)置了 Motion 樣式屬性吐辙,傳遞了 this.state.height 作為高度值。現(xiàn)在蘸劈,高度將在 Motion 組件的回調(diào)中返回昏苏。我們可以在回調(diào)中用這個高度來設(shè)置包裹著列表的 div 樣式。
當按鈕點擊時威沫,調(diào)用了 this.animate 觸發(fā)高度屬性變化贤惯。
4. Animated
Animated 庫基于在 React Native 中使用的同名動畫庫。
Animated 的基本思想是你可以創(chuàng)建聲明式動畫棒掠,并傳遞配置對象來控制在動畫中發(fā)生的事情孵构。
優(yōu)點:跨平臺。在 React Native 中也非常穩(wěn)定烟很,所以如果你在 Web 中學習了就不用再學一次了颈墅。Animated 允許我們通過 interpolate 方法插入一個單一的值到多個樣式中。我們還可以利用多個 Easing 屬性的優(yōu)勢溯职,開箱即用精盅。
缺點:根據(jù)我通過 Twitter 的交流帽哑,看起來這個庫在 Web 上還沒有達到 100% 穩(wěn)定谜酒,像為老版本瀏覽器自動添加前綴的問題及一些性能問題。如果你還沒有從 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愕掏。
創(chuàng)建了一個 animatedValue 類屬性度秘,通過調(diào)用 *new Animated.Value(0) *設(shè)為 0.
創(chuàng)建了一個 animated 方法。這個方法控制動畫的發(fā)生饵撑,我們稍后將使用這個動畫值并使用 interpolate 方法創(chuàng)建其他動畫值剑梳。在這個方法中,我們通過調(diào)用 this.animatedValue.setValue(0) 將動畫值設(shè)為 0滑潘,這樣每次這個函數(shù)被調(diào)用時都能觸發(fā)動畫垢乙。然后調(diào)用了 Animated.timing, 傳遞動畫值作為第一個參數(shù)(this.animatedValued),第二個參數(shù)是一個配置對象语卤。這個配置對象有個 toValue 屬性追逮,將成為最終的動畫值酪刀。duration 是動畫的時長,easing 屬性將聲明動畫的類型(我們選擇了 Elastic)钮孵。
在我們的 render 方法中骂倘,我們首先通過使用 interpolate 方法創(chuàng)建了一個可動畫化的值叫 marginLeft。interpolate 接受一個配置對象包含 inputRange 數(shù)組和一個 outputRange 數(shù)組巴席,將會基于輸入和輸出創(chuàng)建一個新值稠茂。我們用這個值來設(shè)置 UI 中消息的 marginLeft 屬性。
用 Animated.div 取代常規(guī)的 div情妖。
我們用 animatedValue 和 marginLeft 屬性為* Animated.div* 添加樣式睬关,用 animatedValue 設(shè)置 opacity,marginLeft 設(shè)置 marginLeft毡证。
5. Velocity React
Velocity React 基于已有的 Velocity DOM 庫电爹。
用過之后,我的感覺是它的 API 像 Animated 和 React Motion 的結(jié)合體料睛∝ぢ幔總體來說,他看起來是一個有趣的庫恤煞,我會在 web 上做動畫的時候想到它屎勘,但我想的比較多的是 React Motion 和 Animated。
優(yōu)點:非常容易上手居扒。API 相當簡單明了概漱,比 React Motion 更容易掌握。
缺點:學它的時候有幾個瑕疵必須要克服喜喂,包括不在 componentDidMount 中運行動畫瓤摧,而是必須聲明 runOnMount 屬性。同樣不是跨平臺的玉吁。
基礎(chǔ)的 API 看起來像這樣:
<VelocityComponent
animation={{ opacity: this.state.showSubComponent ? 1 : 0 }}
duration={500}
>
<MySubComponent/>
</VelocityComponent>
可以通過 npm 或 yarn 來安裝:
yarn add velocity-react
在這個例子中我們會創(chuàng)建一個很酷的輸入動畫:
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照弥。
我們創(chuàng)建了一個可以重用的組件來保存每個要動畫化的字符。
在這個組件中进副,我們設(shè)置動畫的 opacity 為 1这揣,marginTop 為 0. 子組件會根據(jù)我們傳入的值重寫這些值。這個例子中影斑,<p> 的初始 opacity 為 0给赞, marginTop 為 100. 當組件被創(chuàng)建時,我們將 opacity 從 0 設(shè)為 1鸥昏,將 marginTop 從 100 設(shè)為 0. 我們同時設(shè)置了時長為 500 毫秒塞俱,以及一個 runOnMount 屬性,聲明我們想讓動畫在組件被安裝或者創(chuàng)建時運行吏垮。
在 render 中 input 元素回調(diào)了一個 onChange 方法障涯。onChange 將會從 input 中得到每個字符罐旗,并使用上面的 VelocityLetter 組件創(chuàng)建了一個新的數(shù)組。
在 render 中唯蝶,我們用這個數(shù)組來渲染字符到 UI 中九秀。
總結(jié)
總體來說,我會適應(yīng) JS 樣式動畫來做基礎(chǔ)動畫粘我,React Motion 來做任何 Web 上瘋狂的東西鼓蜒。至于 React Native,我堅持使用 Animated征字。盡管我現(xiàn)在正在開始享受使用 React Motion都弹,一旦 Animated 變得更加成熟,我可能在 web 上也會切換到 Animated匙姜!
原文鏈接:React Animations in Depth
推薦閱讀:教你用 Web Speech API 和 Node.js 來創(chuàng)建一個簡單的 AI 聊天機器人