React中常見的動畫實現的幾種方式,react動畫實現幾種

來源:互聯網
上載者:User

React中常見的動畫實現的幾種方式,react動畫實現幾種

現在,使用者對於前端頁面的要求已經不能滿足於實現功能,更要有顏值,有趣味。除了整體 UI 的美觀,在合適的地方添加合適的動畫效果往往比靜態頁面更具有表現力,達到更自然的效果。比如,一個簡單的 loading 動畫或者頁面轉場效果不僅能緩解使用者的等待情緒,甚至通過使用品牌 logo 等形式,默默達到品牌宣傳的效果。

React 作為最近幾年比較流行的前端開發架構,提出了虛擬 DOM 概念,所有 DOM 的變化都先發生在虛擬 DOM 上,通過 DOM diff 來分析網頁的實際變化,然後反映在真實 DOM 上,從而極大地提升網頁效能。然而,在動畫實現方面,React 作為架構並不會直接給組件提供動畫效果,需要開發人員自行實現,而傳統 web 動畫大多數都通過直接操作實際 DOM 元素來實現,這在 React 中顯然是不被提倡的。那麼,在 React 中動畫都是如何?的呢?

所有動畫的本質都是連續修改 DOM 元素的一個或者多個屬性,使其產生連貫的變化效果,從而形成動畫。在 React 中實現動畫本質上與傳統 web 動畫一樣,仍然是兩種方式: 通過 css3 動畫實現和通過 js 修改元素屬性。只不過在具體實現時,要更為符合 React 的架構特性,可以概括為幾類:

  1. 基於定時器或 requestAnimationFrame(RAF) 的間隔動畫;
  2. 基於 css3 的簡單動畫;
  3. React 動畫外掛程式 CssTransitionGroup;
  4. 結合 hook 實現複雜動畫;
  5. 其他第三方動畫庫。
一、基於定時器或 RAF 的間隔動畫

最早,動畫的實現都是依靠定時器 setIntervalsetTimeout 或者 requestAnimationFrame (RAF) 直接修改 DOM 元素的屬性。不熟悉 React 特性的開發人員可能會習慣性地通過 ref 或者 findDOMNode() 擷取真實的 DOM 節點,直接修改其樣式。然而,通過 ref 直接擷取真實 DOM 並對其操作是是不被提倡使用,應當盡量避免這種操作。

因此,我們需要將定時器或者 RAF 等方法與 DOM 節點屬性通過 state 聯絡起來。首先,需要提取出與變化樣式相關的屬性,替換為 state ,然後在合適的生命週期函數中添加定時器或者 requestAnimationFrame 不斷修改 state ,觸發組件更新,從而實現動畫效果。

樣本

以一個進度條為例,代碼如下所示:

// 使用requestAnimationFrame改變stateimport React, { Component } from 'react';export default class Progress extends Component {   constructor(props) {    super(props);    this.state = {      percent: 10    };  }  increase = () => {    const percent = this.state.percent;    const targetPercent = percent >= 90 ? 100 : percent + 10;    const speed = (targetPercent - percent) / 400;    let start = null;    const animate = timestamp => {      if (!start) start = timestamp;      const progress = timestamp - start;      const currentProgress = Math.min(parseInt(speed * progress + percent, 10), targetPercent);      this.setState({        percent: currentProgress      });      if (currentProgress < targetPercent) {        window.requestAnimationFrame(animate);      }    };    window.requestAnimationFrame(animate);  }  decrease = () => {    const percent = this.state.percent;    const targetPercent = percent < 10 ? 0 : percent - 10;    const speed = (percent - targetPercent) / 400;    let start = null;    const animate = timestamp => {      if (!start) start = timestamp;      const progress = timestamp - start;      const currentProgress = Math.max(parseInt(percent - speed * progress, 10), targetPercent);      this.setState({          percent: currentProgress        });      if (currentProgress > targetPercent) {        window.requestAnimationFrame(animate);      }    };    window.requestAnimationFrame(animate);  }  render() {    const { percent } = this.state;    return (      <div>        <div className="progress">          <div className="progress-wrapper" >            <div className="progress-inner" style = {{width: `${percent}%`}} ></div>          </div>          <div className="progress-info" >{percent}%</div>        </div>        <div className="btns">          <button onClick={this.decrease}>-</button>          <button onClick={this.increase}>+</button>        </div>      </div>    );  }}

