創建 React 動畫的五種方式

簡評:這篇文章將介紹五種可選方式來創建 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 的變換,因為我們想讓 backgroundColorwidth 屬性同時動畫化。

3. React Motion

React MotionCheng 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 中導入了 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 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 中導入 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 看起來像這樣:

<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. 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 聊天機器人

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,316評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,481評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,241評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,939評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,697評論 6 409
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,182評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,247評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,406評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,933評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,772評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,973評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,516評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,638評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,866評論 1 285
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,644評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,953評論 2 373

推薦閱讀更多精彩內容

  • 以前一直投入在 React Native 中,寫動畫的時候不是用 CSS 中的 transitions / ani...
    楓上霧棋閱讀 948評論 0 8
  • 發現 關注 消息 iOS 第三方庫、插件、知名博客總結 作者大灰狼的小綿羊哥哥關注 2017.06.26 09:4...
    肇東周閱讀 12,151評論 4 61
  • 記憶中第一次上網是在初中。當時,只聽聞某年級的學生半夜翻墻跑到網吧通宵或者某某同學沉迷于網吧荒廢學業,卻未曾到網吧...
    萬卷無書閱讀 283評論 0 0
  • 楊柳岸 曉風月 自古情多是傷離別 灞橋邊 難眠夜 望盡紅塵悲歌 知是故人遠踏雪 深夜煎茶共邀月 遙憶當年事 天寒心...
    書云公子閱讀 536評論 0 1