# 14|Reanimated:如何让动画变得更流畅? 你好,我是蒋宏伟。 今天我们来聊一聊React Native中动画的原理。在开始之前,我想请你思考一下:动画的本质到底是什么? 你可能知道,与真实世界中连续运动的事物不同,我们在手机、电脑、电影院的屏幕中看到的动画,实际是由一张张快速切换图片组成的。看动画时,我们的眼睛接收到的是一张张并不连续的静态图片,但我们的大脑把这些不连续的图片“想象”成了一系列连续事件,这就是动画的基本原理。 而手机动画要想流畅,一般而言需要保证每 1 秒渲染 60 帧的速度。这里的每一帧都是一张静态图片,也就是说 1 秒钟需要渲染出 60 个静态图片。这也意味着手机处理每一帧动画的耗时,需要保证在 16.6ms(=1000/60)以内,如果处理一帧的耗时超过 16.6ms ,就会掉帧。掉帧多了,我们的大脑就会感觉到动画中的不连续性,也就是常说的卡顿。 动画对渲染性能的要求很高。理论上,你可以使用 setInterval 每 16.6ms 执行一次 setState 改变状态,渲染新的视图,来实现动画。但实际上,setState 是一种耗时比较长的更新页面的方法,特别是在复杂页面、复杂交互的情况下,setInterval + setState 的方案并不适合用来实现动画。 所以,为了保障动画的流畅性,在涉及动画的业务场景中,**我们还需要引入动画库**。 在上一讲中,我给你介绍了 React Native 中常用的三种动画工具,包括:适合轻量级动画场景使用的 React Native 自带的 [Animated](https://reactnative.cn/docs/animated) 动画;适合无交互场景的、能找 UI 设计师帮忙自动生成的 [Lottie](https://github.com/lottie-react-native/lottie-react-native) 动画;以及我今天重点要和你聊的、适用于可交互场景的 [Reanimated](https://docs.swmansion.com/react-native-reanimated/) 动画。 ## 初学 Reanimated Reanimated 的名字来源于它的那句口号: > React Native’s Animated library reimplemented. Reanimated 名字中的 Re 就是 Reimplemented 重新实现的意思,Animated 代表就是 React Native 自带的动画库 Animated,加起来就是重新实现的 Animated 动画库的意思。它的潜台词好像就是:如果你觉得 React Native 自带的 Animated 动画库不好用,就来试试我吧,我把 React Native 官方的动画库给重新实现了。 我们先用**“切换宽度的动画”**的例子,看看 Reanimated 到底应该如何使用。 这个例子是这样的:现在你有一个视图和一个按钮,视图的高度是固定的,视图的宽度可以用动画来控制,你每点一下切换宽度的文字,视图宽度都会随机改变。示意图如下: ![图片](https://static001.geekbang.org/resource/image/05/69/05ae457ac9eba406300aafa246549069.png?wh=1920x886) 你可以看到,在页面中蓝色视图初始化的宽度是 10 像素,当你点击切换宽度的文字后,蓝色视图的宽度会在 0~350 像素之间随机变化。因为是动画,所以蓝色视图的宽度并不是一下就变宽的,而是连续改变的。在 1s 内,先增长几像素,然后再增长几像素,依次类推直到目标长度,因为刷新的帧率很快,因此人的肉眼看起来就是宽度就是连续变化的。 那我们怎么用Reanimated实现这个动画效果呢? 别急,为了帮你更好地吃透这个新知识,我们引入之前学过的State,对比着来学习。那么,使用 Reanimated 实现视图“动画”和使用 State 实现视图“变化”有什么相似之处呢?你可以看下它们的更新步骤的对比: ![图片](https://static001.geekbang.org/resource/image/b4/7e/b4653520a9bd29e348671f19b439317e.png?wh=1920x970) 你可以看到,无论是用 State 更新页面,还是用 Reanimated 更新动画,都需要 4 步。具体它们有什么相似之处呢?我们直接来分析 Reanimated中的这4个概念: 第一个概念:共享值(SharedValue)。**Reanimated中共享值这个概念类似于 React 中的状态 State**,我们简单对比下它们各自的代码,先看看State的: ```plain // State 示例代码 import { useState } from 'react'; const [randomWidth, setRandomWidth] = useState(10); // randomWidth === 10 ``` 在 State 示例代码中,驱动视图变化的最初因子是状态。用于初始化状态的钩子函数 useState 是从 react 中引入的,然后在组件中使用 useState 创建出一个随机宽度状态 randomWidth,以及一个改变该状态的函数 setRandomWidth。其中,初始化出来的 randomWidth 是一个默认赋值数字10。 接着我们看看Reanimated的代码: ```plain // Reanimated 示例代码 import { useSharedValue} from 'react-native-reanimated'; const randomWidth = useSharedValue(10); // randomWidth.value === 10 ``` 在 Reanimated 的示例代码中,驱动动画的最初因子是共享值(ShareValue)。用于初始化共享值的钩子函数 useSharedValue 是从 react-native-reanimated 中引入的。然后使用 useSharedValue 创建出一个对象 randomWidth,randomWidth 的 value 属性是一个默认赋值数字 10。 第二个概念:衍生值(DerivedValue)。**Reanimated 的衍生值(DerivedValue)这个概念类似于 React 中的状态衍生值**。我们同样先来看State的示例代码: ```plain // State 示例代码 const style = { width: randomWidth } ``` 在 State 示例代码中,你可以在组件函数中的任意位置直接使用状态 randomWidth,或者将状态 randomWidth 封装到样式对象 style 中。 然后是Reanimated的示例代码: ```plain // Reanimated 示例代码 import { useAnimatedStyle } from 'react-native-reanimated'; // 错误示范 const style = { width: randomWidth.value } // 正确示范 const style = useAnimatedStyle(() => { return { width: randomWidth.value, }; }); ``` 但在 Reanimated 示例代码中,如果你直接将 `const style = {width: randomWidth.value}` 组成的样式对象赋值给 JSX 元素,控制视图宽度改变的动画是不生效的。这是 Reanimated 驱动动画和 State 驱动视图的机制不一样导致的。 这时你需要从 react-native-reanimated 中引入钩子函数 useAnimatedStyle,这个钩子函数是专门用来处理动画样式的衍生值的,它的第一个入参函数的返回值就是动画组件的样式值。 第三个概念,动画组件(AnimatedComponent)。**Reanimated 的动画组件和 React/React Native 中的组件(Component)概念是类似的**。我们同样先看 State 示例代码: ```plain // State 示例代码 import { View } from 'react-native'; ``` 在 State 示例代码中,你要从 react-native 库中引入 View 组件,并将组件 View 实例化为 JSX 元素。 然后再看Reanimated的示例代码: ```plain // Reanimated 示例代码 import Animated from 'react-native-reanimated'; ``` 你可以看到,在 Reanimated 示例代码中,你需要从 react-native-reanimated 引入 Animated 对象,在该对象上挂了常用的 react-native 组件,比如示例代码中的 `Animated.View`,还有 `Animated.Text`、 `Animated.FlatList`等等。 这些由 Reanimated 包装好的动画组件,比如 `Animated.View` 等等,使用方式和 `View` 基本类似。不同的是共享值(ShareValue)和衍生值(DerivedValue)是专门给动画组件(AnimatedComponent)用的,普通组件(Component)用不了。 第四个概念,更新共享值。**Reanimated 的更新共享值的方式和 React/React Native 更新状态的方式方式是不一样的。** State的示例代码如下: ```plain // State 示例代码 const [randomWidth, setRandomWidth] = useState(10); setRandomWidth(Math.random() * 350) ``` 在 State 示例代码中,你是通过钩子函数 useState 生成的状态更新函数 setRandomWidth 来更新状态的。 然后是Reanimated的示例代码: ```plain // Reanimated 示例代码 const randomWidth = useSharedValue(10); // 不带动画的更新 randomWidth.value = Math.random() * 350; // 带动画的更新 randomWidth.value = withTiming(Math.random() * 350); ``` 我们可以看到,在 Reanimated 示例代码中,没有共享值的更新函数,它只生成了一个共享值对象,其真正的值是挂在 value 属性下的。你可以直接通过等号 `=` 把最新的视图宽度 `Math.random() * 350` 赋值给`randomWidth.value`。 事实上,Reanimated 有两种更新方式,一种是不带动画曲线的更新方式,另一种是带**动画曲线**的更新方式。 你直接把视图宽度 `Math.random() * 350` 赋值给`randomWidth.value`,就是通过指定一个最终共享值的方式进行更新的,比如从 10 像素宽度直接变为 100 像素宽度。这种更新方式是一步到位的,没有动画曲线。 真正的动画是从 10 像素宽度,增长到 11 像素,然后增长到 12 像素,以此类推,通过连续的方式增长到 100 像素宽度的。具体地说,控制每一帧增长多少像素、减少多少像素,是通过类似 `withTiming` 的动画曲线实现的。`withTiming` 动画曲线的意思是,启动一个基于时间的动画,在每个单位时间内增长或减少的像素是相等的。 使用 `withTiming(100)` 更新共享值时,就会启动基于时间的动画曲线,其默认的持续时间是 300ms。理论上,在这 300ms 内,视图的宽度会从 10 像素开始,以每一帧增加一个固定的宽度的速度,增加到 100 像素。 切换宽度动画的完整示例代码如下: ```plain import Animated, { useSharedValue, withTiming, useAnimatedStyle, Easing, } from 'react-native-reanimated'; import { View, Button } from 'react-native'; import React from 'react'; function AnimatedStyleUpdateExample(): React.ReactElement { const randomWidth = useSharedValue(10); const style = useAnimatedStyle(() => { console.log('==Animated==') return { width: withTiming(randomWidth.value), }; }); console.log('==render==') return (