1. 程式人生 > >ReactNative Animated動畫詳解

ReactNative Animated動畫詳解

最近ReactNative(以下簡稱RN)在前端的熱度越來越高,不少同學開始在業務中嘗試使用RN,這裡著重介紹一下RN中動畫的使用與實現原理。

使用篇

舉個簡單的栗子

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

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, // 目標值

            duration: 2500, // 動畫時間

            easing: Easing.linear // 緩動函式

        }).start();

    },

    render() {

        return (

            <Animated.View style={[styles.demo, {

                    opacity: this.state.fadeInOpacity

                }]}>

                <Text style={styles.text}>悄悄的,我出現了</Text>

            </Animated.View>

        );

    }

});

var styles = StyleSheet.create({

    demo: {

        flex: 1,

        alignItems: 'center',

        justifyContent: 'center',

        backgroundColor: 'white',

    },

    text: {

        fontSize: 30

    }

});

demo1

是不是很簡單易懂<(▰˘◡˘▰)> 和JQuery的Animation用法很類似。

步驟拆解

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

  1. 使用基本的Animated元件,如Animated.View Animated.Image Animated.Text (重要!不加Animated的後果就是一個看不懂的報錯,然後查半天動畫引數,最後懷疑人生
  2. 使用Animated.Value設定一個或多個初始化值(透明度,位置等等)。
  3. 將初始化值繫結到動畫目標的屬性上(如style)
  4. 通過Animated.timing等函式設定動畫引數
  5. 呼叫start啟動動畫。

栗子敢再複雜一點嗎?

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

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]

                })

            }}>我騎著七彩祥雲出現了??</Animated.Text>

            </Animated.View>

    );

}

注意到我們給文字區域加上了字型增大的動畫效果,相應地,也要修改Text為Animated.Text

demo2

強大的interpolate

上面的栗子使用了interpolate函式,也就是插值函式。這個函式很強大,實現了數值大小、單位的對映轉換,比如

1

2

3

4

{  

    inputRange: [0,1],

    outPutRange: ['0deg','180deg']

}

當setValue(0.5)時,會自動對映成90deg。 inputRange並不侷限於[0,1]區間,可以畫出多段。 interpolate一般用於多個動畫共用一個Animated.Value,只需要在每個屬性裡面對映好對應的值,就可以用一個變數控制多個動畫。 事實上,上例中的fadeInOpacityfontSizerotation用一個變數來宣告就可以了。(那你寫那麼多變數逗我嗎(╯‵□′)╯︵┻━┻) (因為我要強行使用parallel ┬─┬ ノ( ' – 'ノ))

流程控制

在剛才的栗子中,我們使用了Parallel來實現多個動畫的並行渲染,其它用於流程控制的API還有:

  • sequence接受一系列動畫陣列為引數,並依次執行
  • stagger接受一系列動畫陣列和一個延遲時間,按照序列,每隔一個延遲時間後執行下一個動畫(其實就是插入了delay的parrllel)
  • delay生成一個延遲時間(基於timing的delay引數生成)

例3

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

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>;

}

demo3

Spring/Decay/Timing

前面的幾個動畫都是基於時間實現的,事實上,在日常的手勢操作中,基於時間的動畫往往難以滿足複雜的互動動畫。對此,RN還提供了另外兩種動畫模式。

  • Spring 彈簧效果
    • friction 摩擦係數,預設40
    • tension 張力系數,預設7
    • bounciness
    • speed
  • Decay 衰變效果
    • velocity 初速率
    • deceleration 衰減係數 預設0.997

Spring支援 friction與tension 或者 bounciness與speed 兩種組合模式,這兩種模式不能並存。 其中friction與tension模型來源於origami,一款F家自制的動畫原型設計工具,而bounciness與speed則是傳統的彈簧模型引數。

Track && Event

RN動畫支援跟蹤功能,這也是日常互動中很常見的需求,比如跟蹤使用者的手勢變化,跟蹤另一個動畫。而跟蹤的用法也很簡單,只需要指定toValue到另一個Animated.Value就可以了。 互動動畫需要跟蹤使用者的手勢操作,Animated也很貼心地提供了事件介面的封裝,示例:

1

2

3

4

// Animated.event 封裝手勢事件等值對映到對應的Animated.Value

onPanResponderMove: Animated.event(

    [null, {dx: this.state.x, dy: this.state.y}] // map gesture to leader

)

在官方的demo上改了一下,加了一張費玉汙的圖,效果圖如下 程式碼太長,就不貼出來了,可以參考官方Github程式碼

demo4

動畫迴圈

Animated的start方法是支援回撥函式的,在動畫或某個流程結束的時候執行,這樣子就可以很簡單地實現迴圈動畫了。

1

2

3

4

5

6

7

8

startAnimation() {

    this.state.rotateValue.setValue(0);

    Animated.timing(this.state.rotateValue, {

        toValue: 1,

        duration: 800,

        easing: Easing.linear

    }).start(() => this.startAnimation());

}

demo5

是不是很魔性?[doge]

