ReactNative Animated詳解(轉(zhuǎn))

參考鏈接:
React Native開(kāi)發(fā)之動(dòng)畫(huà)(Animations)

最近ReactNative(以下簡(jiǎn)稱(chēng)RN)在前端的熱度越來(lái)越高,不少同學(xué)開(kāi)始在業(yè)務(wù)中嘗試使用RN,這里著重介紹一下RN中動(dòng)畫(huà)的使用與實(shí)現(xiàn)原理。

使用篇
舉個(gè)簡(jiǎ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, // 動(dòng)畫(huà)時(shí)間
            easing: Easing.linear // 緩動(dòng)函數(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
    }
});


是不是很簡(jiǎn)單易懂<(?????)> 和JQuery的Animation用法很類(lèi)似。
步驟拆解
一個(gè)RN的動(dòng)畫(huà),可以按照以下步驟進(jìn)行。
使用基本的Animated組件,如Animated.View Animated.Image Animated.Text (重要!不加Animated的后果就是一個(gè)看不懂的報(bào)錯(cuò),然后查半天動(dòng)畫(huà)參數(shù),最后懷疑人生
使用Animated.Value設(shè)定一個(gè)或多個(gè)初始化值(透明度,位置等等)。
將初始化值綁定到動(dòng)畫(huà)目標(biāo)的屬性上(如style)
通過(guò)Animated.timing等函數(shù)設(shè)定動(dòng)畫(huà)參數(shù)
調(diào)用start啟動(dòng)動(dòng)畫(huà)。

栗子敢再?gòu)?fù)雜一點(diǎn)嗎?
顯然,一個(gè)簡(jiǎn)單的漸顯是無(wú)法滿(mǎn)足各位觀眾老爺們的好奇心的.我們?cè)囈辉嚰由隙鄠€(gè)動(dòng)畫(huà)


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ū)域加上了字體增大的動(dòng)畫(huà)效果,相應(yīng)地,也要修改Text為Animated.Text


強(qiáng)大的interpolate
上面的栗子使用了interpolate函數(shù),也就是插值函數(shù)。這個(gè)函數(shù)很強(qiáng)大,實(shí)現(xiàn)了數(shù)值大小、單位的映射轉(zhuǎn)換,比如

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

當(dāng)setValue(0.5)時(shí),會(huì)自動(dòng)映射成90deg。 inputRange并不局限于[0,1]區(qū)間,可以畫(huà)出多段。 interpolate一般用于多個(gè)動(dòng)畫(huà)共用一個(gè)Animated.Value,只需要在每個(gè)屬性里面映射好對(duì)應(yīng)的值,就可以用一個(gè)變量控制多個(gè)動(dòng)畫(huà)。 事實(shí)上,上例中的fadeInOpacityfontSizerotation用一個(gè)變量來(lái)聲明就可以了。(那你寫(xiě)那么多變量逗我嗎(╯‵□′)╯︵┻━┻) (因?yàn)槲乙獜?qiáng)行使用parallel ?┬─┬ ノ( ' – 'ノ))
流程控制
在剛才的栗子中,我們使用了Parallel來(lái)實(shí)現(xiàn)多個(gè)動(dòng)畫(huà)的并行渲染,其它用于流程控制的API還有:
sequence接受一系列動(dòng)畫(huà)數(shù)組為參數(shù),并依次執(zhí)行
stagger接受一系列動(dòng)畫(huà)數(shù)組和一個(gè)延遲時(shí)間,按照序列,每隔一個(gè)延遲時(shí)間后執(zhí)行下一個(gè)動(dòng)畫(huà)(其實(shí)就是插入了delay的parrllel)
delay生成一個(gè)延遲時(shí)間(基于timing的delay參數(shù)生成)

例3

getInitialState() {
    return (
        anim: [1,2,3].map(() => new Animated.Value(0)) // 初始化3個(gè)值
    );
},
 
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,
                    });
                })
            )), // 三個(gè)view滾到右邊再還原,每個(gè)動(dòng)作間隔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
            }))) // 同時(shí)回到原位置
        ]
    ).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}個(gè)View</Text>
 
            </Animated.View>
        );
    });
    return <View style={styles.container}>
               <Text>sequence/delay/stagger/parallel演示</Text>
               {views}
           </View>;
}


