React Web 動畫的 5 種創(chuàng)建方式深浮,每一種都不簡單

以前一直投入在 React Native 中剩辟,寫動畫的時候不是用 CSS 中的 transitions / animations元扔,就是依賴像 GreenSock 這樣的庫躯保,最近轉(zhuǎn)向 Web,在 Tweet 得到很多大佬關(guān)于 React Web 動畫 的回應(yīng)澎语,于是決定分享給大家途事,如有其他見解验懊,非常歡迎在下面評論中交流

以下便是本文要分享的創(chuàng)建 React 動畫 的幾種方式

下面,勒次個特斯大特一特

CSS animation

給元素添加 class 是最簡單尸变,最常見的書寫方式义图。如果你的 app 正在使用 CSS,那么這將是你最愉快的選擇

贊同者: 我們只需修改 opacitytransform 這樣的屬性召烂,就可構(gòu)建基本的動畫碱工,而且,在組件中奏夫,我們可以非常容易地通過 state 去更新這些值

反對者:這種方式并不跨平臺怕篷,在 React Native 中就不適用,而且酗昼,對于較復(fù)雜的動畫匙头,這種方式難以控制

接下來,我們通過一個實例來體驗一下這種創(chuàng)建方式:當 input focus 的時候仔雷,我們增加它的寬度

首先,我們要創(chuàng)建兩個 input 要用到的 class

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

.input-focused {
  width: 240px;
}

一個是它原始的樣式舔示,一個是它 focus 后的樣式

下面碟婆,我們就開始書寫我們的 React 組件

在此,推薦一個 在線的 React VS Code IDE惕稻,真的很強大竖共,讀者不想構(gòu)建自己的 React app,可以在其中檢驗以下代碼的正確性

class App extends Component {
  state = {
    focused: false,
  }

  componentDidMount() {
    this._input.addEventListener('focus', this.focus);
    this._input.addEventListener('blur', this.focus);
  }

  focus = () => {
    this.setState(prevState => ({
      focused: !prevState.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>
    );
  }
}
  • 我們有一個 focusedstate俺祠,初始值為 false公给,我們通過更新該值來創(chuàng)建我們的動畫
  • componentDidMount 中,我們添加兩個監(jiān)聽器蜘渣,一個 focus淌铐,一個 blur,指定的回調(diào)函數(shù)都focus
  • focus 方法會獲取之前 focused 的值蔫缸,并負責(zé)切換該值
  • render 中腿准,我們通過 state 來改變 inputclassNames,從而實現(xiàn)我們的動畫

JS Style

JavaScipt stylesCSS 中的 classes 類似拾碌,在 JS 文件中吐葱,我們就可以擁有所有邏輯

贊同者:跟 CSS 動畫 一樣,且它的表現(xiàn)更加清晰校翔。它也不失為一個好方法弟跑,可以不必依賴任何 CSS

反對者:跟 CSS 動畫 一樣,也是不跨平臺的防症,且動畫一旦復(fù)雜孟辑,也難以控制

在下面的實例中哎甲,我們將創(chuàng)建一個 input,當用戶輸入時扑浸,我們將一個 buttondisable 轉(zhuǎn)變?yōu)?enable

class App extends Component {
  state = {
    disabled: true,
  }

