創(chuàng)建 React 動畫的五種方式

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

1. 基于 React 組件狀態(tài)的 CSS 動畫

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

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

優(yōu)點:高性能。關于 CSS 動畫,有一條已知的規(guī)則:除了透明度和變換意外,不要改變任何屬性,通常會有很棒的性能?;跔顟B(tài)更新這些值非常簡單,而且只要簡單地重新渲染我們的組件就能達到平滑變換的效果。

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

首先我們要創(chuàng)建兩個類關連上我們的 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。

現(xiàn)在在我們的 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. 我們創(chuàng)建了一個 focused 狀態(tài)并設為 false。我們將用這個狀態(tài)出發(fā)更新我們動畫化的組件。

  2. componentDidMount 中,我們添加了兩個監(jiān)聽器,一個監(jiān)聽 blur,一個監(jiān)聽 focus。兩個監(jiān)聽器都能夠調用 focus 方法。注意到我們正在引用 this.input,這是因為我們使用 ref 方法創(chuàng)建了一個引用,然后把它設置為一個類屬性。我們在 componentDidMount 中做這些因為在 componentWillMount 時我們還沒有進入 dom。

  3. focus 方法會檢查上個 focused 狀態(tài)的值,并基于他的值來觸發(fā)。

  4. 在 render 中,主要注意的是我們給 input 設置了 classNames。我們檢查 this.state.focused 是否為 true,如果是,我們會加入 input-focused 類。我們創(chuàng)建了一個數(shù)組,并調用 .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,
  }
}
  1. 初始化一個 disabled 狀態(tài),設為 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 上發(fā)表了一個很棒的關于討論動畫的演講

React Motion 背后的思想是它將 API 引用的內容作為 “Spring”,這是一個非常穩(wěn)定的基礎動畫配置,在大多數(shù)情況下工作良好,同時也是可配置的。它不依賴于時間,所以當你想要取消/停止/撤銷一個動畫或者在你的應用中使用可變維度的時候會更好用。

React Motion 的用法是你在一個 React Motion 組件中設置一個樣式配置,然后你會收到一個包含這些樣式值的回調函數(shù)?;A的例子看起來是這樣的:

<Motion style={{ x: spring(this.state.x) }}>
  {
    ({ x }) =>
      <div style={{ transform: `translateX(${x}px)` }} />
  }
</Motion>

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

缺點:我注意到在某些情況下它的性能不如純 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',
  },
}
  1. 我們從 react-motion 中導入了 Motionspring。

  2. height 狀態(tài)初始化為 38. 我們將會用它來動畫化菜單的高度。

  3. animate 方法會檢查當前高度值,如果是 38 就改為 250,否則將它重置為 38.

  4. render 中,我們使用 Motion 組件包裹了一個 p 標簽列表。我們設置了 Motion 樣式屬性,傳遞了 this.state.height 作為高度值?,F(xiàn)在,高度將在 Motion 組件的回調中返回。我們可以在回調中用這個高度來設置包裹著列表的 div 樣式。

  5. 當按鈕點擊時,調用了 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>
    );
  }
}
  1. animated 中導入 AnimatedEasing。注意到我們沒有直接導入整個庫,但我們實際上直接引入了 react-domEasing APIs。

  2. 創(chuàng)建了一個 animatedValue 類屬性,通過調用 *new Animated.Value(0) *設為 0.

  3. 創(chuàng)建了一個 animated 方法。這個方法控制動畫的發(fā)生,我們稍后將使用這個動畫值并使用 interpolate 方法創(chuàng)建其他動畫值。在這個方法中,我們通過調用 this.animatedValue.setValue(0) 將動畫值設為 0,這樣每次這個函數(shù)被調用時都能觸發(fā)動畫。然后調用了 Animated.timing, 傳遞動畫值作為第一個參數(shù)(this.animatedValued),第二個參數(shù)是一個配置對象。這個配置對象有個 toValue 屬性,將成為最終的動畫值。duration 是動畫的時長,easing 屬性將聲明動畫的類型(我們選擇了 Elastic)。

  4. 在我們的 render 方法中,我們首先通過使用 interpolate 方法創(chuàng)建了一個可動畫化的值叫 marginLeft。interpolate 接受一個配置對象包含 inputRange 數(shù)組和一個 outputRange 數(shù)組,將會基于輸入和輸出創(chuàng)建一個新值。我們用這個值來設置 UI 中消息的 marginLeft 屬性。

  5. Animated.div 取代常規(guī)的 div。

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

5. Velocity React

Velocity React 基于已有的 Velocity DOM 庫。

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

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

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

基礎的 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',
  }
}
  1. velocity-react 中導入 VelocityComponent。

  2. 我們創(chuàng)建了一個可以重用的組件來保存每個要動畫化的字符。

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

  4. renderinput 元素回調了一個 onChange 方法。onChange 將會從 input 中得到每個字符,并使用上面的 VelocityLetter 組件創(chuàng)建了一個新的數(shù)組。

  5. render 中,我們用這個數(shù)組來渲染字符到 UI 中。

總結

總體來說,我會適應 JS 樣式動畫來做基礎動畫,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 聊天機器人

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

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

友情鏈接更多精彩內容