Spring/Decay/Timing
前面的幾個(gè)動(dòng)畫(huà)都是基于時(shí)間實(shí)現(xiàn)的,事實(shí)上,在日常的手勢(shì)操作中,基于時(shí)間的動(dòng)畫(huà)往往難以滿(mǎn)足復(fù)雜的交互動(dòng)畫(huà)。對(duì)此,RN還提供了另外兩種動(dòng)畫(huà)模式。
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模型來(lái)源于origami,一款F家自制的動(dòng)畫(huà)原型設(shè)計(jì)工具,而bounciness與speed則是傳統(tǒng)的彈簧模型參數(shù)。
Track && Event
RN動(dòng)畫(huà)支持跟蹤功能,這也是日常交互中很常見(jiàn)的需求,比如跟蹤用戶(hù)的手勢(shì)變化,跟蹤另一個(gè)動(dòng)畫(huà)。而跟蹤的用法也很簡(jiǎn)單,只需要指定toValue到另一個(gè)Animated.Value就可以了。 交互動(dòng)畫(huà)需要跟蹤用戶(hù)的手勢(shì)操作,Animated也很貼心地提供了事件接口的封裝,示例:

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

在官方的demo上改了一下,加了一張費(fèi)玉污的圖,效果圖如下 代碼太長(zhǎng),就不貼出來(lái)了,可以參考官方Github代碼
[圖片上傳中。。。(4)]
動(dòng)畫(huà)循環(huán)
Animated的start方法是支持回調(diào)函數(shù)的,在動(dòng)畫(huà)或某個(gè)流程結(jié)束的時(shí)候執(zhí)行,這樣子就可以很簡(jiǎn)單地實(shí)現(xiàn)循環(huán)動(dòng)畫(huà)了。

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,接下來(lái)我們來(lái)了解一下這些API是如何設(shè)計(jì)出來(lái)的。
聲明: 以下內(nèi)容參考自Animated原作者的分享視頻
首先,從React的生命周期來(lái)編程的話,一個(gè)動(dòng)畫(huà)大概是這樣子寫(xiě):

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

只需要通過(guò)requestAnimationFrame調(diào)用onChange,輸入對(duì)應(yīng)的value,動(dòng)畫(huà)就簡(jiǎn)單粗暴地跑起來(lái)了????,全劇終。
然而事實(shí)總是沒(méi)那么簡(jiǎn)單,問(wèn)題在哪?
我們看到,上述動(dòng)畫(huà)基本是以毫秒級(jí)的頻率在調(diào)用setState,而React的每次setState都會(huì)重新調(diào)用render方法,并切遍歷子元素進(jìn)行渲染,即使有Dom Diff也可能扛不住這么大的計(jì)算量和UI渲染。


那么該如何優(yōu)化呢?
關(guān)鍵詞:

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

ShouldComponentUpdate
學(xué)過(guò)React的都知道,ShouldComponentUpdate是性能優(yōu)化利器,只需要在子組件的shouldComponentUpdate返回false,分分鐘渲染性能爆表。
[圖片上傳中。。。(7)]
然而并非所有的子元素都是一成不變的,粗暴地返回false的話子元素就變成一灘死水了。而且組件間應(yīng)該是獨(dú)立的,子組件很可能是其他人寫(xiě)的,父元素不能依賴(lài)于子元素的實(shí)現(xiàn)。
<StaticContainer>(靜態(tài)容器)
這時(shí)候可以考慮封裝一個(gè)容器,管理ShouldCompontUpdate,如圖示:
[圖片上傳中。。。(8)]
小明和老王再也不用關(guān)心父元素的動(dòng)畫(huà)實(shí)現(xiàn)啦。
一個(gè)簡(jiǎn)單的<StaticContainer>實(shí)現(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時(shí),React通過(guò)DOM Diff就不再渲染子元素了。
上面的方法都有弊端,就是條件競(jìng)爭(zhēng)。當(dāng)動(dòng)畫(huà)在進(jìn)行的時(shí)候,子元素恰好獲得了新的state,而這時(shí)候動(dòng)畫(huà)無(wú)視了這個(gè)更新,最后就會(huì)導(dǎo)致?tīng)顟B(tài)不一致,或者動(dòng)畫(huà)結(jié)束的時(shí)候子元素發(fā)生了閃動(dòng),這些都是影響用戶(hù)操作的問(wèn)題。
Raw DOM Mutation 原生DOM操作
剛剛都是在React的生命周期里實(shí)現(xiàn)動(dòng)畫(huà),事實(shí)上,我們只想要變更這個(gè)元素的left值,并不希望各種重新渲染、DOM DIFF等等發(fā)生。
“React,我知道自己要干啥,你一邊涼快去“
如果我們跳出這個(gè)生命周期,直接找到元素進(jìn)行變更,是不是更簡(jiǎn)單呢?


