ReactNative Animated動畫詳解

最近ReactNative(以下簡稱RN)在前端的熱度越來越高揭糕,不少同學(xué)開始在業(yè)務(wù)中嘗試使用RN赴背,這里著重介紹一下RN中動畫的使用與實現(xiàn)原理赋兵。

使用篇

舉個簡單的栗子

var React = require('react-native');
var {
    Animated,
    Easing,
    View,
    StyleSheet,
    Text
} = React;
 
var Demo = React.createClass({
    getInitialState() {
        return {
            fadeInOpacity: new Animated.Value(0) // 初始值
        };
    },
    componentDidMount() {
        Animated.timing(this.state.fadeInOpacity, {
            toValue: 1, // 目標(biāo)值
            duration: 2500, // 動畫時間
            easing: Easing.linear // 緩動函數(shù)
        }).start();
    },
    render() {
        return (
            <Animated.View style={[styles.demo, {
                    opacity: this.state.fadeInOpacity
                }]}>
                <Text style={styles.text}>悄悄的至扰,我出現(xiàn)了</Text>
            </Animated.View>
        );
    }
});
 
var styles = StyleSheet.create({
    demo: {
        flex: 1,
        alignItems: 'center',
        justifyContent: 'center',
        backgroundColor: 'white',
    },
    text: {
        fontSize: 30
    }
});

注意:Android使用Animated.View時 必須設(shè)置它的demo屬性楞遏,不然動畫視圖顯示不出來


是不是很簡單易懂<(?????)> 和JQuery的Animation用法很類似茬暇。

步驟拆解

一個RN的動畫,可以按照以下步驟進行寡喝。

  • 1糙俗、使用基本的Animated組件,如Animated.View Animated.Image Animated.Text (重要预鬓!不加Animated的后果就是一個看不懂的報錯巧骚,然后查半天動畫參數(shù),最后懷疑人生)
  • 2珊皿、使用Animated.Value設(shè)定一個或多個初始化值(透明度网缝,位置等等)。
  • 3蟋定、將初始化值綁定到動畫目標(biāo)的屬性上(如style)
  • 4粉臊、通過Animated.timing等函數(shù)設(shè)定動畫參數(shù)
  • 5、調(diào)用start啟動動畫驶兜。

栗子敢再復(fù)雜一點嗎扼仲?

顯然,一個簡單的漸顯是無法滿足各位觀眾老爺們的好奇心的.我們試一試加上多個動畫

getInitialState() {
    return (
        fadeInOpacity: new Animated.Value(0),
            rotation: new Animated.Value(0),
            fontSize: new Animated.Value(0)
    );
},
componentDidMount() {
    var timing = Animated.timing;
    Animated.parallel(['fadeInOpacity', 'rotation', 'fontSize'].map(property => {
                return timing(this.state[property], {
                toValue: 1,
                duration: 1000,
                easing: Easing.linear
            });
        })).start();
}抄淑,
render() {
    return (<Animated.View style={[styles.demo, {
            opacity: this.state.fadeInOpacity,
                transform: [{
                    rotateZ: this.state.rotation.interpolate({
                        inputRange: [0,1],
                        outputRange: ['0deg', '360deg']
                    })
                }]
            }]}><Animated.Text style={{
                fontSize: this.state.fontSize.interpolate({
                    inputRange: [0,1],
                    outputRange: [12,26]
                })
            }}>我騎著七彩祥云出現(xiàn)了????</Animated.Text>
            </Animated.View>
    );
}

注意到我們給文字區(qū)域加上了字體增大的動畫效果屠凶,相應(yīng)地,也要修改Text為Animated.Text

強大的interpolate

上面的栗子使用了interpolate函數(shù)肆资,也就是插值函數(shù)矗愧。這個函數(shù)很強大,實現(xiàn)了數(shù)值大小郑原、單位的映射轉(zhuǎn)換唉韭,比如

{   
    inputRange: [0,1],
    outPutRange: ['0deg','180deg']
}