  onChange = (e) => {
    const length = e.target.value.length;

    if (length > 0) {
      this.setState({ disabled: false });
    } else {
      this.setState({ disabled: true });
    }
  }
  render() {
    const { disabled } = this.state;
    const label = disabled ? 'Disabled' : 'Submit';

    return (
      <div style={styles.App}>
        <input
          style={styles.input}
          onChange={this.onChange}
        />
        <button
          style={Object.assign({},
            styles.button,
            !this.state.disabled && styles.buttonEnabled
          )}
          disabled={disabled}
        >
          {label}
        </button>
      </div>
    );
  }
}


const styles = {
  App: {
    display: 'flex',
    justifyContent: 'left',
  },
  input: {
    marginRight: 10,
    padding: 10,
    width: 190,
    fontSize: 20,
    border: 'none',
    backgroundColor: '#ddd',
    outline: 'none',
  },
  button: {
    width: 90,
    height: 43,
    fontSize: 17,
    border: 'none',
    borderRadius: 4,
    transition: '.25s all',
    cursor: 'pointer',
  },
  buttonEnabled: {
    width: 120,
    backgroundColor: '#ffc107',
  }
}
  • 我們有一個 disabledstate烧给,初始值為 true
  • onChange 方法會獲取用戶的輸入,當輸入非空時喝噪,就切換 disabled 的值
  • 根據(jù) disabled 的值础嫡,確定是否將 buttonEnabled 添加到 button

React Motion

React MotionCheng Lou 書寫的一個非常不錯的開源項目。它的思想是你可以對Motion 組件 進行簡單的樣式設(shè)置酝惧,然后你就可以在回調(diào)函數(shù)中通過這些值榴鼎,享受動畫帶來的樂趣

對于絕大多數(shù)的動畫組件,我們往往不希望對動畫屬性(寬高晚唇、顏色等)的變化時間做硬編碼處理巫财,react-motion 提供的 spring 函數(shù)就是用來解決這一需求的,它可以逼真地模仿真實的物理效果哩陕,也就是我們常見的各類緩動效果

下面是一個森破的示例

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

這是官方提供的幾個 demo平项,真的可以是不看不知道,一看嚇一跳

贊同者React Motion 可以在 React Web 中使用悍及,也可以在 React Native 中使用闽瓢,因為它是跨平臺的。其中的 spring 概念最開始對我來說感覺挺陌生心赶,然而上手之后扣讼,發(fā)現(xiàn)它真的很神奇,并且缨叫,它有很詳細的 API

反對者:在某些情況下椭符,他不如純 CSS / JS 動畫,雖然它有不錯的 API耻姥,容易上手销钝,但也需要學(xué)習(xí)成本

為了使用它,首先我們要用 yarnnpm 安裝它

yarn add react-motion

在下面的實例中琐簇,我們將創(chuàng)建一個 dropdown 菜單曙搬,當點擊按鈕時,下拉菜單友好展開

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: {
    marginTop: 20,
    width: 300,
    border: '2px solid #ddd',
    overflow: 'hidden',
  },
  button: {
    display: 'flex',
    width: 200,
    height: 45,
    justifyContent: 'center',
    alignItems: 'center',
    border: 'none',
    borderRadius: 4,
    backgroundColor: '#ffc107',
    cursor: 'pointer',
  },
  selection: {
    margin: 0,
    padding: 10,
    borderBottom: '1px solid #ededed',
  },
}
  • 我們從 react-motion 中 import Motionspring
  • 我們有一個 heightstate鸽嫂,初始值為 38纵装,代表 menu 的高度
  • animate 方法設(shè)置 menuheight,如果 原 height38据某,則設(shè)置 新 height233橡娄,如果 原 height233,則設(shè)置 新 height38
  • render 中癣籽,我們使用 Motion 組件 包裝整個 p 標簽 列表挽唉,將 this.state.height 的當前值設(shè)為組件的 height滤祖,然后在組件的回調(diào)函數(shù)中使用該值作為整個下拉的高度
  • 當按鈕被點擊時,我們通過 this.animate 切換下拉的高度

Animated

Animated 是基于 React Native 使用的同一個動畫庫建立起來的

它背后的思想是創(chuàng)建聲明式動畫瓶籽,通過傳遞配置對象來控制動畫

贊同者跨平臺匠童,它在 React Native 中已經(jīng)非常穩(wěn)定,如果你在 React Native 中使用過塑顺,那么你將不用再重復(fù)學(xué)習(xí)汤求。其中的 interpolate 是一個神奇的插值函數(shù),我們將在下面看到

反對者:基于 Twitter 的交流严拒,它目前貌似不是 100% 的穩(wěn)定扬绪,在老的瀏覽器中的,存在前綴性能的問題裤唠,而且挤牛,它也有學(xué)習(xí)成本

為了使用 Animated,我們首先還是要用 yarnnpm 安裝它

yarn add animated

在下面的實例中种蘸,我們將模擬在提交表單成功后顯示的動畫 message

import Animated from 'animated/lib/targets/react-dom';
import Easing from 'animated/lib/Easing';

class AnimatedApp 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>- 我們將 `animatedValue`
 和 `marginLeft`  作為 `Animated.div ` 的 `style` 屬性墓赴,  );
  }
}

const styles = {
  button: {
    display: 'flex',
    width: 125,
    height: 50,
    justifyContent: 'center',
    alignItems: 'center',
    border: 'none',
    borderRadius: 4,
    backgroundColor: '#ffc107',
    cursor: 'pointer',
  },
  box: {
    display: 'inline-block',
    marginTop: 10,
    padding: '0.6rem 2rem',
    fontSize:'0.8rem',
    border: '1px #eee solid',
    borderRadius: 4,
    boxShadow: '0 2px 8px rgba(0,0,0,.2)',
  },
}
  • animated 中 import AnimatedEasing
  • new Animated.Value(0) 創(chuàng)建一個值為 0 的類屬性 - animatedValue
  • 創(chuàng)建 animate 方法,處理所有的動畫航瞭,首先通過 this.animatedValue.setValue(0) 初始化動畫值诫硕,實現(xiàn)的效果就是每次重新執(zhí)行該動畫,然后調(diào)用 Animated.timing沧奴,animatedValue 作為第一個參數(shù)傳遞,配置對象 作為第二個參數(shù)长窄,一個設(shè)置最終動畫值滔吠,一個設(shè)置持續(xù)時間,一個設(shè)置緩動效果
  • render 中挠日,我們用 interpolate 方法創(chuàng)建 marginLeft 對象疮绷,包含 inputRangeoutputRange 數(shù)組,我們使用此對象作為 UImessagestyle 屬性
  • 我們使用 Animated.div 替代默認的 div
  • 我們將 animatedValuemarginLeft 作為 Animated.divstyle 屬性

Velocity React

Velocity React 是基于已經(jīng)存在的 Velocity 建立起來的

贊同者:上手容易嚣潜,API 簡單明了冬骚,相對其他庫更易于掌握