簡(jiǎn)單易懂,性能彪悍,有木有?!
然而弊端也很明顯,比如這個(gè)組件unmount之后,動(dòng)畫(huà)就報(bào)錯(cuò)了。
Uncaught Exception: Cannot call ‘style’ of null
而且這種方法照樣避不開(kāi)條件競(jìng)爭(zhēng)——?jiǎng)赢?huà)值改變的時(shí)候,有可能發(fā)生setState之后,left又回到初始值之類(lèi)的情況。
再者,我們使用React,就是因?yàn)椴幌肴リP(guān)心dom元素的操作,而是交給React管理,直接使用Dom操作顯然違背了初衷。
↑↑↓↓←→←→BA (秘籍)
嘮叨了這么多,這也不行,那也不行,什么才是真理?
我們既想要原生DOM操作的高性能,又想要React完善的生命周期管理,如何把兩者優(yōu)勢(shì)結(jié)合到一起呢?答案就是Data Binding(數(shù)據(jù)綁定)

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

首先,需要實(shí)現(xiàn)一個(gè)具有數(shù)據(jù)綁定功能的類(lèi)Animated.Value,提供setValueonChange等接口。 其次,由于原生的組件并不能識(shí)別Value,需要將動(dòng)畫(huà)元素用Animated包裹起來(lái),在內(nèi)部處理數(shù)據(jù)變更與DOM操作。
一個(gè)簡(jiǎn)單的動(dòng)畫(huà)組件實(shí)現(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';
        });
        
        // 將動(dòng)畫(huà)值解析為普通數(shù)值傳給原生div
        this._props = React.addons.update(
            nextProps,
            {style:{left:{$set: nextProps.style.left.getValue()}}}
        );
    },
    render() {
        return <div ...{this._props} />;
    }
}

代碼很簡(jiǎn)短,做的事情有:
遍歷傳入的props,查找是否有Animated.Value的實(shí)例,并綁定相應(yīng)的DOM操作。
每次props變更或者組件unmount的時(shí)候,停止監(jiān)聽(tīng)數(shù)據(jù)綁定事件,避免了條件競(jìng)爭(zhēng)和內(nèi)存泄露問(wèn)題。
將初始傳入的Animated.Value值逐個(gè)轉(zhuǎn)化為普通數(shù)值,再交給原生的React組件進(jìn)行渲染。

綜上,通過(guò)封裝一個(gè)Animated的元素,內(nèi)部通過(guò)數(shù)據(jù)綁定和DOM操作變更元素,結(jié)合React的生命周期完善內(nèi)存管理,解決條件競(jìng)爭(zhēng)問(wèn)題,對(duì)外表現(xiàn)則與原生組件相同,實(shí)現(xiàn)了高效流暢的動(dòng)畫(huà)效果。
讀到這里,應(yīng)該知道為什么ImageText等做動(dòng)畫(huà)一定要使用Animated加持過(guò)的元素了吧?
參考資料

React Addons Update
React Component Lifecycle
Christopher Chedeau – Animated


好書(shū)推薦 React Native開(kāi)發(fā)指南》

原創(chuàng)文章轉(zhuǎn)載請(qǐng)注明:
轉(zhuǎn)載自AlloyTeam:http://www.alloyteam.com/2016/01/reactnative-animated/

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

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,111評(píng)論 25 709
  • 最近ReactNative(以下簡(jiǎn)稱(chēng)RN)在前端的熱度越來(lái)越高,不少同學(xué)開(kāi)始在業(yè)務(wù)中嘗試使用RN,這里著重介紹一下...
    街角仰望閱讀 5,134評(píng)論 6 6
  • 原教程內(nèi)容詳見(jiàn)精益 React 學(xué)習(xí)指南,這只是我在學(xué)習(xí)過(guò)程中的一些閱讀筆記,個(gè)人覺(jué)得該教程講解深入淺出,比目前大...
    leonaxiong閱讀 2,944評(píng)論 1 18
  • It's a common pattern in React to wrap a component in an ...
    jplyue閱讀 3,407評(píng)論 0 2
  • 叫花子都知道是要飯的,但還有其它稱(chēng)謂,人們也把這些人稱(chēng)作叫討飯的、拾荒者、乞丐等。這些人給人感覺(jué)或印象是窮人、可憐...
    西安吳墨閱讀 1,027評(píng)論 0 0

友情鏈接更多精彩內(nèi)容