當(dāng)setValue(0.5)時,會自動映射成90deg犯犁。 inputRange并不局限于[0,1]區(qū)間属愤,可以畫出多段。 interpolate一般用于多個動畫共用一個Animated.Value酸役,只需要在每個屬性里面映射好對應(yīng)的值住诸,就可以用一個變量控制多個動畫驾胆。 事實上,上例中的fadeInOpacityfontSizerotation用一個變量來聲明就可以了贱呐。(那你寫那么多變量逗我嗎(╯‵□′)╯︵┻━┻) (因為我要強行使用parallel ?┬─┬ ノ( ' – 'ノ))

流程控制

在剛才的栗子中丧诺,我們使用了Parallel來實現(xiàn)多個動畫的并行渲染,其它用于流程控制的API還有:

  • sequence接受一系列動畫數(shù)組為參數(shù)奄薇,并依次執(zhí)行
  • stagger接受一系列動畫數(shù)組和一個延遲時間锅必,按照序列,每隔一個延遲時間后執(zhí)行下一個動畫(其實就是插入了delay的parrllel)
  • delay生成一個延遲時間(基于timing的delay參數(shù)生成)

例3

getInitialState() {
    return (
        anim: [1,2,3].map(() => new Animated.Value(0)) // 初始化3個值
    );
},
 
componentDidMount() {
    var timing = Animated.timing;
    Animated.sequence([
        Animated.stagger(200, this.state.anim.map(left => {
            return timing(left, {
                toValue: 1,
              });
            }).concat(
                this.state.anim.map(left => {
                    return timing(left, {
                        toValue: 0,
                    });
                })
            )), // 三個view滾到右邊再還原惕艳,每個動作間隔200ms
            Animated.delay(400), // 延遲400ms,配合sequence使用
            timing(this.state.anim[0], {
                toValue: 1 
            }),
            timing(this.state.anim[1], {
                toValue: -1
            }),
            timing(this.state.anim[2], {
                toValue: 0.5
            }),
            Animated.delay(400),
            Animated.parallel(this.state.anim.map((anim) => timing(anim, {
                toValue: 0
            }))) // 同時回到原位置
        ]
    ).start();
},
render() {
    var views = this.state.anim.map(function(value, i) {
        return (
            <Animated.View
                key={i}
                style={[styles.demo, styles['demo' + i], {
                    left: value.interpolate({
                        inputRange: [0,1],
                        outputRange: [0,200]
                    })
                }]}>
                <Text style={styles.text}>我是第{i + 1}個View</Text>
 
            </Animated.View>
        );
    });
    return <View style={styles.container}>
               <Text>sequence/delay/stagger/parallel演示</Text>
               {views}
           </View>;
}

Spring/Decay/Timing

前面的幾個動畫都是基于時間實現(xiàn)的驹愚,事實上远搪,在日常的手勢操作中,基于時間的動畫往往難以滿足復(fù)雜的交互動畫逢捺。對此谁鳍,RN還提供了另外兩種動畫模式。

  • Spring 彈簧效果

  • friction 摩擦系數(shù)劫瞳,默認(rèn)40

  • tension 張力系數(shù)倘潜,默認(rèn)7

  • bounciness

  • speed

  • Decay 衰變效果

  • velocity 初速率

  • deceleration 衰減系數(shù) 默認(rèn)0.997

Spring支持 friction與tension 或者 bounciness與speed 兩種組合模式,這兩種模式不能并存志于。 其中friction與tension模型來源于origami,一款F家自制的動畫原型設(shè)計工具涮因,而bounciness與speed則是傳統(tǒng)的彈簧模型參數(shù)。

Track && Event

RN動畫支持跟蹤功能伺绽,這也是日常交互中很常見的需求养泡,比如跟蹤用戶的手勢變化,跟蹤另一個動畫奈应。而跟蹤的用法也很簡單澜掩,只需要指定toValue到另一個Animated.Value就可以了。 交互動畫需要跟蹤用戶的手勢操作杖挣,Animated也很貼心地提供了事件接口的封裝肩榕,示例:

// Animated.event 封裝手勢事件等值映射到對應(yīng)的Animated.Value
onPanResponderMove: Animated.event(
    [null, {dx: this.state.x, dy: this.state.y}] // map gesture to leader
)

在官方的demo上改了一下,加了一張費玉污的圖惩妇,效果圖如下 代碼太長株汉,就不貼出來了,可以參考官方Github代碼

動畫循環(huán)

Animated的start方法是支持回調(diào)函數(shù)的屿附,在動畫或某個流程結(jié)束的時候執(zhí)行郎逃,這樣子就可以很簡單地實現(xiàn)循環(huán)動畫了。

startAnimation() {
    this.state.rotateValue.setValue(0);
    Animated.timing(this.state.rotateValue, {
        toValue: 1,
        duration: 800,
        easing: Easing.linear
    }).start(() => this.startAnimation());
}

是不是很魔性?[doge]

原理篇

首先感謝能看到這里的小伙伴們:)

在上面的文章中挺份,我們已經(jīng)基本掌握了RN Animated的各種常用API褒翰,接下來我們來了解一下這些API是如何設(shè)計出來的。

聲明: 以下內(nèi)容參考自Animated原作者的分享視頻

首先,從React的生命周期來編程的話优训,一個動畫大概是這樣子寫:

getInitialState() {
    return {left: 0};
}
 
render(){
    return (
        <div style={{left: this.state.left}}>
            <Child />
        </div>
    );
}
 
onChange(value) {
    this.setState({left: value});
}

只需要通過requestAnimationFrame調(diào)用onChange朵你,輸入對應(yīng)的value,動畫就簡單粗暴地跑起來了????揣非,全劇終抡医。

然而事實總是沒那么簡單,問題在哪早敬?

我們看到忌傻,上述動畫基本是以毫秒級的頻率在調(diào)用setState,而React的每次setState都會重新調(diào)用render方法搞监,并切遍歷子元素進行渲染水孩,即使有Dom Diff也可能扛不住這么大的計算量和UI渲染。

那么該如何優(yōu)化呢琐驴?

  • 關(guān)鍵詞:
  • ShouldComponentUpdate
  • <StaticContainer>(靜態(tài)容器)
  • Element Caching (元素緩存)
  • Raw DOM Mutation (原生DOM操作)
  • ↑↑↓↓←→←→BA (秘籍)

ShouldComponentUpdate

學(xué)過React的都知道俘种,ShouldComponentUpdate是性能優(yōu)化利器,只需要在子組件的shouldComponentUpdate返回false绝淡,分分鐘渲染性能爆表宙刘。


然而并非所有的子元素都是一成不變的,粗暴地返回false的話子元素就變成一灘死水了牢酵。而且組件間應(yīng)該是獨立的悬包,子組件很可能是其他人寫的,父元素不能依賴于子元素的實現(xiàn)馍乙。

<StaticContainer>(靜態(tài)容器)

這時候可以考慮封裝一個容器玉罐,管理ShouldCompontUpdate,如圖示:

小明和老王再也不用關(guān)心父元素的動畫實現(xiàn)啦潘拨。

一個簡單的<StaticContainer>實現(xiàn)如下:

class StaticContainer extends React.Component {
    render(){
        return this.props.children; 
    }
    shouldComponentUpdate(nextProps){
        return nextProps.shouldUpdate; // 父元素控制是否更新
    }
}
 
// 父元素嵌入StaticContainer
render() {
    return (
        <div style={{left: this.state.left}}>
            <StaticContainer
            shouldUpdate={!this.state.isAnimating}>
                <ExpensiveChild />
            </StaticContainer>
        </div>
    );
}

Element Caching 緩存元素

還有另一種思路優(yōu)化子元素的渲染吊输,那就是緩存子元素的渲染結(jié)果到局地變量。

render(){
    this._child = this._child || <ExpensiveChild />;
    return (
        <div style={{left:this.state.left}}>
            {this._child}
        </div>
    );
}

緩存之后铁追,每次setState時季蚂,React通過DOM Diff就不再渲染子元素了。