原理篇

首先感謝能看到這裡的小夥伴們:)

在上面的文章中,我們已經基本掌握了RN Animated的各種常用API,接下來我們來了解一下這些API是如何設計出來的。

宣告: 以下內容參考自Animated原作者的分享視訊

首先,從React的生命週期來程式設計的話,一個動畫大概是這樣子寫:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

getInitialState() {

    return {left: 0};

}

render(){

    return (

        <div style={{left: this.state.left}}>

            <Child />

        </div>

    );

}

onChange(value) {

    this.setState({left: value});

}

只需要通過requestAnimationFrame呼叫onChange,輸入對應的value,動畫就簡單粗暴地跑起來了。◕‿◕,全劇終。

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

我們看到,上述動畫基本是以毫秒級的頻率在呼叫setState,而React的每次setState都會重新呼叫render方法,並切遍歷子元素進行渲染,即使有Dom Diff也可能扛不住這麼大的計算量和UI渲染。

demo6

那麼該如何優化呢?

  • 關鍵詞:
    • ShouldComponentUpdate
    • <StaticContainer>(靜態容器)
    • Element Caching (元素快取)
    • Raw DOM Mutation (原生DOM操作)
    • ↑↑↓↓←→←→BA (祕籍)

ShouldComponentUpdate

學過React的都知道,ShouldComponentUpdate是效能優化利器,只需要在子元件的shouldComponentUpdate返回false,分分鐘渲染效能爆表。

demo7

然而並非所有的子元素都是一成不變的,粗暴地返回false的話子元素就變成一灘死水了。而且元件間應該是獨立的,子元件很可能是其他人寫的,父元素不能依賴於子元素的實現。

<StaticContainer>(靜態容器)

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

demo8

小明和老王再也不用關心父元素的動畫實現啦。

一個簡單的\<StaticContainer\>實現如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

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 快取元素

還有另一種思路優化子元素的渲染,那就是快取子元素的渲染結果到局地變數。

1

2

3

4

5

6

7

8

render(){

    this._child = this._child || <ExpensiveChild />;

    return (

        <div style={{left:this.state.left}}>

            {this._child}

        </div>

    );

}

快取之後,每次setState時,React通過DOM Diff就不再渲染子元素了。

上面的方法都有弊端,就是條件競爭。當動畫在進行的時候,子元素恰好獲得了新的state,而這時候動畫無視了這個更新,最後就會導致狀態不一致,或者動畫結束的時候子元素髮生了閃動,這些都是影響使用者操作的問題。

Raw DOM Mutation 原生DOM操作

剛剛都是在React的生命週期裡實現動畫,事實上,我們只想要變更這個元素的left值,並不希望各種重新渲染、DOM DIFF等等發生。

“React,我知道自己要幹啥,你一邊涼快去“

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

demo9

簡單易懂,效能彪悍,有木有?!

然而弊端也很明顯,比如這個元件unmount之後,動畫就報錯了。

Uncaught Exception: Cannot call ‘style’ of null

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

再者,我們使用React,就是因為不想去關心dom元素的操作,而是交給React管理,直接使用Dom操作顯然違背了初衷。

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

嘮叨了這麼多,這也不行,那也不行,什麼才是真理?

我們既想要原生DOM操作的高效能,又想要React完善的生命週期管理,如何把兩者優勢結合到一起呢?答案就是Data Binding(資料繫結)

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

render(){

    return(

        <Animated.div style={{left: this.state.left}}>

             <ExpensiveChild />

        </Animated.div>

    );

}

getInitialState(){

    return {left: new Animated.Value(0)}; // 實現了資料繫結的類

}

onUpdate(value){

    this.state.left.setValue(value); // 不是setState

}

首先,需要實現一個具有資料繫結功能的類Animated.Value,提供setValueonChange等介面。 其次,由於原生的元件並不能識別Value,需要將動畫元素用Animated包裹起來,在內部處理資料變更與DOM操作。

一個簡單的動畫元件實現如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

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';

        });

        // 將動畫值解析為普通數值傳給原生div

        this._props = React.addons.update(

            nextProps,

            {style:{left:{$set: nextProps.style.left.getValue()}}}

        );

    },

    render() {

        return <div ...{this._props} />;

    }

}

程式碼很簡短,做的事情有:

  1. 遍歷傳入的props,查詢是否有Animated.Value的例項,並繫結相應的DOM操作。
  2. 每次props變更或者元件unmount的時候,停止監聽資料繫結事件,避免了條件競爭和記憶體洩露問題。
  3. 將初始傳入的Animated.Value值逐個轉化為普通數值,再交給原生的React元件進行渲染。

綜上,通過封裝一個Animated的元素,內部通過資料繫結和DOM操作變更元素,結合React的生命週期完善記憶體管理,解決條件競爭問題,對外表現則與原生元件相同,實現了高效流暢的動畫效果。

讀到這裡,應該知道為什麼ImageText等做動畫一定要使用Animated加持過的元素了吧?

1846.743

原創文章轉載請註明: