標籤:

創建 React 動畫的五種方式

簡評:這篇文章將介紹五種可選方式來創建 React Web 動畫,其中有一些是跨平台的(可以支持 React Native )

1. 基於 React 組件狀態的 CSS 動畫

對於我來說最基礎也是最顯然的來創建動畫就是使用 CSS 類的屬性並通過添加或刪除他們來展現動畫。如果在你的應用中已經使用了 CSS,這是種很好的方式來實現基礎動畫。

缺點:不是跨平台的(不支持 React Native),依賴於 CSS 和 DOM,如果需要實現複雜的效果,這種方式會變得難以控制。

優點:高性能。關於 CSS 動畫,有一條已知的規則:除了透明度和變換意外,不要改變任何屬性,通常會有很棒的性能。基於狀態更新這些值非常簡單,而且只要簡單地重新渲染我們的組件就能達到平滑變換的效果。

看個例子:我們將會基於 React 組件使用 CSS 動畫來動畫化一個 input 組件。

首先我們要創建兩個類關連上我們的 input:

.input {n transition: width .35s linear;n outline: none;n border: none;n border-radius: 4px;n padding: 10px;n font-size: 20px;n width: 150px;n background-color: #dddddd;n}nn.input-focused {n width: 240px;n}n

我們有一些基礎的屬性,並且我們設置了 width .35 linear 的變換,給動畫一些屬性。

同時 input-focused 類將把寬度從 150 px 改動到 240 px。

現在在我們的 React 應用中把他們用起來:

class App extends Component {n state = {n focused: falsen }n componentDidMount() {n this.input.addEventListener(focus, this.focus);n this.input.addEventListener(blur, this.focus);n }n focus = () => {n this.setState((state) => ({ focused: !state.focused }))n }n render() {n return (n <div className="App">n <div className="container">n <inputn ref={input => this.input = input}n className={[input, this.state.focused && input-focused].join( )}n />n </div>n </div>n );n }n}n

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 {n state = {n disabled: true,n }n onChange = (e) => {n const length = e.target.value.length;n if (length >= 4) {n this.setState(() => ({ disabled: false }))n } else if (!this.state.disabled) {n this.setState(() => ({ disabled: true }))n }n }n render() {n const label = this.state.disabled ? Disabled : Submit;n return (n <div className="App">n <buttonn style={Object.assign({}, styles.button, !this.state.disabled && styles.buttonEnabled)}n disabled={this.state.disabled}n >{label}</button>n <inputn style={styles.input}n onChange={this.onChange}n />n </div>n );n }n}nnconst styles = {n input: {n width: 200,n outline: none,n fontSize: 20,n padding: 10,n border: none,n backgroundColor: #ddd,n marginTop: 10,n },n button: {n width: 180,n height: 50,n border: none,n borderRadius: 4,n fontSize: 20,n cursor: pointer,n transition: .25s all,n },n buttonEnabled: {n backgroundColor: #ffc107,n width: 220,n }n}n

1. 初始化一個 disabled 狀態,設為 true

2. onChange 方法綁定了 input,我們會檢查輸入了多少個字元。如果有 4 個或以上,我們將 disabled 設為 false,否則它還沒被設為 true 的話那就設為 true

3. 按鈕元素的樣式屬性將會決定添加動畫類 buttonEnabled 與否,取決於 this.state.disabled 的值。

4. 按鈕的樣式有一個 .25s all 的變換,因為我們想讓 backgroundColorwidth 屬性同時動畫化。

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) }}>n {n ({ x }) =>n <div style={{ transform: `translateX(${x}px)` }} />n }n</Motion>n

優點:React Motion 是跨平台的。spring 的概念一開始覺得很奇怪,但在真正使用後會覺得它是個天才的想法,並且將所有的東西都處理得非常好。同時 API 設計的也很棒!

缺點:我注意到在某些情況下它的性能不如純 CSS/JS 樣式動畫。儘管 API 很容易上手,但你還是要花時間去學習。

要使用這個庫,你可以通過 npm 或者 yarn 安裝:

yarn add react-motionn

這個例子中,我們將創建一個下拉菜單,按鈕按下會觸髮菜單展開動畫。

