详解React Native动画
JorjaDeMais
8年前
<p>大多数情况下,在 React Native 中创建动画是推荐使用 Animated API 的,其提供了三个主要的方法用于创建动画:</p> <ol> <li><strong>Animated.timing()</strong> -- 推动一个值按照一个过渡曲线而随时间变化。 Easing 模块定义了很多缓冲曲线函数。</li> <li><strong>Animated.decay()</strong> -- 推动一个值以一个初始的速度和一个衰减系数逐渐变为0。</li> <li><strong>Animated.spring()</strong> -- 产生一个基于 Rebound 和 Origami 实现的Spring动画。它会在 toValue 值更新的同时跟踪当前的速度状态,以确保动画连贯。</li> </ol> <p>译者注:React Native(0.37) 目前只支持Animated.Text/Animated.View/Animated.Image</p> <p>以我的经验来看, <strong>Animated.timing()</strong> 和 <strong>Animated.spring()</strong> 在创建动画方面是非常有效的。</p> <p>除了这三个创建动画的方法,对于每个独立的方法都有三种调用该动画的方式:</p> <ol> <li><strong>Animated.parallel()</strong> -- 同时开始一个动画数组里的全部动画。默认情况下,如果有任何一个动画停止了,其余的也会被停止。你可以通过 stopTogether 选项来改变这个效果。</li> <li><strong>Animated.sequence()</strong> -- 按顺序执行一个动画数组里的动画,等待一个完成后再执行下一个。如果当前的动画被中止,后面的动画则不会继续执行。</li> <li><strong>Animated.stagger()</strong> -- 一个动画数组,里面的动画有可能会同时执行(重叠),不过会以指定的延迟来开始。</li> </ol> <h2><strong>1. Animated.timing()</strong></h2> <p style="text-align:center"><img src="https://simg.open-open.com/show/bfab3b47cf89b1214386d4b3b0b1d02b.gif"></p> <p>第一个要创建的动画是使用 Animated.timing 创建的旋转动画。</p> <pre> <code class="language-javascript">// Example implementation: Animated.timing( someValue, { toValue: number, duration: number, easing: easingFunction, delay: number } )</code></pre> <p>这种方式常用于创建需要loading指示的动画,在我使用React Native的项目中,这也是创建动画最有效的方式。这个理念也可以用于其它诸如按比例放大和缩小类型的指示动画。</p> <p>开始之前,我们需要创建一个新的React Native 项目或者一个空的React Native项目。创建新项目之前,需要输入 react-native init 来初始化一个项目,并切换到该项目目录:</p> <pre> <code class="language-javascript">react-native init animations cd animations</code></pre> <p>然后打开 index.android.js 和 index.ios.js 。</p> <p>现在已经创建了一个新项目,则第一件事是在已经引入的 <strong>View</strong> 之后从 react native 中引入 <strong>Animated</strong> , <strong>Image</strong> 和 <strong>Easing</strong> :</p> <pre> <code class="language-javascript">import { AppRegistry, StyleSheet, Text, View, Animated, Image, Easing } from 'react-native'</code></pre> <p>Animated 是我们将用于创建动画的库,和React Native交互的载体。</p> <p>Image用于在UI中显示图片。</p> <p>Easing也是用React Native创建动画的载体,它允许我们使用已经定义好的各种缓冲函数,例如: <strong>linear</strong> , <strong>ease</strong> , <strong>quad</strong> , <strong>cubic</strong> , <strong>sin</strong> , <strong>elastic</strong> , <strong>bounce</strong> , <strong>back</strong> , <strong>bezier</strong> , <strong>in</strong> , <strong>out</strong> , <strong>inout</strong> 。由于有直线运动,我们将使用 <strong>linear</strong> 。在这节(阅读)完成之后,对于实现直线运动的动画,你或许会有更好的实现方式。</p> <p>接下来,需要在构造函数中初始化一个带动画属性的值用于旋转动画的初始值:</p> <pre> <code class="language-javascript">constructor () { super() this.spinValue = new Animated.Value(0) }</code></pre> <p>我们使用 ** Animated.Value** 声明了一个 <strong>spinValue</strong> 变量,并传了一个 <strong>0</strong> 作为初始值。</p> <p>然后创建了一个名为 spin 的方法,并在 componentDidMount 中调用它,目的是在 app 加载之后运行动画:</p> <pre> <code class="language-javascript">componentDidMount () { this.spin() } spin () { this.spinValue.setValue(0) Animated.timing( this.spinValue, { toValue: 1, duration: 4000, easing: Easing.linear } ).start(() => this.spin()) }</code></pre> <p>spin() 方法的作用如下:</p> <ol> <li>将 <strong>this.spinValue</strong> 重置成 0</li> <li>调用 <strong>Animated.timing</strong> ,并驱动 this.spinValue 的值以 Easing.linear 的动画方式在 4000 毫秒从 0 变成 1。 <strong>Animated.timing</strong> 需要两个参数,一个要变化的值(本文中是 <strong>this.spinValue</strong> ) 和一个可配置对象。这个配置对象有四个属性: <strong>toValue</strong> (终值)、 <strong>duration</strong> (一次动画的持续时间)、 <strong>easing</strong> (缓存函数)和 <strong>delay</strong> (延迟执行的时间)</li> <li>调用 <strong>start()</strong> ,并将 <strong>this.spin</strong> 作为回调传递给 start ,它将在(一次)动画完成之后调用,这也是创建无穷动画的一种基本方式。 start() 需要一个完成回调,该回调在动画正常的运行完成之后会被调用,并有一个参数是 {finished: true} ,但如果动画是在它正常运行完成之前而被停止了(如:被手势动作或者其它动画中断),则回调函数的参数变为 {finished: false} 。</li> </ol> <p>译者注:如果在回调中将动画的初始值设置成其终值,该动画就不会再执行。如将 this.spinValue.setValue(0) 改为 this.spinValue.setValue(1),spin动画不会执行了</p> <p>现在方法已经创建好了,接下来就是在UI中渲染动画了。为了渲染动画,需要更新 render 方法:</p> <pre> <code class="language-javascript">render () { const spin = this.spinValue.interpolate({ inputRange: [0, 1], outputRange: ['0deg', '360deg'] }) return ( <View style={styles.container}> <Animated.Image style={{ width: 227, height: 200, transform: [{rotate: spin}] }} source={{uri: 'https://s3.amazonaws.com/media-p.slid.es/uploads/alexanderfarennikov/images/1198519/reactjs.png'}} /> </View> ) }</code></pre> <ol> <li>在 render 方法中,创建了一个 <strong>spin</strong> 变量,并调用了 <strong>this.spinValue</strong> 的 <strong>interpolate</strong> 方法。 <strong>interpolate</strong> 方法可以在任何一个 <strong>Animated.Value</strong> 返回的实例上调用,该方法会在属性更新之前插入一个新值,如将 0 1 映射到 1 10。在我们的demo中,利用 <strong>interpolate</strong> 方法将数值 0 1 映射到了 0deg 360deg。我们传递了 inputRange 和 outputRange 参数给 <strong>interpolate</strong> 方法,并分别赋值为 <strong>[0,1]</strong> 和 & <strong>[‘0deg’, ‘360deg’]</strong> 。</li> <li>我们返回了一个带 container 样式值的 <strong>View</strong> 和 带 <strong>height</strong> , <strong>width</strong> 和 <strong>transform</strong> 属性的 <strong>Animated.Image</strong> ,并将 <strong>spin</strong> 的值赋给 transform 的 <strong>rotate</strong> 属性,这也是动画发生的地方:</li> </ol> <pre> <code class="language-javascript">transform: [{rotate: spin}]</code></pre> <p>最后,在 container 样式中,使所有元素都居中:</p> <pre> <code class="language-javascript">const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center' } })</code></pre> <p>这个示例动画的最终代码在 这里 。</p> <h3><strong>关于Easing</strong></h3> <p>这是 Easing 模块的源码链接,从源码中可以看到每一个 easing 方法。</p> <p>我创建了另外一个示例项目,里面包含了大部分 easing 动画的实现,可以供你参考,链接在 这里 。(项目的运行截图)依据在下面:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/ea61449c9bb145a4e1472d515f0daa42.png"></p> <h2><strong>2. Animated.timing 示例</strong></h2> <p style="text-align:center"><img src="https://simg.open-open.com/show/459a4637da7d26fc9c12d40d01a0cbff.gif"></p> <p>上文已经说过了 <strong>Animated.timing</strong> 的基础知识,这一节会例举更多使用 <strong>Animated.timing</strong> 与 <strong>interpolate</strong> 结合实现的动画示例。</p> <p>下一个示例中,会声明一个单一的动画值, this.animatedValue ,然后将该值和 interpolate 一起使用来驱动下列属性值的变化来创建复杂动画:</p> <ol> <li>marginLeft</li> <li>opacity</li> <li>fontSize</li> <li>rotateX</li> </ol> <p>在开始之前,可以创建一个新分支或者清除上一个项目的旧代码。</p> <p>第一件事是在构造函数中初始化一个需要用到的动画属性值:</p> <pre> <code class="language-javascript">constructor () { super() this.animatedValue = new Animated.Value(0) }</code></pre> <p>接下来,创建一个名为 animate 的方法,并在 <strong>componentDidMount()</strong> 中调用该方法:</p> <pre> <code class="language-javascript">componentDidMount () { this.animate() } animate () { this.animatedValue.setValue(0) Animated.timing( this.animatedValue, { toValue: 1, duration: 2000, easing: Easing.linear } ).start(() => this.animate()) }</code></pre> <p>在 render 方法中,我们创建 5 个不同的插值变量:</p> <pre> <code class="language-javascript">render () { const marginLeft = this.animatedValue.interpolate({ inputRange: [0, 1], outputRange: [0, 300] }) const opacity = this.animatedValue.interpolate({ inputRange: [0, 0.5, 1], outputRange: [0, 1, 0] }) const movingMargin = this.animatedValue.interpolate({ inputRange: [0, 0.5, 1], outputRange: [0, 300, 0] }) const textSize = this.animatedValue.interpolate({ inputRange: [0, 0.5, 1], outputRange: [18, 32, 18] }) const rotateX = this.animatedValue.interpolate({ inputRange: [0, 0.5, 1], outputRange: ['0deg', '180deg', '0deg'] }) ... }</code></pre> <p>interpolate是一个很强大的方法,允许我们用多种方式来使用单一的动画属性值: <strong>this.animatedValue</strong> 。因为 this.animatedValue 只是简单的从0变到1,因而我们能将这个值插入到 opacity、margins、text sizes 和 rotation 等样式属性中。</p> <p>最后,返回实现了上述变量的 Animated.View 和 Animated.Text 组件:</p> <pre> <code class="language-javascript">return ( <View style={styles.container}> <Animated.View style={{ marginLeft, height: 30, width: 40, backgroundColor: 'red'}} /> <Animated.View style={{ opacity, marginTop: 10, height: 30, width: 40, backgroundColor: 'blue'}} /> <Animated.View style={{ marginLeft: movingMargin, marginTop: 10, height: 30, width: 40, backgroundColor: 'orange'}} /> <Animated.Text style={{ fontSize: textSize, marginTop: 10, color: 'green'}} > Animated Text! </Animated.Text> <Animated.View style={{ transform: [{rotateX}], marginTop: 50, height: 30, width: 40, backgroundColor: 'black'}}> <Text style={{color: 'white'}}>Hello from TransformX</Text> </Animated.View> </View> )</code></pre> <p>当然,也需要更新下 container 样式:</p> <pre> <code class="language-javascript">const styles = StyleSheet.create({ container: { flex: 1, paddingTop: 150 } })</code></pre> <h2><strong>3. Animated.spring()</strong></h2> <p style="text-align:center"><img src="https://simg.open-open.com/show/50ea72025d5c320bd35c7a6f17a739e9.gif"></p> <p>接下来,我们将会使用 <strong>Animated.spring()</strong> 方法创建动画。</p> <pre> <code class="language-javascript">// Example implementation: Animated.spring( someValue, { toValue: number, friction: number } )</code></pre> <p>我们继续使用上一个项目,并只需要更新少量代码就行。在构造函数中,创建一个 <strong>springValue</strong> 变量,初始化其值为0.3:</p> <pre> <code class="language-javascript">constructor () { super() this.springValue = new Animated.Value(0.3) }</code></pre> <p>然后,删除 animated 方法和 componentDidMount 方法,创建一个新的 spring 方法:</p> <pre> <code class="language-javascript">spring () { this.springValue.setValue(0.3) Animated.spring( this.springValue, { toValue: 1, friction: 1 } ).start() }</code></pre> <ol> <li>将 springValue 的值重置为 0.3</li> <li>调用 Animated.spring 方法,并传递两个参数:一个要变化的值和一个可配置对象。可配置对象的属性可以是下列的任何值: <strong>toValue</strong> (number), <strong>overshootClamping</strong> (boolean), <strong>restDisplacementThreshold</strong> (number), <strong>restSpeedThreshold</strong> (number), <strong>velocity</strong> (number), <strong>bounciness</strong> (number), <strong>speed</strong> (number), <strong>tension</strong> (number), 和 <strong>friction</strong> (number)。除了 <strong>toValue</strong> 是必须的,其他值都是可选的,但 <strong>friction</strong> 和 <strong>tension</strong> 能帮你更好地控制 spring 动画。</li> <li>调用 start() 启动动画</li> </ol> <p>动画已经设置好了,我们将其放在 View 的click事件中,动画元素依然是之前使用过的 React logo 图片:</p> <pre> <code class="language-javascript"><View style={styles.container}> <Text style={{marginBottom: 100}} onPress={this.spring.bind(this)}>Spring</Text> <Animated.Image style={{ width: 227, height: 200, transform: [{scale: this.springValue}] }} source={{uri: 'https://s3.amazonaws.com/media-p.slid.es/uploads/alexanderfarennikov/images/1198519/reactjs.png'}}/> </View></code></pre> <ol> <li>我们返回一个Text组件,并将 <strong>spring()</strong> 添加到组件的onPress事件中</li> <li>我们返回一个 Animated.Image ,并为其 scale 属性添加 this.springValue</li> </ol> <h2><strong>4. Animated.parallel()</strong></h2> <p style="text-align:center"><img src="https://simg.open-open.com/show/c333d4825e836777d8de9bac60450ee1.gif"></p> <p>Animated.parallel()会同时开始一个动画数组里的全部动画。</p> <p>先看一下这个api是怎么调用的:</p> <pre> <code class="language-javascript">// API Animated.parallel(arrayOfAnimations) // In use: Animated.parallel([ Animated.spring( animatedValue, { //config options } ), Animated.timing( animatedValue2, { //config options } ) ])</code></pre> <p>开始之前,我们先直接创建三个我们需要的动画属性值:</p> <pre> <code class="language-javascript">constructor () { super() this.animatedValue1 = new Animated.Value(0) this.animatedValue2 = new Animated.Value(0) this.animatedValue3 = new Animated.Value(0) }</code></pre> <p>然后,创建一个 animate 方法并在 <strong>componendDidMount()</strong> 中调用它:</p> <pre> <code class="language-javascript">componentDidMount () { this.animate() } animate () { this.animatedValue1.setValue(0) this.animatedValue2.setValue(0) this.animatedValue3.setValue(0) const createAnimation = function (value, duration, easing, delay = 0) { return Animated.timing( value, { toValue: 1, duration, easing, delay } ) } Animated.parallel([ createAnimation(this.animatedValue1, 2000, Easing.ease), createAnimation(this.animatedValue2, 1000, Easing.ease, 1000), createAnimation(this.animatedValue3, 1000, Easing.ease, 2000) ]).start() }</code></pre> <p>在 animate 方法中,我们将三个动画属性值重置为0。此外,还创建了一个 <strong>createAnimation</strong> 方法,该方法接受四个参数:value, duration, easing, delay(默认值是0),返回一个新的动画。</p> <p>然后,调用 Animated.parallel() ,并将三个使用 createAnimation 创建的动画作为参数传递给它。</p> <p>在 render 方法中,我们需要设置插值:</p> <pre> <code class="language-javascript">render () { const scaleText = this.animatedValue1.interpolate({ inputRange: [0, 1], outputRange: [0.5, 2] }) const spinText = this.animatedValue2.interpolate({ inputRange: [0, 1], outputRange: ['0deg', '720deg'] }) const introButton = this.animatedValue3.interpolate({ inputRange: [0, 1], outputRange: [-100, 400] }) ... }</code></pre> <ol> <li><strong>scaleText</strong> -- 插值的输出范围是从0.5到2,我们会用这个值对文本按0.5到2的比例进行缩放</li> <li><strong>spinText</strong> -- 插值的输出范围是 0 degrees 到 720 degrees,即将元素旋转两周</li> <li><strong>introButton</strong> -- 插值的输出范围是 -100 到 400,该值会用于 View 的 margin 属性</li> </ol> <p>最后,我们用一个主 View 包裹三个 Animated.Views:</p> <pre> <code class="language-javascript"><View style={[styles.container]}> <Animated.View style={{ transform: [{scale: scaleText}] }}> <Text>Welcome</Text> </Animated.View> <Animated.View style={{ marginTop: 20, transform: [{rotate: spinText}] }}> <Text style={{fontSize: 20}}> to the App! </Text> </Animated.View> <Animated.View style={{top: introButton, position: 'absolute'}}> <TouchableHighlight onPress={this.animate.bind(this)} style={styles.button}> <Text style={{color: 'white', fontSize: 20}}> Click Here To Start </Text> </TouchableHighlight> </Animated.View> </View></code></pre> <p>当 animate() 被调用时,三个动画会同时执行。</p> <h2><strong>5. Animated.Sequence()</strong></h2> <p style="text-align:center"><img src="https://simg.open-open.com/show/b6ab214c0c3c04e42f2a9f37ffc379ee.gif"></p> <p>先看一下这个api是怎么调用的:</p> <pre> <code class="language-javascript">// API Animated.sequence(arrayOfAnimations) // In use Animated.sequence([ Animated.timing( animatedValue, { //config options } ), Animated.spring( animatedValue2, { //config options } ) ])</code></pre> <p>和 <strong>Animated.parallel()</strong> 一样, <strong>Animated.sequence()</strong> 接受一个动画数组。但不同的是, <strong>Animated.sequence()</strong> 是按顺序执行一个动画数组里的动画,等待一个完成后再执行下一个。</p> <pre> <code class="language-javascript">import React, { Component } from 'react'; import { AppRegistry, StyleSheet, Text, View, Animated } from 'react-native' const arr = [] for (var i = 0; i < 500; i++) { arr.push(i) } class animations extends Component { constructor () { super() this.animatedValue = [] arr.forEach((value) => { this.animatedValue[value] = new Animated.Value(0) }) } componentDidMount () { this.animate() } animate () { const animations = arr.map((item) => { return Animated.timing( this.animatedValue[item], { toValue: 1, duration: 50 } ) }) Animated.sequence(animations).start() } render () { const animations = arr.map((a, i) => { return <Animated.View key={i} style={{opacity: this.animatedValue[a], height: 20, width: 20, backgroundColor: 'red', marginLeft: 3, marginTop: 3}} /> }) return ( <View style={styles.container}> {animations} </View> ) } } const styles = StyleSheet.create({ container: { flex: 1, flexDirection: 'row', flexWrap: 'wrap' } }) AppRegistry.registerComponent('animations', () => animations);</code></pre> <p>由于 Animated.sequence() 和 Animated.parallel() 很相似,因而对 Animated.sequence() 就不多作介绍了。主要不同的一点是我们是使用循环创建 Animated.Values。</p> <h2><strong>6. Animated.Stagger()</strong></h2> <p>先看一下这个api是怎么调用的:</p> <pre> <code class="language-javascript">// API Animated.stagger(delay, arrayOfAnimations) // In use: Animated.stagger(1000, [ Animated.timing( animatedValue, { //config options } ), Animated.spring( animatedValue2, { //config options } ) ])</code></pre> <p>和 Animated.parallel() 和 Animated.sequence() 一样, Animated.Stagger 接受一个动画数组。但不同的是,Animated.Stagger 里面的动画有可能会同时执行(重叠),不过会以指定的延迟来开始。</p> <p>与上述两个动画主要的不同点是 Animated.Stagger 的第一个参数, delay 会被应用到每一个动画:</p> <pre> <code class="language-javascript">import React, { Component } from 'react'; import { AppRegistry, StyleSheet, Text, View, Animated } from 'react-native' const arr = [] for (var i = 0; i < 500; i++) { arr.push(i) } class animations extends Component { constructor () { super() this.animatedValue = [] arr.forEach((value) => { this.animatedValue[value] = new Animated.Value(0) }) } componentDidMount () { this.animate() } animate () { const animations = arr.map((item) => { return Animated.timing( this.animatedValue[item], { toValue: 1, duration: 4000 } ) }) Animated.stagger(10, animations).start() } render () { const animations = arr.map((a, i) => { return <Animated.View key={i} style={{opacity: this.animatedValue[a], height: 20, width: 20, backgroundColor: 'red', marginLeft: 3, marginTop: 3}} /> }) return ( <View style={styles.container}> {animations} </View> ) } } const styles = StyleSheet.create({ container: { flex: 1, flexDirection: 'row', flexWrap: 'wrap' } }) AppRegistry.registerComponent('SampleApp', () => animations);</code></pre> <h2><strong>参考</strong></h2> <p><a href="/misc/goto?guid=4959726747763360885" rel="nofollow,noindex">React Native Animations Using the Animated API</a></p> <p><a href="/misc/goto?guid=4959726747853727436" rel="nofollow,noindex">Animated Docs</a></p> <p> </p> <p>来自:https://github.com/dwqs/blog/issues/41</p> <p> </p>