上面的方法都有弊端琅束,就是條件競爭扭屁。當(dāng)動畫在進行的時候,子元素恰好獲得了新的state涩禀,而這時候動畫無視了這個更新料滥,最后就會導(dǎo)致狀態(tài)不一致,或者動畫結(jié)束的時候子元素發(fā)生了閃動艾船,這些都是影響用戶操作的問題葵腹。

Raw DOM Mutation 原生DOM操作

剛剛都是在React的生命周期里實現(xiàn)動畫高每,事實上,我們只想要變更這個元素的left值践宴,并不希望各種重新渲染鲸匿、DOM DIFF等等發(fā)生。

“React阻肩,我知道自己要干啥带欢,你一邊涼快去“

如果我們跳出這個生命周期,直接找到元素進行變更烤惊,是不是更簡單呢乔煞?

簡單易懂,性能彪悍柒室,有木有瘤缩?!

然而弊端也很明顯伦泥,比如這個組件unmount之后,動畫就報錯了锦溪。

Uncaught Exception: Cannot call ‘style’ of null

而且這種方法照樣避不開條件競爭——動畫值改變的時候不脯,有可能發(fā)生setState之后,left又回到初始值之類的情況刻诊。

再者防楷,我們使用React,就是因為不想去關(guān)心dom元素的操作则涯,而是交給React管理复局,直接使用Dom操作顯然違背了初衷。

↑↑↓↓←→←→BA (秘籍)

嘮叨了這么多粟判,這也不行亿昏,那也不行,什么才是真理档礁?

我們既想要原生DOM操作的高性能角钩,又想要React完善的生命周期管理,如何把兩者優(yōu)勢結(jié)合到一起呢呻澜?答案就是Data Binding(數(shù)據(jù)綁定)

render(){
    return(
        <Animated.div style={{left: this.state.left}}>
             <ExpensiveChild />
        </Animated.div>
    );
}
 
getInitialState(){
    return {left: new Animated.Value(0)}; // 實現(xiàn)了數(shù)據(jù)綁定的類
}
 
onUpdate(value){
    this.state.left.setValue(value); // 不是setState
}

首先递礼,需要實現(xiàn)一個具有數(shù)據(jù)綁定功能的類Animated.Value,提供setValueonChange等接口羹幸。 其次脊髓,由于原生的組件并不能識別Value,需要將動畫元素用Animated包裹起來栅受,在內(nèi)部處理數(shù)據(jù)變更與DOM操作将硝。

一個簡單的動畫組件實現(xiàn)如下:

Animated.div = class extends React.Component{
    componentWillUnmount() {
        nextProps.style.left.removeAllListeners();
    },
    // componentWillMount需要完成與componentWillReceiveProps同樣的操作恭朗,此處略
    componentWillReceiveProps(nextProps) {
        nextProps.style.left.removeAllListeners();
        nextProps.style.left.onChange(value => {
            React.findDOMNode(this).style.left = value + 'px';
        });
        
        // 將動畫值解析為普通數(shù)值傳給原生div
        this._props = React.addons.update(
            nextProps,
            {style:{left:{$set: nextProps.style.left.getValue()}}}
        );
    },
    render() {
        return <div ...{this._props} />;
    }
}

代碼很簡短,做的事情有:

  • 1袋哼、遍歷傳入的props冀墨,查找是否有Animated.Value的實例,并綁定相應(yīng)的DOM操作涛贯。
  • 2诽嘉、每次props變更或者組件unmount的時候,停止監(jiān)聽數(shù)據(jù)綁定事件弟翘,避免了條件競爭和內(nèi)存泄露問題虫腋。
  • 3、將初始傳入的Animated.Value值逐個轉(zhuǎn)化為普通數(shù)值稀余,再交給原生的React組件進行渲染悦冀。

綜上,通過封裝一個Animated的元素睛琳,內(nèi)部通過數(shù)據(jù)綁定和DOM操作變更元素盒蟆,結(jié)合React的生命周期完善內(nèi)存管理,解決條件競爭問題师骗,對外表現(xiàn)則與原生組件相同历等,實現(xiàn)了高效流暢的動畫效果。