在樣本中,我們在 increasedecrease 函數中構建線性過渡函數 animationrequestAnimationFrame 在瀏覽器每次重繪前執行會執行過渡函數,計算當前進度條 width 屬性並更新該 state ,使得進度條重新渲染。該樣本的效果如下所示:

這種實現方式在使用 requestAnimationFrame 時效能不錯,完全使用純 js 實現,不依賴於 css,使用定時器時可能出現掉幀卡頓現象。此外,還需要開發人員根據速度函數自己計算狀態,比較複雜。

二、基於 css3 的簡單動畫

當 css3 中的 animationtransition 出現和普及後,我們可以輕鬆地利用 css 實現元素樣式的變化,而不用通過人為計算即時樣式。

樣本

我們仍以上面的進度條為例,使用 css3 實現進度條動態效果,代碼如下所示:

import React, { Component } from 'react';export default class Progress extends Component {   constructor(props) {    super(props);    this.state = {      percent: 10    };  }  increase = () => {    const percent = this.state.percent + 10;    this.setState({      percent: percent > 100 ? 100 : percent,    })  }  decrease = () => {    const percent = this.state.percent - 10;    this.setState({      percent: percent < 0 ? 0 : percent,    })  }  render() {    // 同上例, 省略    ....  }}
.progress-inner { transition: width 400ms cubic-bezier(0.08, 0.82, 0.17, 1); // 其他樣式同上,省略 ...}

在樣本中, increasedecrease 函數中不再計算 width ,而是直接設定增減後的寬度。需要注意的是,在 css 樣式中設定了 transition 屬性,該屬性在其指定的 transition-property 發生變化時自動實現樣式的動態變化效果,並且可以設定不同的速度效果的速度曲線。該樣本的效果如所示,可以發現,與上一個例子不同的是,右側的進度資料是直接變化為目標數字,沒有具體的變化過程,而進度條的動態效果因為不再是線性變化,效果更為生動。

基於 css3 的實現方式具有較高的效能,代碼量少,但是只能依賴於 css 效果,對於複雜動畫也很難實現。此外,通過修改 state 實現動畫效果,只能作用於已經存在於 DOM 樹中的節點。如果想用這種方式為組件添加入場和離場動畫,需要維持至少兩個 state 來實現入場和離場動畫,其中一個 state 用於控制元素是否顯示,另一個 state 用於控制元素在動畫中的變化屬性。在這種情況下,開發人員需要花費大量精力來維護組件的動畫邏輯,十分複雜繁瑣。

三、React 動畫外掛程式 CssTransitionGroup

React 曾為開發人員提供過動畫外掛程式 react-addons-css-transition-group ,後交由社區維護,形成現在的 react-transition-group ,該外掛程式可以方便地實現組件的入場和離場動畫,使用時需要開發人員額外安裝。 react-transition-group 包含 CSSTransitionGroupTransitionGroup 兩個動畫外掛程式,其中,後者是底層 api,前者是後者的進一步封裝,可以較為便捷地實現 css 動畫。

樣本

以一個動態增加tab的為例,代碼如下:

import React, { Component } from 'react'; import { CSSTransitionGroup } from 'react-transition-group';let uid = 2; export default class Tabs extends Component {   constructor(props) {    super(props);    this.state = {      activeId: 1,      tabData: [{        id: 1,        panel: '選項1'      }, {        id: 2,        panel: '選項2'      }]    };  }  addTab = () => {    // 添加tab代碼    ...  }  deleteTab = (id) => {    // 刪除tab代碼    ...  }  render() {    const { tabData, activeId } = this.state;    const renderTabs = () => {      return tabData.map((item, index) => {        return (          <div            className={`tab-item${item.id === activeId ? ' tab-item-active' : ''}`}            key={`tab${item.id}`}          >            {item.panel}            <span className="btns btn-delete" onClick={() => this.deleteTab(item.id)}>✕</span>          </div>        );      })    }    return (      <div>        <div className="tabs" >          <CSSTransitionGroup           transitionName="tabs-wrap"           transitionEnterTimeout={500}           transitionLeaveTimeout={500}          >           {renderTabs()}          </CSSTransitionGroup>          <span className="btns btn-add" onClick={this.addTab}>+</span>        </div>        <div className="tab-cont">          cont        </div>      </div>    );  }}
/* tab動態增加動畫 */.tabs-wrap-enter { opacity: 0.01;}.tabs-wrap-enter.tabs-wrap-enter-active { opacity: 1; transition: all 500ms ease-in;}.tabs-wrap-leave { opacity: 1;}.tabs-wrap-leave.tabs-wrap-leave-active { opacity: 0.01; transition: all 500ms ease-in;}

CSSTransitionGroup 可以為其子節點添加額外的 css 類,然後通過 css 動畫達到入場和離場動畫效果。為了給每個 tab 節點添加動畫效果,需要先將它們包裹在 CSSTransitionGroup 組件中。 當設定 transitionName 屬性為 'tabs-wrapper'transitionEnterTimeout 為400毫秒後,一旦 CSSTransitionGroup 中新增節點,該新增節點會在出現時被添加上 css 類 'tabs-wrapper-enter' ,然後在下一幀時被添加上 css 類 'tabs-wrapper-enter-active' 。由於這兩個 css 類中設定了不同的透明度和 css3 transition 屬性,所以節點實現了透明度由小到大的入場效果。400毫秒後 css 類 'tabs-wrapper-enter''tabs-wrapper-enter-active' 將會同時被移除,節點完成整個入場動畫過程。離場動畫的實作類別似於入場動畫,只不過被添加的 css 類名為 'tabs-wrapper-leave''tabs-wrapper-leave-active' 。該樣本效果如所示:

CSSTransitionGroup 支援以下7個屬性:

其中,入場和離場動畫是預設開啟的,使用時需要設定 transitionEnterTimeouttransitionLeaveTimeout 。值得注意的是, CSSTransitionGroup 還提供出現動畫(appear),使用時需要設定 transitionAppearTimeout 。那麼,出現動畫和入場動畫有什麼區別呢?當設定 transitionAppeartrue 時, CSSTransitionGroup初次渲染 時,會添加一個出現階段。在該階段中, CSSTransitionGroup 的已有子節點都會被相繼添加 css 類 'tabs-wrapper-appear''tabs-wrapper-appear-active' ,實現出現動畫效果。因此, 出現動畫僅適用於 CSSTransitionGroup 在初次渲染時就存在的子節點 ,一旦 CSSTransitionGroup 完成渲染,其子節點就只可能有入場動畫(enter),不可能有出現動畫(appear)。

此外,使用 CSSTransitionGroup 需要注意以下幾點:

  1. CSSTransitionGroup 預設在 DOM 樹中產生一個 span 標籤包裹其子節點,如果想要使用其他 html 標籤,可設定 CSSTransitionGroupcomponent 屬性;
  2. CSSTransitionGroup 的子項目必須添加 key 值才會在節點發生變化時,準確地計算出哪些節點需要添加入場動畫,哪些節點需要添加離場動畫;
  3. CSSTransitionGroup 的動畫效果只作用於直接子節點,不作用於其孫子節點;
  4. 動畫的結束時間不以 css 中 transition-duration 為準,而是以 transitionEnterTimeouttransitionLeaveTimeoutTransitionAppearTimeout 為準,因為某些情況下 transitionend 事件不會被觸發,詳見 MDN transitionend 。

CSSTransitionGroup 實現動畫的優點是:

  1. 簡單易用,可以方便快捷地實現元素的入場和離場動畫;
  2. 與 React 結合,效能比較好。

CSSTransitionGroup 缺點也十分明顯:

  1. 局限於出現動畫,入場動畫和離場動畫;
  2. 由於需要制定 transitionName ,靈活性不夠;
  3. 只能依靠 css 實現簡單的動畫。
四、結合 hook 實現複雜動畫

在實際項目中,可能需要一些更炫酷的動畫效果,這些效果僅依賴於 css3 往往較難實現。此時,我們不妨藉助一些成熟的第三方庫,如 jQuery 或 GASP,結合 React 組件中的生命週期鉤子方法 hook 函數,實現複雜動畫效果。除了 React 組件正常的生命週期外, CSSTransitionGroup 的底層 api TransitonGroup 還為其子項目額外提供了一系列特殊的生命週期 hook 函數,在這些 hook 函數中結合第三方動畫庫可以實現豐富的入場、離場動畫效果。

TransisitonGroup 分別提供一下六個生命週期 hook 函數:

  1. componentWillAppear(callback)
  2. componentDidAppear()
  3. componentWillEnter(callback)
  4. componentDidEnter()
  5. componentWillLeave(callback)
  6. componentDidLeave()

它們的觸發時機:

樣本

GASP 是一個 flash 時代發展至今的動畫庫,借鑒視訊框架的概念,特別適合做長時間的序列動畫效果。本文中,我們用 TransitonGroupreact-gsap-enhancer (一個可以將 GSAP 應用於 React 的增強庫)完成一個圖片畫廊,代碼如下:

import React, { Component } from 'react'; import { TransitionGroup } from 'react-transition-group'; import GSAP from 'react-gsap-enhancer' import { TimelineMax, Back, Sine } from 'gsap';class Photo extends Component {   constructor(props) {    super(props);  }  componentWillEnter(callback) {    this.addAnimation(this.enterAnim, {callback: callback})  }  componentWillLeave(callback) {    this.addAnimation(this.leaveAnim, {callback: callback})  }  enterAnim = (utils) => {    const { id } = this.props;    return new TimelineMax()      .from(utils.target, 1, {        x: `+=${( 4 - id ) * 60}px`,        autoAlpha: 0,        onComplete: utils.options.callback,      }, id * 0.7);  }  leaveAnim = (utils) => {    const { id } = this.props;    return new TimelineMax()      .to(utils.target, 0.5, {        scale: 0,        ease: Sine.easeOut,        onComplete: utils.options.callback,      }, (4 - id) * 0.7);  }  render() {    const { url } = this.props;    return (      <div className="photo">        <img src={url} />      </div>    )  }}const WrappedPhoto = GSAP()(Photo);export default class Gallery extends Component {   constructor(props) {    super(props);    this.state = {      show: false,      photos: [{        id: 1,        url: 'http://img4.imgtn.bdimg.com/it/u=1032683424,3204785822&fm=214&gp=0.jpg'      }, {        id: 2,        url: 'http://imgtu.5011.net/uploads/content/20170323/7488001490262119.jpg'      }, {        id: 3,        url: 'http://tupian.enterdesk.com/2014/lxy/2014/12/03/18/10.jpg'      }, {        id: 4,        url: 'http://img4.imgtn.bdimg.com/it/u=360498760,1598118672&fm=27&gp=0.jpg'      }]    };  }  toggle = () => {    this.setState({      show: !this.state.show    })  }  render() {    const { show, photos } = this.state;    const renderPhotos = () => {      return photos.map((item, index) => {        return <WrappedPhoto id={item.id} url={item.url} key={`photo${item.id}`} />;      })    }    return (      <div>        <button onClick={this.toggle}>toggle</button>        <TransitionGroup component="div">          {show && renderPhotos()}        </TransitionGroup>      </div>    );  }}

在該樣本中,我們在子組件 PhotocomponentWillEntercomponentWillLeave 兩個 hook 函數中為每個子組件添加了入場動畫 enterAnim 和 離場動畫 LeaveAnim 。在入場動畫中,使用 TimeLineMax.from(target, duration, vars, delay) 方式建立時間軸動畫,指定了每個子組件的動畫移動距離隨 id 增大而減小,延期時間隨著 id 增大而增大,離場動畫中每個子組件的延期時間隨著 id 增大而減小,從而實現根據組件 id 不同具有不同的動畫效果。實際使用時,你可以根據需求對任一子組件添加不同的效果。該樣本的效果如所示:

在使用 TransitionGroup 時,在 componentnWillAppear(callback)componentnWillEntercallback)componentnWillLeave(callback) 函數中一定要 在函數邏輯結束後調用 callback ,以保證 TransitionGroup 能正確維護子節點的狀態序列 。

結合 hook 實現動畫可以支援各種複雜動畫,如時間序列動畫等,由於依賴第三方庫,往往動畫效果比較流暢,使用者體驗較好。但是第三方庫的引入,需要開發人員額外學習對應的 api,也提升了代碼複雜度。

五、其他第三方動畫庫

此外,還有很多優秀的第三方動畫庫,如 react-motion ,Animated, velocity-react 等,這些動畫庫在使用時也各有千秋。

Animated

Animated 是一個跨平台的動畫庫,相容 React 和 React Native。由於在動畫過程中,我們只關心動畫的初始狀態、結束狀態和變化函數,並不關心每個時刻元素屬性的具體值,所以 Animatied 採用聲明式的動畫,通過它提供的特定方法計算 css 對象,並傳入 Animated.div 實現動畫效果。

樣本

我們使用 Animated 實現一個圖片翻轉的效果,代碼如下。

import React, { Component } from 'react'; import Animated from 'animated/lib/targets/react-dom';export default class PhotoPreview extends Component {   constructor(props) {    super(props);    this.state = {      anim: new Animated.Value(0)    };  }  handleClick = () => {    const { anim } = this.state;    anim.stopAnimation(value => {      Animated.spring(anim, {        toValue: Math.round(value) + 1      }).start();    });  }  render() {    const { anim } = this.state;    const rotateDegree = anim.interpolate({      inputRange: [0, 4],      outputRange: ['0deg', '360deg']    });    return (      <div>        <button onClick={this.handleClick}>向右翻轉</button>        <Animated.div          style={{            transform: [{              rotate: rotateDegree            }]          }}          className="preivew-wrapper"        >          <img            alt="img"            src="http://img4.imgtn.bdimg.com/it/u=1032683424,3204785822&fm=214&gp=0.jpg"          />        </Animated.div>      </div>    );  }}

在該樣本中,我們希望實現每點擊一次按鈕,圖片向右旋轉90°。在組件初始化時建立了一個初始值為 0 的 Animated 對象 this.state.anim ,在 render 函數中通過插值函數 interpolate 根據 Animated 對象的當前值計算得到對應的旋轉角度 rotateDegree 。我們假設每點擊一次按鈕, Animated 對象的值加 1,相應地映像轉動90°,所以,設定 interpolate 函數的輸入區間為[0, 4],輸出區間為['0deg', '360deg']進行線性插值。如果 Animated 對象當前值為 2,對應的旋轉角度就是 180deg。在組件渲染結構中,需要使用 Animated.div 包裹動畫節點,並將變化的元素屬性封裝為 css 對象作為 stlye 傳入 Animated.div 中。在點擊事件中,考慮到按鈕可以多次連續點擊,我們首先使用 stopAnimation 停止當前動畫,並擷取 Animated 對象的當前值 value ,隨後使用 Animated.spring 函數開啟一次彈簧動畫過程,從而實現一個流暢的動畫效果。由於每次轉動停止時,我們希望圖片的翻轉角度都是90°的整數倍,所以需要對 Animated.spring 的終止值進行取整。最終我們實現了如下效果:

使用時需要注意一下幾點:

  1. Animated 對象的值和其插值結果只能作用於 Animated.div 節點;
  2. interpolate 預設會根據輸入區間和輸出區間進行線性插值,如果輸入值超出輸入區間不受影響,插值結果預設會根據輸出區間向外延展插值,可以通過設定 extrapolate 屬性限制插值結果區間。

Animated 在動畫過程中不直接修改組件 state ,而是通過其建立對象的組件和方法直接修改元素的屬性,不會重複觸發 render 函數,是 React Native 中非常穩定的動畫庫。但是在 React 中存在低版本瀏覽器安全色問題,且具有一定學習成本。

結語

當我們在 React 中實現動畫時,首先要考量動畫的難易程度和使用情境,對於簡單動畫,優先使用 css3 實現,其次是基於 js 的時間間隔動畫。如果是元素入場動畫和離場動畫,則建議結合 CSSTransitionGroup 或者 TransitionGroup 實現。當要實現的動畫效果較為複雜時,不妨嘗試一些優秀的第三方庫,開啟精彩的動效大門。

Ps. 本文所有範例程式碼可訪問 github 查看

參考資料:

react-transition-group

react-gsap-enhancer

A Comparison of Animation Technologies

React Animations in Depth

以上就是本文的全部內容,希望對大家的學習有所協助,也希望大家多多支援幫客之家。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.