如今,用戶對於前端頁面的要求已經不能知足於實現功能,更要有顏值,有趣味。除了總體 UI 的美觀,在合適的地方添加合適的動畫效果每每比靜態頁面更具備表現力,達到更天然的效果。好比,一個簡單的 loading 動畫或者頁面切換效果不只能緩解用戶的等待情緒,甚至經過使用品牌 logo 等形式,默默達到品牌宣傳的效果。javascript
React 做爲最近幾年比較流行的前端開發框架,提出了虛擬 DOM 概念,全部 DOM 的變化都先發生在虛擬 DOM 上,經過 DOM diff 來分析網頁的實際變化,而後反映在真實 DOM 上,從而極大地提高網頁性能。然而,在動畫實現方面,React 做爲框架並不會直接給組件提供動畫效果,須要開發者自行實現,而傳統 web 動畫大多數都經過直接操做實際 DOM 元素來實現,這在 React 中顯然是不被提倡的。那麼,在 React 中動畫都是如何實現的呢?css
全部動畫的本質都是連續修改 DOM 元素的一個或者多個屬性,使其產生連貫的變化效果,從而造成動畫。在 React 中實現動畫本質上與傳統 web 動畫同樣,仍然是兩種方式: 經過 css3 動畫實現和經過 js 修改元素屬性。只不過在具體實現時,要更爲符合 React 的框架特性,能夠歸納爲幾類:html
CssTransitionGroup
;最先,動畫的實現都是依靠定時器 setInterval
,setTimeout
或者 requestAnimationFrame
(RAF) 直接修改 DOM 元素的屬性。不熟悉 React 特性的開發者可能會習慣性地經過 ref
或者 findDOMNode()
獲取真實的 DOM 節點,直接修改其樣式。然而,經過 ref
直接獲取真實 DOM 並對其操做是是不被提倡使用,應當儘可能避免這種操做。前端
所以,咱們須要將定時器或者 RAF 等方法與 DOM 節點屬性經過 state
聯繫起來。首先,須要提取出與變化樣式相關的屬性,替換爲 state
,而後在合適的生命週期函數中添加定時器或者 requestAnimationFrame
不斷修改 state
,觸發組件更新,從而實現動畫效果。java
以一個進度條爲例,代碼以下所示:react
// 使用requestAnimationFrame改變state
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;
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>
);
}
}
複製代碼
在示例中,咱們在 increase
和 decrease
函數中構建線性過渡函數 animation
,requestAnimationFrame
在瀏覽器每次重繪前執行會執行過渡函數,計算當前進度條width
屬性並更新該 state
,使得進度條從新渲染。該示例的效果以下所示:css3
這種實現方式在使用 requestAnimationFrame
時性能不錯,徹底使用純 js 實現,不依賴於 css,使用定時器時可能出現掉幀卡頓現象。此外,還須要開發者根據速度函數本身計算狀態,比較複雜。git
當 css3 中的 animation
和 transition
出現和普及後,咱們能夠輕鬆地利用 css 實現元素樣式的變化,而不用經過人爲計算實時樣式。github
咱們仍以上面的進度條爲例,使用 css3 實現進度條動態效果,代碼以下所示:web
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);
// 其餘樣式同上,省略
...
}
複製代碼
在示例中,increase
和 decrease
函數中再也不計算 width
,而是直接設置增減後的寬度。須要注意的是,在 css 樣式中設置了 transition
屬性,當 width 屬性發生變化時自動實現樣式的動態變化效果,而且能夠設置不一樣的速度效果的速度曲線。該示例的效果以下圖所示,能夠發現,與上一個例子不一樣的是,右側的進度數據是直接變化爲目標數字,沒有具體的變化過程,而進度條的動態效果由於再也不是線性變化,效果更爲生動。
基於 css3 的實現方式具備較高的性能,代碼量少,可是隻能依賴於 css 效果,對於複雜動畫也很難實現。此外,經過修改 state
實現動畫效果,只能做用於已經存在於 DOM 樹中的節點。若是想用這種方式爲組件添加入場和離場動畫,須要維持至少兩個 state
來實現入場和離場動畫,其中一個 state
用於控制元素是否顯示,另外一個 state
用於控制元素在動畫中的變化屬性。在這種狀況下,開發者須要花費大量精力來維護組件的動畫邏輯,十分複雜繁瑣。
CssTransitionGroup
React 曾爲開發者提供過動畫插件 react-addons-css-transition-group
,後交由社區維護,造成如今的 react-transition-group
,該插件能夠方便地實現組件的入場和離場動畫,使用時須要開發者額外安裝。react-transition-group
包含 CSSTransitionGroup
和 TransitionGroup
兩個動畫插件,其中,後者是底層 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個屬性:
其中,入場和離場動畫是默認開啓的,使用時須要設置 transitionEnterTimeout
和 transitionLeaveTimeout
。值得注意的是,CSSTransitionGroup
還提供出現動畫(appear),使用時須要設置 transitionAppearTimeout
。那麼,出現動畫和入場動畫有什麼區別呢?當設定 transitionAppear
爲 true
時,CSSTransitionGroup
在初次渲染時,會添加一個出現階段。在該階段中,CSSTransitionGroup
的已有子節點都會被相繼添加 css 類 'tabs-wrapper-appear'
和 'tabs-wrapper-appear-active'
,實現出現動畫效果。所以,出現動畫僅適用於 CSSTransitionGroup
在初次渲染時就存在的子節點,一旦 CSSTransitionGroup
完成渲染,其子節點就只可能有入場動畫(enter),不可能有出現動畫(appear)。
此外,使用 CSSTransitionGroup
須要注意如下幾點:
CSSTransitionGroup
默認在 DOM 樹中生成一個 span
標籤包裹其子節點,若是想要使用其餘 html 標籤,可設定 CSSTransitionGroup
的 component
屬性;CSSTransitionGroup
的子元素必須添加 key
值纔會在節點發生變化時,準確地計算出哪些節點須要添加入場動畫,哪些節點須要添加離場動畫;CSSTransitionGroup
的動畫效果只做用於直接子節點,不做用於其孫子節點;transitionEnterTimeout
,transitionLeaveTimeout
,TransitionAppearTimeout
爲準,由於某些狀況下 transitionend 事件不會被觸發,詳見MDN transitionend。CSSTransitionGroup
實現動畫的優勢是:
CSSTransitionGroup
缺點也十分明顯:
transitionName
,靈活性不夠;在實際項目中,可能須要一些更炫酷的動畫效果,這些效果僅依賴於 css3 每每較難實現。此時,咱們不妨藉助一些成熟的第三方庫,如 jQuery 或 GASP,結合 React 組件中的生命週期鉤子方法 hook 函數,實現複雜動畫效果。除了 React 組件正常的生命週期外,CSSTransitionGroup
的底層 api TransitonGroup
還爲其子元素額外提供了一系列特殊的生命週期 hook 函數,在這些 hook 函數中結合第三方動畫庫能夠實現豐富的入場、離場動畫效果。
TransisitonGroup
分別提供一下六個生命週期 hook 函數:
它們的觸發時機如圖所示:
GASP 是一個 flash 時代發展至今的動畫庫,借鑑視頻幀的概念,特別適合作長時間的序列動畫效果。本文中,咱們用 TransitonGroup
和 react-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>
);
}
}
複製代碼
在該示例中,咱們在子組件 Photo
的 componentWillEnter
和 componentWillLeave
兩個 hook 函數中爲每一個子組件添加了入場動畫 enterAnim
和 離場動畫 LeaveAnim
。在入場動畫中,使用 TimeLineMax.from(target, duration, vars, delay)
方式創建時間軸動畫,指定了每一個子組件的動畫移動距離隨 id
增大而減少,延期時間隨着 id
增大而增大,離場動畫中每一個子組件的延期時間隨着 id
增大而減少,從而實現根據組件 id
不一樣具備不一樣的動畫效果。實際使用時,你能夠根據需求對任一子組件添加不一樣的效果。該示例的效果以下圖所示:
在使用 TransitionGroup
時,在 componentnWillAppear(callback)
,componentnWillEntercallback)
,componentnWillLeave(callback)
函數中必定要在函數邏輯結束後調用 callback
,以保證 TransitionGroup
能正確維護子節點的狀態序列。關於 GASP 的詳細使用方法可參考GASP官方文檔和博文GSAP,專業的Web動畫庫,本文再也不贅述。
結合 hook 實現動畫能夠支持各類複雜動畫,如時間序列動畫等,因爲依賴第三方庫,每每動畫效果比較流暢,用戶體驗較好。可是第三方庫的引入,須要開發者額外學習對應的 api,也提高了代碼複雜度。
此外,還有不少優秀的第三方動畫庫,如 react-motion,Animated,velocity-react等,這些動畫庫在使用時也各有千秋。
Animated 是一個跨平臺的動畫庫,兼容 React 和 React Native。因爲在動畫過程當中,咱們只關心動畫的初始狀態、結束狀態和變化函數,並不關心每一個時刻元素屬性的具體值,因此 Animated 採用聲明式的動畫,經過它提供的特定方法計算 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
。Animated
對象中有插值函數 interpolate
,當設定輸入區間 inputRange
和輸出區間 outputRange
後,插值函數能夠根據 Animated
對象的當前值進行線性插值,計算獲得對應的映射值。
在本例中,咱們假設每點擊一次按鈕,this.state.anim
的值加 1,圖像須要轉動90°。在 render 函數中,咱們設置插值函數 this.state.anim.interpolate
的輸入區間爲[0, 4],輸出區間爲['0deg', '360deg']。當執行動畫時,this.state.anim
的值發生變化,插值函數根據 this.state.anim
當前值,計算獲得旋轉角度 rotateDegree
,觸發組件的從新渲染。所以,若是 Animated
對象當前值爲 2,對應的旋轉角度就是 180deg。在組件渲染結構中,須要使用 Animated.div
包裹動畫節點,並將 rotateDegree
封裝爲 css 對象做爲 stlye 傳入 Animated.div
中,實現節點 css 屬性的變化。
在點擊事件中,考慮到按鈕可能連續屢次點擊,咱們首先使用 stopAnimation
中止當前正在進行的動畫,該函數會在回調函數中返回一個 {value : number} 對象,value 對應最後一刻的動畫屬性值。根據獲取的 value
值,隨後使用 Animated.spring
函數開啓一次新的彈簧動畫過程,從而實現一個流暢的動畫效果。因爲每次轉動中止時,咱們但願圖片的翻轉角度都是90°的整數倍,因此須要對 Animated.spring
的終止值進行取整。最終咱們實現了以下效果:
使用時須要注意一下幾點:
Animated
對象的值和其插值結果只能做用於 Animated.div
節點;interpolate
默認會根據輸入區間和輸出區間進行線性插值,若是輸入值超出輸入區間不受影響,插值結果默認會根據輸出區間向外延展插值,能夠經過設置 extrapolate
屬性限制插值結果區間。Animated 在動畫過程當中不直接修改組件 state
,而是經過其新建對象的組件和方法直接修改元素的屬性,不會重複觸發 render 函數,是 React Native 中很是穩定的動畫庫。可是在 React 中存在低版本瀏覽器兼容問題,且具備必定學習成本。
當咱們在 React 中實現動畫時,首先要考量動畫的難易程度和使用場景,對於簡單動畫,優先使用 css3 實現,其次是基於 js 的時間間隔動畫。若是是元素入場動畫和離場動畫,則建議結合 CSSTransitionGroup
或者 TransitionGroup
實現。當要實現的動畫效果較爲複雜時,不妨嘗試一些優秀的第三方庫,打開精彩的動效大門。
Ps. 本文全部示例代碼可訪問 github 查看
A Comparison of Animation Technologies
本文首發於有贊技術博客。