讀到這里辟癌,應(yīng)該知道為什么ImageText等做動畫一定要使用Animated加持過的元素了吧寒屯?

參考資料
React Addons Update
React Component Lifecycle
Christopher Chedeau – Animated
http://www.alloyteam.com/2016/01/reactnative-animated/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市黍少,隨后出現(xiàn)的幾起案子寡夹,更是在濱河造成了極大的恐慌,老刑警劉巖厂置,帶你破解...
    沈念sama閱讀 211,639評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件菩掏,死亡現(xiàn)場離奇詭異,居然都是意外死亡昵济,警方通過查閱死者的電腦和手機患蹂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來砸紊,“玉大人传于,你說我怎么就攤上這事∽硗纾” “怎么了沼溜?”我有些...
    開封第一講書人閱讀 157,221評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長游添。 經(jīng)常有香客問我系草,道長通熄,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,474評論 1 283
  • 正文 為了忘掉前任找都,我火速辦了婚禮唇辨,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘能耻。我一直安慰自己赏枚,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,570評論 6 386
  • 文/花漫 我一把揭開白布晓猛。 她就那樣靜靜地躺著饿幅,像睡著了一般。 火紅的嫁衣襯著肌膚如雪戒职。 梳的紋絲不亂的頭發(fā)上栗恩,一...
    開封第一講書人閱讀 49,816評論 1 290
  • 那天,我揣著相機與錄音洪燥,去河邊找鬼磕秤。 笑死,一個胖子當(dāng)著我的面吹牛捧韵,可吹牛的內(nèi)容都是我干的市咆。 我是一名探鬼主播,決...
    沈念sama閱讀 38,957評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼纫版,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了客情?” 一聲冷哼從身側(cè)響起其弊,我...
    開封第一講書人閱讀 37,718評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎膀斋,沒想到半個月后梭伐,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,176評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡仰担,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,511評論 2 327
  • 正文 我和宋清朗相戀三年糊识,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片摔蓝。...
    茶點故事閱讀 38,646評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡赂苗,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出贮尉,到底是詐尸還是另有隱情拌滋,我是刑警寧澤,帶...
    沈念sama閱讀 34,322評論 4 330
  • 正文 年R本政府宣布猜谚,位于F島的核電站败砂,受9級特大地震影響赌渣,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜昌犹,卻給世界環(huán)境...
    茶點故事閱讀 39,934評論 3 313
  • 文/蒙蒙 一坚芜、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧斜姥,春花似錦鸿竖、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,755評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至搞坝,卻和暖如春搔谴,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背桩撮。 一陣腳步聲響...
    開封第一講書人閱讀 31,987評論 1 266
  • 我被黑心中介騙來泰國打工敦第, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人店量。 一個月前我還...
    沈念sama閱讀 46,358評論 2 360
  • 正文 我出身青樓芜果,卻偏偏與公主長得像,于是被迫代替她去往敵國和親融师。 傳聞我的和親對象是個殘疾皇子右钾,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,514評論 2 348

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

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,777評論 25 707
  • 參考鏈接:React Native開發(fā)之動畫(Animations) 最近ReactNative(以下簡稱RN)在...
    滕的世界閱讀 9,524評論 1 13
  • 3. JSX JSX是對JavaScript語言的一個擴展語法, 用于生產(chǎn)React“元素”旱爆,建議在描述UI的時候...
    pixels閱讀 2,809評論 0 24
  • 原教程內(nèi)容詳見精益 React 學(xué)習(xí)指南舀射,這只是我在學(xué)習(xí)過程中的一些閱讀筆記,個人覺得該教程講解深入淺出怀伦,比目前大...
    leonaxiong閱讀 2,813評論 1 18
  • 有時感覺人生仿佛一個圓,有人站在圓心上桑孩,有人立在周長拜鹤。 人群熙熙攘攘,各有各的步伐流椒,向外走署惯。 他們選定一個角度,然...
    子白君閱讀 257評論 0 0