在线文档教程
React native

Animations

Animations

动画对于创造出色的用户体验非常重要。固定物体在开始移动时必须克服惯性。运动中的物体有动力,很少立即停下来。动画可让您在界面中传达身体上可信的动作。

React Native提供了两个互补的动画系统:Animated用于精确和交互式控制特定值以及LayoutAnimation动画全局布局事务。

Animated API

AnimatedAPI旨在以非常高效的方式简明扼要地表达各种有趣的动画和交互模式。Animated侧重于输入和输出之间的声明性关系,以及两者之间的可配置变换,以及简单start/ stop方法来控制基于时间的动画执行。

Animated出口4种动画组件类型:ViewTextImage,和ScrollView,但你也可以自己用创建Animated.createAnimatedComponent()

例如,安装时淡入的容器视图可能如下所示:

import React from 'react'; import { Animated, Text, View } from 'react-native'; class FadeInView extends React.Component { state = { fadeAnim: new Animated.Value(0), // Initial value for opacity: 0 } componentDidMount() { Animated.timing( // Animate over time this.state.fadeAnim, // The animated value to drive { toValue: 1, // Animate to opacity: 1 (opaque) duration: 10000, // Make it take a while } ).start( // Starts the animation } render() { let { fadeAnim } = this.state; return ( <Animated.View // Special animatable View style={{ ...this.props.style, opacity: fadeAnim, // Bind opacity to animated value }} > {this.props.children} </Animated.View> } } // You can then use your `FadeInView` in place of a `View` in your components: export default class App extends React.Component { render() { return ( <View style={{flex: 1, alignItems: 'center', justifyContent: 'center'}}> <FadeInView style={{width: 250, height: 50, backgroundColor: 'powderblue'}}> <Text style={{fontSize: 28, textAlign: 'center', margin: 10}}>Fading in</Text> </FadeInView> </View> ) } }

让我们来分解这里发生的事情。在FadeInView构造函数中,一个新的Animated.Value调用fadeAnim被初始化为一部分stateView映射到此动画值的不透明属性。在幕后,提取数字值并用于设置不透明度。

当组件加载时,不透明度设置为0.然后,在fadeAnim动画值上启动缓动动画,该动画值将在每帧上更新其所有相关映射(在这种情况下,仅为不透明度),作为动画值最终值为1。

这是以比调用setState和重新渲染更快的优化方式完成的。

由于整个配置是声明性的,我们将能够实现进一步的优化,将配置序列化并在高优先级线程上运行动画。

配置动画

动画是非常可配置的。自定义和预定义的缓动函数,延迟,持续时间,衰减因子,弹簧常数等都可以根据动画的类型进行调整。

Animated提供了几种动画类型,最常用的一种Animated.timing()。它支持使用各种预定义的缓动功能之一随时间推移动画值,或者您可以使用自己的。缓动功能通常用于动画来传递对象的逐渐加速和减速。

默认情况下,timing将使用easeInOut曲线,将渐进加速传递到全速,并通过逐渐减速停止结束。您可以通过传递easing参数来指定不同的缓动函数。自定义duration甚至delay在动画开始之前也支持。

例如,如果我们想要在移动到最终位置之前稍微备份一个物体,并创建一个长度为2秒的动画:

Animated.timing( this.state.xPosition, { toValue: 100, easing: Easing.back, duration: 2000, } ).start(

查看AnimatedAPI参考的“配置动画”部分,了解有关内置动画支持的所有配置参数的更多信息。

组成动画

动画可以组合并按顺序或并行播放。连续动画可以在上一个动画结束后立即播放,或者可以在指定的延迟后开始播放。所述AnimatedAPI提供了几种方法,如sequence()delay(),其中的每一个简单地采取动画执行的阵列和自动呼叫start()/ stop()根据需要。

例如,以下动画惯性停止,然后在并行旋转时弹回:

Animated.sequence([ // decay, then spring to start and twirl Animated.decay(position, { // coast to a stop velocity: {x: gestureState.vx, y: gestureState.vy}, // velocity from gesture release deceleration: 0.997, }), Animated.parallel([ // after decay, in parallel: Animated.spring(position, { toValue: {x: 0, y: 0} // return to start }), Animated.timing(twirl, { // and twirl toValue: 360, }), ]), ]).start( // start the sequence group

如果一个动画停止或中断,则该组中的所有其他动画也会停止。Animated.parallel有一个stopTogether选项可以设置false为禁用此选项。

您可以在AnimatedAPI参考的“合成动画”部分找到合成方法的完整列表。

结合动画值

您可以通过添加,乘法,除法或模数来组合两个动画值,以创建新的动画值。

在某些情况下,动画值需要反转另一个动画值进行计算。一个例子是反转比例尺(2x - > 0.5x):

const a = Animated.Value(1 const b = Animated.divide(1, a Animated.spring(a, { toValue: 2, }).start(

插值

每个属性都可以先通过插值运行。插值将输入范围映射到输出范围,通常使用线性插值,但也支持缓动功能。默认情况下,它将推断超出给定范围的曲线,但也可以使曲线限制输出值。

将0-1范围转换为0-100范围的简单映射将是:

value.interpolate{ inputRange: [0, 1], outputRange: [0, 100], }

例如,您可能想要考虑Animated.Value从0到1,但将位置从150px设置为0px,并将不透明度从0设置为1.这可以通过style从上面的示例进行修改来轻松完成,如下所示:

style={{ opacity: this.state.fadeAnim, // Binds directly transform: [{ translateY: this.state.fadeAnim.interpolate{ inputRange: [0, 1], outputRange: [150, 0] // 0 : 150, 0.5 : 75, 1 : 0 }), }], }}

interpolate()支持多个范围段,这对定义死区和其他方便的技巧非常方便。例如,要获得-300处的否定关系,该关系在-100处变为0,然后在0处回到1,然后在100处回到零,然后在除此之外的所有区域都保持为0的死区,你可以这样做:

value.interpolate{ inputRange: [-300, -100, 0, 100, 101], outputRange: [300, 0, 1, 0, 0], }

这将映射如下:

Input | Output ------|------- -400| 450 -300| 300 -200| 150 -100| 0 -50| 0.5 0| 1 50| 0.5 100| 0 101| 0 200| 0

interpolate()还支持映射到字符串,允许您使用单位设置动画的颜色和值。例如,如果你想动画旋转,你可以这样做:

value.interpolate{ inputRange: [0, 360], outputRange: ['0deg', '360deg'] })

interpolate()还支持任意缓动功能,其中许多功能已在Easing模块中实现。interpolate()也有可配置的行为来推断outputRange。您可以通过设置设置的外推extrapolateextrapolateLeftextrapolateRight选项。默认值是,extend但可以clamp用来防止输出值超出outputRange

跟踪动态值

动画值也可以跟踪其他值。只需将toValue动画设置为另一个动画值而不是普通数字。例如,像在Android使用信使的一个“聊天头”动画可以与实施spring()寄托在另一个动画值,或者与timing()duration0刚性跟踪。它们也可以用插值组成:

Animated.spring(follower, {toValue: leader}).start( Animated.timing(opacity, { toValue: pan.x.interpolate{ inputRange: [0, 300], outputRange: [1, 0], }), }).start(

leaderfollower动画值将采用以下方式实现Animated.ValueXY()ValueXY是处理2D交互的便捷方式,例如平移或拖动。它是一个简单的包装,基本上包含两个Animated.Value实例和一些帮助函数,通过它们进行调用,从而在许多情况下ValueXY进行替换Value。它允许我们在上面的例子中跟踪x和y值。

跟踪手势

手势,如平移或滚动,以及其他事件可以直接映射到使用动画值Animated.event。这是通过结构化地图语法完成的,以便可以从复杂的事件对象中提取值。第一个级别是允许跨多个参数映射的数组,并且该数组包含嵌套对象。

例如,使用水平滚动手势时,您需要执行以下操作以映射event.nativeEvent.contentOffset.xscrollX(an Animated.Value):

onScroll={Animated.event( // scrollX = e.nativeEvent.contentOffset.x [{ nativeEvent: { contentOffset: { x: scrollX } } }] )}

使用时PanResponder,可以使用以下代码从gestureState.dx和中提取x和y位置gestureState.dy。我们null在数组的第一个位置使用a ,因为我们只关心传递给PanResponder处理程序的第二个参数,这就是gestureState

onPanResponderMove={Animated.event( [null, // ignore the native event // extract dx and dy from gestureState // like 'pan.x = gestureState.dx, pan.y = gestureState.dy' {dx: pan.x, dy: pan.y} ])}

回应当前的动画值

您可能会注意到在动画制作中没有明显的方法来读取当前值。这是因为优化可能只会在本机运行时知道该值。如果您需要针对当前值运行JavaScript,则有两种方法:

  • spring.stopAnimation(callback)将停止动画并callback以最终值调用。这在做手势转换时很有用。

  • spring.addListener(callback)callback在动画运行时异步调用,提供最近的值。这对于触发状态更改非常有用,例如当用户将它拖拽得更近时,将跳动捕捉到新选项,因为与连续手势相比,这些较大的状态更改不太敏感,比如需要在60 FPS。

Animated被设计为完全可序列化的,因此可以以高性能方式运行动画,而不依赖于正常的JavaScript事件循环。这确实会影响API,所以请记住,与完全同步的系统相比,执行某些操作似乎有点麻烦。退房Animated.Value.addListener的方式来解决这些局限性,但要节制利用,因为它可能在未来的业绩产生影响。

使用本机驱动程序

AnimatedAPI被设计为可序列化。通过使用本地驱动程序,我们会在开始动画之前将所有关于动画的内容发送到本地,从而允许本地代码在UI线程上执行动画,而无需通过每一帧的桥接。一旦动画开始,JS线程就可以被阻塞而不会影响动画。

对普通动画使用本地驱动程序非常简单。启动时只需添加useNativeDriver: true到动画配置中即可。

Animated.timing(this.state.animatedValue, { toValue: 1, duration: 500, useNativeDriver: true, // <-- Add this }).start(

动画值只与一个驱动程序兼容,因此如果在对某个值启动动画时使用本机驱动程序,请确保该值上的每个动画也使用本地驱动程序。

本地驱动程序也适用于Animated.event。由于React Native的异步特性,这对于在没有本地驱动程序的情况下滚动位置后面的动画特别有用,动画将始终在手势后面运行一帧。

<Animated.ScrollView // <-- Use the Animated ScrollView wrapper scrollEventThrottle={1} // <-- Use 1 here to make sure no events are ever missed onScroll={Animated.event( [{ nativeEvent: { contentOffset: { y: this.state.animatedValue } } }], { useNativeDriver: true } // <-- Add this )} > {content} </Animated.ScrollView>

您可以通过运行RNTester应用程序来查看本机驱动程序,然后加载本地动画示例。您还可以查看源代码以了解这些示例的制作方式。

注意事项

Animated本地驱动程序目前不支持您所能做的所有事情。主要的限制是,你只能动画非布局属性:之类的东西transform,并opacity会工作,但Flexbox的和position属性不会。使用时Animated.event,它只能用于直接事件而不是冒泡事件。这意味着它不起作用,PanResponder但可以处理类似的事情ScrollView#onScroll

记住

虽然使用变换样式(如rotateY,,rotateX等)确保了变换样式perspective已到位。目前,如果没有它,某些动画可能无法在Android上呈现。下面的例子。

<Animated.View style={{ transform: [ { scale: this.state.scale }, { rotateY: this.state.rotateY }, { perspective: 1000 } // without this line this Animation will not render on Android while working fine on iOS ] }} />

其他例子

RNTester应用程序有各种Animated使用示例:

LayoutAnimation API

LayoutAnimation允许您全局配置createupdate动画,将在下一个渲染/布局循环中用于所有视图。这对于执行flexbox布局更新很有用,而不必费心去测量或计算特定属性以便直接对它们进行动画处理,而且在布局更改可能会影响祖先时特别有用,例如“看更多”扩展也会增加父级的大小并向下推动下面的行,否则将需要组件之间的明确协调以使它们全部同步地动画化。

请注意,尽管LayoutAnimation功能非常强大并且非常有用,但Animated与其他动画库相比,它提供的控制要少得多,所以如果无法LayoutAnimation按照自己的想法操作,则可能需要使用其他方法。

请注意,为了让它在Android上运行,您需要通过以下方式设置以下标志UIManager

UIManager.setLayoutAnimationEnabledExperimental && UIManager.setLayoutAnimationEnabledExperimental(true

import React from 'react'; import { NativeModules, LayoutAnimation, Text, TouchableOpacity, StyleSheet, View, } from 'react-native'; const { UIManager } = NativeModules; UIManager.setLayoutAnimationEnabledExperimental && UIManager.setLayoutAnimationEnabledExperimental(true export default class App extends React.Component { state = { w: 100, h: 100, }; _onPress = () => { // Animate the update LayoutAnimation.spring( this.setState{w: this.state.w + 15, h: this.state.h + 15}) } render() { return ( <View style={styles.container}> <View style={[styles.box, {width: this.state.w, height: this.state.h}]} /> <TouchableOpacity onPress={this._onPress}> <View style={styles.button}> <Text style={styles.buttonText}>Press me!</Text> </View> </TouchableOpacity> </View> } } const styles = StyleSheet.create{ container: { flex: 1, alignItems: 'center', justifyContent: 'center', }, box: { width: 200, height: 200, backgroundColor: 'red', }, button: { backgroundColor: 'black', paddingHorizontal: 20, paddingVertical: 15, marginTop: 15, }, buttonText: { color: '#fff', fontWeight: 'bold', }, }

本示例使用预设值,您可以根据需要自定义动画,请参阅LayoutAnimation.js以获取更多信息。

补充笔记

requestAnimationFrame

requestAnimationFrame是您可能熟悉的浏览器的填充。它接受一个函数作为唯一的参数并在下一次重绘之前调用该函数。它是所有基于JavaScript的动画API的基础动画基础构件。一般来说,你不需要自己调用它 - 动画API将为你管理帧更新。

setNativeProps

如直接操作部分所述,setNativeProps允许我们直接修改本机支持的组件(实际由本机视图支持的组件)的属性,而无需setState重新呈现组件层次结构。

我们可以在Rebound示例中使用它来更新比例 - 如果我们正在更新的组件深度嵌套并且未使用优化组件,这可能会有所帮助shouldComponentUpdate

如果您发现丢帧的动画(每秒低于60帧),请查看使用setNativePropsshouldComponentUpdate优化它们。或者,您可以在UI线程上运行动画,而不是使用useNativeDriver选项运行JavaScript线程。您可能还想使用InteractionManager推迟任何计算密集型工作,直到动画完成后。您可以使用应用内开发人员菜单“FPS监视器”工具来监视帧速率。