Animations
Animations
动画对于创造出色的用户体验非常重要。固定物体在开始移动时必须克服惯性。运动中的物体有动力,很少立即停下来。动画可让您在界面中传达身体上可信的动作。
React Native提供了两个互补的动画系统:Animated
用于精确和交互式控制特定值以及LayoutAnimation
动画全局布局事务。
Animated API
该Animated
API旨在以非常高效的方式简明扼要地表达各种有趣的动画和交互模式。Animated
侧重于输入和输出之间的声明性关系,以及两者之间的可配置变换,以及简单start
/ stop
方法来控制基于时间的动画执行。
Animated
出口4种动画组件类型:View
,Text
,Image
,和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
被初始化为一部分state
。View
映射到此动画值的不透明属性。在幕后,提取数字值并用于设置不透明度。
当组件加载时,不透明度设置为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(
查看Animated
API参考的“配置动画”部分,了解有关内置动画支持的所有配置参数的更多信息。
组成动画
动画可以组合并按顺序或并行播放。连续动画可以在上一个动画结束后立即播放,或者可以在指定的延迟后开始播放。所述Animated
API提供了几种方法,如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
为禁用此选项。
您可以在Animated
API参考的“合成动画”部分找到合成方法的完整列表。
结合动画值
您可以通过添加,乘法,除法或模数来组合两个动画值,以创建新的动画值。
在某些情况下,动画值需要反转另一个动画值进行计算。一个例子是反转比例尺(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
。您可以通过设置设置的外推extrapolate
,extrapolateLeft
或extrapolateRight
选项。默认值是,extend
但可以clamp
用来防止输出值超出outputRange
。
跟踪动态值
动画值也可以跟踪其他值。只需将toValue
动画设置为另一个动画值而不是普通数字。例如,像在Android使用信使的一个“聊天头”动画可以与实施spring()
寄托在另一个动画值,或者与timing()
和duration
0刚性跟踪。它们也可以用插值组成:
Animated.spring(follower, {toValue: leader}).start(
Animated.timing(opacity, {
toValue: pan.x.interpolate{
inputRange: [0, 300],
outputRange: [1, 0],
}),
}).start(
在leader
和follower
动画值将采用以下方式实现Animated.ValueXY()
。ValueXY
是处理2D交互的便捷方式,例如平移或拖动。它是一个简单的包装,基本上包含两个Animated.Value
实例和一些帮助函数,通过它们进行调用,从而在许多情况下ValueXY
进行替换Value
。它允许我们在上面的例子中跟踪x和y值。
跟踪手势
手势,如平移或滚动,以及其他事件可以直接映射到使用动画值Animated.event
。这是通过结构化地图语法完成的,以便可以从复杂的事件对象中提取值。第一个级别是允许跨多个参数映射的数组,并且该数组包含嵌套对象。
例如,使用水平滚动手势时,您需要执行以下操作以映射event.nativeEvent.contentOffset.x
到scrollX
(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
的方式来解决这些局限性,但要节制利用,因为它可能在未来的业绩产生影响。
使用本机驱动程序
该Animated
API被设计为可序列化。通过使用本地驱动程序,我们会在开始动画之前将所有关于动画的内容发送到本地,从而允许本地代码在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
允许您全局配置create
和update
动画,将在下一个渲染/布局循环中用于所有视图。这对于执行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帧),请查看使用setNativeProps
或shouldComponentUpdate
优化它们。或者,您可以在UI线程上运行动画,而不是使用useNativeDriver选项运行JavaScript线程。您可能还想使用InteractionManager推迟任何计算密集型工作,直到动画完成后。您可以使用应用内开发人员菜单“FPS监视器”工具来监视帧速率。