深入了解React Native Reanimated

4,896 阅读7分钟

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: 10
  • mass: 1
  • stiffness: 100
  • overshootClamping: false
  • restDisplacementThreshold: 0.01
  • restSpeedThreshold: 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 Gif Dropdown

让我们添加另一个共享价值和工作单元来控制文本的不透明度。我们还将在造型上增加一些细节。

现在,我们有了一个功能齐全的动画文本下拉菜单

Reanimated Gif Styling

总结

Reanimated将事件驱动的交互从JavaScript线程中卸载出来,而是在UI线程中同步执行。有了Reanimated,你不必再担心丢帧或限制你的JavaScript线程的工作量了。

关于Reanimated的方法、钩子和动画的更多信息,我建议查看Reanimated文档

The postDeep dive into React Native Reanimatedappeared first onLogRocket Blog.