import React, { Component } from react;nnimport {Motion, spring} from react-motion;nnclass App extends Component {n state = {n height: 38n }n animate = () => {n this.setState((state) => ({ height: state.height === 233 ? 38 : 233 }))n }n render() {n return (n <div className="App">n <div style={styles.button} onClick={this.animate}>Animate</div>n <Motion style={{ height: spring(this.state.height) }}>n {n ({ height }) => <div style={Object.assign({}, styles.menu, { height } )}>n <p style={styles.selection}>Selection 1</p>n <p style={styles.selection}>Selection 2</p>n <p style={styles.selection}>Selection 3</p>n <p style={styles.selection}>Selection 4</p>n <p style={styles.selection}>Selection 5</p>n <p style={styles.selection}>Selection 6</p>n </div>n }n </Motion>n </div>n );n }n}nnconst styles = {n menu: {n overflow: hidden,n border: 2px solid #ddd,n width: 300,n marginTop: 20,n },n selection: {n padding: 10,n margin: 0,n borderBottom: 1px solid #edededn },n button: {n justifyContent: center,n alignItems: center,n display: flex,n cursor: pointer,n width: 200,n height: 45,n border: none,n borderRadius: 4,n backgroundColor: #ffc107,n },n}n

1. 我們從 react-motion 中導入了 Motionspring

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 animatedn

在這個例子中,我們將模仿點擊訂閱後彈出一條消息。

import Animated from animated/lib/targets/react-dom;nimport Easing from animated/lib/Easing;nnclass App extends Component {n animatedValue = new Animated.Value(0)n animate = () => {n this.animatedValue.setValue(0)n Animated.timing(n this.animatedValue,n {n toValue: 1,n duration: 1000,n easing: Easing.elastic(1)n }n ).start();n }n render() {n const marginLeft = this.animatedValue.interpolate({n inputRange: [0, 1],n outputRange: [-120, 0],n })n return (n <div className="App">n <div style={styles.button} onClick={this.animate}>Animate</div>n <Animated.divn style={n Object.assign(n {},n styles.box,n { opacity: this.animatedValue, marginLeft })}>n <p>Thanks for your submission!</p>n </Animated.div>n </div>n );n }n}n

1. 從 animated 中導入 AnimatedEasing。注意到我們沒有直接導入整個庫,但我們實際上直接引入了 react-domEasing 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 方法創建了一個可動畫化的值叫 marginLeftinterpolate 接受一個配置對象包含 inputRange 數組和一個 outputRange 數組,將會基於輸入和輸出創建一個新值。我們用這個值來設置 UI 中消息的 marginLeft 屬性。

5. 用 Animated.div 取代常規的 div。

6. 我們用 animatedValuemarginLeft 屬性為 Animated.div 添加樣式,用 animatedValue 設置 opacitymarginLeft 設置 marginLeft

5. Velocity React

Velocity React 基於已有的 Velocity DOM 庫。

用過之後,我的感覺是它的 API 像 Animated 和 React Motion 的結合體。總體來說,他看起來是一個有趣的庫,我會在 web 上做動畫的時候想到它,但我想的比較多的是 React Motion 和 Animated。

優點:非常容易上手。API 相當簡單明了,比 React Motion 更容易掌握。

缺點:學它的時候有幾個瑕疵必須要克服,包括不在 componentDidMount 中運行動畫,而是必須聲明 runOnMount 屬性。同樣不是跨平台的。

基礎的 API 看起來像這樣:

<VelocityComponentn animation={{ opacity: this.state.showSubComponent ? 1 : 0 }} n duration={500}n>n <MySubComponent/>n</VelocityComponent>n

可以通過 npm 或 yarn 來安裝:

yarn add velocity-reactn

在這個例子中我們會創建一個很酷的輸入動畫:

import { VelocityComponent } from velocity-react;nnconst VelocityLetter = ({ letter }) => (n <VelocityComponentn runOnMountn animation={{ opacity: 1, marginTop: 0 }}n duration={500}n >n <p style={styles.letter}>{letter}</p>n </VelocityComponent>n)nnclass App extends Component {n state = {n letters: [],n }n onChange = (e) => {n const letters = e.target.value.split();n const arr = []n letters.forEach((l, i) => {n arr.push(<VelocityLetter letter={l} />)n })n this.setState(() => ({ letters: arr }))n }nn render() {n return (n <div className="App">n <div className="container">n <input onChange={this.onChange} style={styles.input} />n <div style={styles.letters}>n {n this.state.lettersn }n </div>n </div>n </div>n );n }n}nnconst styles = {n input: {n height: 40,n backgroundColor: #ddd,n width: 200,n border: none,n outline: none,n marginBottom: 20,n fontSize: 22,n padding: 8,n },n letters: {n display: flex,n height: 140,n },n letter: {n opacity: 0,n marginTop: 100,n fontSize: 22,n whiteSpace: pre,n }n}n

1. 從 velocity-react 中導入 VelocityComponent

2. 我們創建了一個可以重用的組件來保存每個要動畫化的字元。

3. 在這個組件中,我們設置動畫的 opacity 為 1,marginTop 為 0. 子組件會根據我們傳入的值重寫這些值。這個例子中,<p> 的初始 opacity 為 0, marginTop 為 100. 當組件被創建時,我們將 opacity 從 0 設為 1,將 marginTop 從 100 設為 0. 我們痛死後設置了時長為 500 毫秒,以及一個 runOnMount 屬性,聲明我們想讓動畫在組件被安裝或者創建時運行。

4. 在 renderinput 元素回調了一個 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 聊天機器人

歡迎關註:知乎專欄「極光日報」,每天為 Makers 導讀三篇優質英文文章。

推薦閱讀:

web 開發中無處不在的狀態機

TAG:React | ReactNative |