React Native Reanimated是一个强大的、直观的库,允许你为iOS和Android应用程序创建流畅的动画和互动。尽管React Native有许多多功能和高性能的动画库,包括内置的Animated API,但我们将深入研究Reanimated,以发现为什么它是我认为的最佳选择。
让我们开始吧!
Reanimated的代码执行
Reanimated的核心优势在于它能够提高React Native应用程序的性能和响应性,使动画具有流畅的完成度,而这只有通过即时的代码执行才能实现。
要了解Reanimated如何工作,我们首先需要了解React Native如何执行代码。
React Native线程
React Native有三个独立的线程,允许执行时间密集的JavaScript代码而不影响UI的响应速度。UI线程是本地线程。它为iOS运行Swift或Objective C,为Android运行Java或Kotlin。应用程序的UI只在UI线程上进行操作。
JavaScript线程负责渲染逻辑,比如哪些视图被显示,以何种方式显示。它通过一个JavaScript引擎单独执行JavaScript。最后,桥接器使UI和JavaScript线程之间能够进行异步通信。为了避免拖慢性能,它应该只用于传输少量的数据。
这些交互在JavaScript线程上开始,必须在主线程上反映出来,在处理事件驱动的交互时有可能造成问题。
Reanimated vs.Animated API
由于UI和JavaScript线程之间的通信是异步的,Animated API至少会延迟一帧的更新,这大约持续16.67ms。当JavaScript线程也在执行其他进程,如运行React diffing和处理网络请求时,延迟可能会持续更长时间。
这些延迟被称为丢帧,会损害你的用户体验,使动画看起来很笨拙。Reanimated通过从JavaScript线程中移除动画和事件处理逻辑并直接在UI线程中运行来解决这个问题。
此外,Reanimated还定义了worklets,它是很小的JavaScript代码块,可以被移到一个单独的JavaScript虚拟机中,并在UI线程上同步执行。Worklets使动画在被触发后立即运行,从而创造出一个更令人满意的用户体验。
共享值
共享值,类似于普通React应用程序中的有状态数据,存储动态数据,你可以在Reanimated中制作动画。然而,共享值不是像有状态数据那样在组件之间共享数据,而是在UI线程和JavaScript线程之间共享数据。
当这些共享值中的数据被更新时,Reanimated库会注册这个变化并执行一个动画,类似于React在状态更新时重新渲染一个组件的方式。
创建一个共享值看起来和用useState Hook创建一块状态很相似;只需用useSharedValue Hook代替。
假设我们想创建一个盒子,当按钮被按下时改变高度。我们将定义一个boxHeight 变量作为一个共享值,这样我们就可以在动画制作过程中在UI线程和JavaScript线程之间共享它。
const boxHeight = useSharedValue(150);
在上面的例子中,我们创建了一个名为boxHeight 的共享值,并将其初始值设置为150 。为了在以后的代码中访问boxHeight ,你必须引用boxHeight.value ,而不仅仅是boxHeight 。
useSharedValue 钩子返回一个对象,初始值被保存在该对象的.value 属性中。要更新共享值,通常是在一些预先确定的用户输入后进行,只需在一个函数中更新.value 属性。Reanimated将注册该变化。
function toggleHeight() {
boxHeight.value === 450 ?
boxHeight.value = 150 :
boxHeight.value = 450;
}
现在,让我们探讨一下如何在Reanimated的工作单元中使用这些共享值。
Reanimated的小程序
Worklets是简单的函数,允许我们在UI线程上同步地执行JavaScript代码。通常情况下,worklet返回React组件的样式属性。一个worklet会被它所引用的共享值的任何变化所触发。
为了声明一个worklet,我们可以在函数定义的开头使用worklet 指令。在下面的代码块中,我们使用worklet 指令声明了boxAnimation 函数,表明该函数是一个worklet。我们正在使用共享值boxHeight ,返回一个更新的height 属性。
const boxAnimation = () => {
'worklet';
return {
height: withTiming(boxHeight.value, {duration: 750})
};
};
更常见的是,我们可以使用useAnimatedStyle Hook来声明一个worklet,它允许我们传入一个回调函数作为参数。现在,回调函数将被视为一个worklet。
const boxAnimation = useAnimatedStyle(() => {
return {
height: withTiming(boxHeight.value, {duration: 750})
};
});
在这两种声明worklet的方法中,我推荐使用useAnimatedStyle Hook。要使用worklet 指令方法,我们必须添加一个二级函数,调用runOnUI 方法,并将boxAnimation 函数作为参数传递。另一方面,useAnimatedStyle 钩子将那个二级函数抽象出来,在UI线程上自动运行传递给useAnimatedStyle 钩子的回调。
在我们的两个例子中,每当boxHeight.value 的值被更新时,worklet就会触发一个动画,显示盒子在垂直方向上扩张或收缩。
实用方法
withTiming
在我们上面的例子中,我们使用了withTiming 实用方法,它允许我们创建一个简单的动画,从起点逐渐过渡到终点,让我们能够控制过渡的时间和 加速度。
withTiming 需要两个参数。第一个是必须的,是要更新的共享值。在我们的例子中,它是boxHeight 。
第二个可选参数是一个有两个属性的对象。duration 控制动画的时间长度,easing 控制动画的加速度和减速度。duration 的默认值是300ms,in-out quad easing 的默认值是easing 。
withSpring
withSpring 方法与withTiming 相似,但是,它创建了一个不同的动画效果,即元素在稳定到结束位置之前会经过它的端点。
withSpring 只有一个必要的参数,那就是要更新的共享值。它还有下面列出的六个可选参数。然而,默认值对大多数使用情况来说已经足够了。
damping: 10mass: 1stiffness: 100overshootClamping: falserestDisplacementThreshold: 0.01restSpeedThreshold: 2
useAnimatedStyle 钩子
在我们之前的例子中,useAnimatedStyle 钩子创建了一个worklet,将共享值boxHeight 与一个在其样式属性中使用boxHeight.value 的组件联系起来。当赋予React组件属性时,我们必须使用我们可以动画化的组件版本。
例如,我们不应该使用<View /> 标签,而应该使用<Animated.View /> 标签。<Animated.View /> 的所有子代都将受到应用于父代的动画的影响。
当为组件设置样式时,一定要以数组的形式传递样式。第一个元素是一个包含所有你通常会使用的样式的对象,包括height 。第二个元素是前面定义的worklet。
现在,让我们结合我们所涉及的所有Reanimated工具来创建一个简单的灰色盒子,当我们按下一个按钮时,它就会膨胀和收缩。
// import statements where we add the functionality to use hooks and methods from Reanimated
import React from 'react';
import { View, Button } from 'react-native';
import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
export default function ExpandingTextBox() {
// creating shared value via useSharedValue
const boxHeight = useSharedValue(150);
// creating worklet via useAnimatedStyle, and incorporating the withTiming method
const boxAnimation = useAnimatedStyle(() => {
return {
height: withTiming(boxHeight.value, {duration: 750})
}
});
// function that toggles the value of boxHeight so it can expand and contract
function toggleHeight() {
boxHeight.value === 450 ?
boxHeight.value = 150 :
boxHeight.value = 450;
};
// styles to use on our grey box
const styles = {
box: {
width: 400,
height: 150,
backgroundColor: 'grey',
borderRadius: 15,
margin: 100,
padding: 20,
display: 'flex'
}
};
return (
<View style={styles.app}>
{/* Animated.View component, with the typical styles contained in styles.box,
and the worklet "boxHeight" that updates the height property alongside it */}
<Animated.View style={[styles.box, boxAnimation]}>
{/* button that fires off toggleHeight() function every time it's pressed */}
<Button title='More' onPress={() => toggleHeight()} />
</Animated.View>
</View>
)
};
上面代码的输出将看起来像下面的动画。

让我们添加另一个共享价值和工作单元来控制文本的不透明度。我们还将在造型上增加一些细节。
现在,我们有了一个功能齐全的动画文本下拉菜单

总结
Reanimated将事件驱动的交互从JavaScript线程中卸载出来,而是在UI线程中同步执行。有了Reanimated,你不必再担心丢帧或限制你的JavaScript线程的工作量了。
关于Reanimated的方法、钩子和动画的更多信息,我建议查看Reanimated文档。
The postDeep dive into React Native Reanimatedappeared first onLogRocket Blog.