反對者:有些不得不克服的問題,比如 componentDidMount 后動畫并沒有真正地起作用等懂算,而且只冻,它不跨平臺

下面是一個森破的示例

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

首先還是要用 yarnnpm 安裝它

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 VelocityApp 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: {
    marginBottom: 20,
    padding: 8,
    width: 200,
    height: 40,
    fontSize: 22,
    backgroundColor: '#ddd',
    border: 'none',
    outline: 'none',
  },
  letters: {
    display: 'flex',
    height: 140,
  },
  letter: {
    marginTop: 100,
    fontSize: 22,
    whiteSpace: 'pre',
    opacity: 0,
  }
}
  • velocity-react 中 import VelocityComponent
  • 我們要創(chuàng)建一個可重復(fù)使用的組件來滿足每個 letter 的動畫
  • 在這個組件中计技,我們將 animationopacity 設(shè)為 1喜德,marginTop 設(shè)為 0,這些值代表著傳入子組件的重寫值垮媒,即當組件被創(chuàng)建時舍悯,組件的 opacity 會由初始的 0 變?yōu)?1航棱,marginTop 會由初始的 100 變?yōu)?0,我們還設(shè)置了 500 ms 的持續(xù)時間萌衬,最后值得一提的是 runOnMount 屬性饮醇,它的意思是在組件 掛載創(chuàng)建 完后執(zhí)行該動畫
  • 其中的 onChange 方法會獲取用戶的每次輸入,并創(chuàng)建一個由 VelocityLetter 組成的新數(shù)組
  • render 中秕豫,我們就使用該數(shù)組在 UI 中渲染 letters

總結(jié)

總的來說朴艰,基本的動畫,我會選擇 JS style馁蒂,復(fù)雜的動畫呵晚,我更偏向 React Motion。而對于 React Native沫屡,我還是堅持使用 Animated饵隙,一旦 Animated 成熟,在 Web 中可能也會投入使用沮脖,目前金矛,我真的很享受 React Motion

原文鏈接: React Animations in Depth

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市勺届,隨后出現(xiàn)的幾起案子驶俊,更是在濱河造成了極大的恐慌,老刑警劉巖免姿,帶你破解...
    沈念sama閱讀 216,843評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件饼酿,死亡現(xiàn)場離奇詭異,居然都是意外死亡胚膊,警方通過查閱死者的電腦和手機故俐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,538評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來紊婉,“玉大人,你說我怎么就攤上這事槽片≈。” “怎么了传轰?”我有些...
    開封第一講書人閱讀 163,187評論 0 353
  • 文/不壞的土叔 我叫張陵路召,是天一觀的道長。 經(jīng)常有香客問我身隐,道長贾铝,這世上最難降的妖魔是什么垢揩? 我笑而不...
    開封第一講書人閱讀 58,264評論 1 292
  • 正文 為了忘掉前任叁巨,我火速辦了婚禮,結(jié)果婚禮上蚀瘸,老公的妹妹穿的比我還像新娘庶橱。我一直安慰自己,他們只是感情好寂嘉,可當我...
    茶點故事閱讀 67,289評論 6 390
  • 文/花漫 我一把揭開白布枫绅。 她就那樣靜靜地躺著撑瞧,像睡著了一般显蝌。 火紅的嫁衣襯著肌膚如雪曼尊。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,231評論 1 299
  • 那天,我揣著相機與錄音肴裙,去河邊找鬼蜻懦。 笑死,一個胖子當著我的面吹牛悠咱,可吹牛的內(nèi)容都是我干的析既。 我是一名探鬼主播谆奥,決...
    沈念sama閱讀 40,116評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼雄右!你這毒婦竟也來了空骚?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,945評論 0 275
  • 序言:老撾萬榮一對情侶失蹤囤屹,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后逢渔,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,367評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡肃廓,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,581評論 2 333
  • 正文 我和宋清朗相戀三年智厌,在試婚紗的時候發(fā)現(xiàn)自己被綠了盲赊。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片铣鹏。...
    茶點故事閱讀 39,754評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖哀蘑,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情绘迁,我是刑警寧澤合溺,帶...
    沈念sama閱讀 35,458評論 5 344
  • 正文 年R本政府宣布棠赛,位于F島的核電站鼎俘,受9級特大地震影響辩涝,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,068評論 3 327
  • 文/蒙蒙 一沧踏、第九天 我趴在偏房一處隱蔽的房頂上張望歌逢。 院中可真熱鬧,春花似錦翘狱、人聲如沸秘案。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,692評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽阱高。三九已至,卻和暖如春茬缩,著一層夾襖步出監(jiān)牢的瞬間赤惊,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,842評論 1 269
  • 我被黑心中介騙來泰國打工凰锡, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留未舟,地道東北人。 一個月前我還...
    沈念sama閱讀 47,797評論 2 369
  • 正文 我出身青樓掂为,卻偏偏與公主長得像裕膀,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子勇哗,可洞房花燭夜當晚...
    茶點故事閱讀 44,654評論 2 354

推薦閱讀更多精彩內(nèi)容