以下文章是《React Native 优化终极指南》的一部分,讨论了如何使用原生解决方案在 60FPS 下实现流畅的动画和手势驱动界面。
为什么这很重要?
漂亮流畅的动画是应用程序成功的关键部分。但是,如果使用不当,可能会导致产品运行速度减慢,从而严重损害您的产品。在本文中,我们将介绍一些有用的做法,以便在 React Native 中实现流畅的 60FPS 动画,同时不影响应用程序的速度。
在基于《React Native 优化终极指南》的其他博文中,我们将讨论以下与通过了解 React Native 实现细节来提高性能相关的主题:
- 为您的应用程序选择合适的外部库
- 使用移动专用库优化电池消耗
- 在本地和 JavaScript 之间找到平衡
- 利用高阶组件提高 React Native 性能
- 优化 React Native 应用程序的 JavaScript 捆绑程序
请务必查看这些内容。现在让我们进入正题。
移动设备上流畅的 60FPS 动画的重要性
移动用户习惯于流畅且设计良好的界面,这些界面能快速响应他们的交互并提供及时的视觉反馈。因此,应用程序必须在许多地方注册大量动画,这些动画必须在其他工作正在进行时运行。
正如我们在本文中所解释的,通过桥接器传递的信息量是有限的。目前还没有内置的优先队列。换句话说,您需要自行架构和设计应用程序,使业务逻辑和动画都能不间断地运行。这与我们习惯于执行动画的方式不同。例如,在 iOS 上,内置 API 提供了前所未有的性能,并且总是以适当的优先级进行调度。长话短说,我们无需担心如何确保它们以 60 FPS 的速度运行。
对于 React Native,情况就有些不同了。如果您不事先自上而下地考虑您的动画并选择正确的工具来应对这一挑战,您迟早会遇到掉帧的问题。
蹩脚的动画会让用户觉得你的应用程序速度缓慢、未完成
在当今这个应用程序林立的世界,提供流畅的交互式用户界面可能是您赢得客户选择应用程序的唯一途径之一。
如果您的应用程序不能提供与用户交互(如手势)配合良好的响应式界面,可能会影响新客户,降低投资回报率和用户情绪。
移动用户喜欢能跟随他们前进并看起来一流的界面,因此确保动画始终运行流畅是楼宇体验的基本要素。
解决方案: 如果可能,使用原生的正确动画
使用原生驱动程序在 React Native 中制作一次性动画
启用原生驱动程序是快速提高动画性能的最简单方法。不过,可以与原生驱动一起使用的样式道具子集是有限的。您可以将其用于变换和不透明度等非布局属性。颜色、高度和其他属性则无法使用。这些属性足以实现应用程序中的大部分动画,因为您通常希望显示/隐藏某些内容或改变其位置。
const fadeAnim = useRef(new Animated.Value(0)).current;
const fadeIn = () => {
Animated.timing(fadeAnim, {
toValue: 1,
duration: 1000,
useNativeDriver: true, // enables native driver
}).start();
};
// [...]
<Animated.View style={{ opacity: fadeAnim }} />
为不透明动画启用本地驱动程序
对于更复杂的用例,您可以使用 React Native Reanimated 库。它的 API 与基本的 Animated 库兼容,还引入了一组更底层的函数来控制动画。更重要的是,它提供了使用原生驱动程序制作所有可能的样式道具动画的可能性。因此,高度或颜色动画将不再是问题。不过,变换和不透明度动画仍然会稍快一些,因为它们是由 GPU 加速的。但普通用户应该看不出任何区别。
实现手势驱动 60FPS 动画的方法
动画所能达到的最理想效果就是能用手势控制动画。对于客户来说,这是界面中最令人愉悦的部分。它能建立一种强烈的情感,让应用程序感觉非常流畅、反应灵敏。普通的 React Native 在将手势与本地驱动的动画相结合方面非常有限。您可以利用 ScrollView 滚动事件来构建平滑的可折叠标题。
对于更复杂的用例,有一个很棒的库--React Native 手势处理程序--可以让您原生处理不同的手势,并将其插值到动画中。您可以将其与 Animated 结合,创建一个简单的可轻扫元素。不过,这仍然需要 JS 回调,但有补救措施!
手势驱动动画最强大的一对工具是手势处理程序和 Reanimated。这两个工具旨在协同工作,为构建复杂的手势驱动动画提供可能性,这些动画在原生端完全经过计算。这里唯一的限制就是你的想象力(和编码技巧,因为 Reanimated 的底层 API 并不简单)。
Reanimated API 支持在 UI 线程上使用工作点概念同步执行 JavaScript。该库的运行时会在 UI 线程上生成一个辅助 JS 上下文,然后该上下文就能以上述 worklet 的形式运行 JavaScript 函数。利用您的想象力和 Reanimated,您可以以最快的速度创建精彩的动画。
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { PanGestureHandler } from 'react-native-gesture-handler';
import Animated, {
useAnimatedGestureHandler,
useAnimatedStyle,
useSharedValue,
withSpring,
} from 'react-native-reanimated';
const Snappable = (props) => {
const startingPosition = 0;
const x = useSharedValue(startingPosition);
const y = useSharedValue(startingPosition);
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [{ translateX: x.value }, { translateY:y.value }],
};
});
const eventHandler = useAnimatedGestureHandler({
onStart: (event, ctx) => {
ctx.startX = x.value;
ctx.startY = y.value;
},
onActive: (event, ctx) => {
x.value = ctx.startX + event.translationX;
y.value = ctx.startY + event.translationY;
},
onEnd: (event, ctx) => {
x.value = withSpring(startingPosition);
y.value = withSpring(startingPosition);
},
});
return (
<PanGestureHandler onGestureEvent={eventHandler}>
<Animated.View style={animatedStyle}>{props.children}</
Animated.View>
</PanGestureHandler>
);
};
const Example = () => {
return (
<View style={styles.container}>
<Snappable>
<View style={styles.box} />
</Snappable>
</View>
);
};
export default Example;
const BOX_SIZE = 100;
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
box: {
width: BOX_SIZE,
height: BOX_SIZE,
borderColor: '#F5FCFF',
alignSelf: 'center',
backgroundColor: 'plum',
margin: BOX_SIZE / 2,
},
});
手势的底层处理可能不是小菜一碟,但幸运的是,已经有第三方库利用上述工具并公开了道具 callbackNode。它只不过是一个从特定手势行为中派生出来的 Animated.Value。它的取值范围通常从 0 到 1,与手势的进程一致。你可以将这些值插值到屏幕上的动画元素上。reanimated-bottom-sheet 和 react-native-tab-view 就是一个暴露 CallbackNode 的优秀库示例。
import * as React from "react";
import { StyleSheet, Text, View } from "react-native";
import Animated from "react-native-reanimated";
import BottomSheet from "reanimated-bottom-sheet";
import Lorem from "./Lorem";
const { Value, interpolateNode: interpolate } = Animated;
const Example = () => {
const gestureCallbackNode = new Value(0);
const renderHeader = () => (
<View style={styles.headerContainer}>
<Text style={styles.headerTitle}>Drag me</Text>
</View>
);
const renderInner = () => (
<View style={styles.innerContainer}>
<Animated.View
style={{
opacity: interpolate(gestureCallbackNode, {
inputRange: [0, 1],
outputRange: [1, 0],
}),
transform: [
{
translateY: interpolate(gestureCallbackNode, {
inputRange: [0, 1],
outputRange: [0, 100],
}),
},
],
}}
>
<Lorem />
<Lorem />
</Animated.View>
</View>
);
return (
<View style={styles.container}>
<BottomSheet
callbackNode={gestureCallbackNode}
snapPoints={[50, 400]}
initialSnap={1}
renderHeader={renderHeader}
renderContent={renderInner}
/>
</View>
);
};
export default Example;
const styles = StyleSheet.create({
container: {
flex: 1,
},
headerContainer: {
width: "100%",
backgroundColor: "lightgrey",
height: 40,
borderWidth: 2,
},
headerTitle: {
textAlign: "center",
fontSize: 20,
padding: 5,
},
innerContainer: {
backgroundColor: "lightblue",
},
});
降低 JS 操作的优先级,让动画更流畅
并非总能完全控制动画的实现方式。例如,React Navigation 结合使用了 React 原生手势处理程序和 Animated,这仍然需要 JavaScript 来控制动画运行时。因此,如果您正在导航的屏幕上加载了繁重的用户界面,您的动画可能会开始闪烁。幸运的是,您可以使用 InteractionManager 推迟此类操作的执行。
import React, { useState, useRef } from "react";
import {
Text,
View,
StyleSheet,
Button,
Animated,
InteractionManager,
Platform,
} from "react-native";
import Constants from "expo-constants";
const ExpensiveTaskStates = {
notStared: "not started",
scheduled: "scheduled",
done: "done",
};
const App = () => {
const animationValue = useRef(new Animated.Value(100));
const [animationState, setAnimationState] = useState(false);
const [expensiveTaskState, setExpensiveTaskState] = useState(
ExpensiveTaskStates.notStared
);
const startAnimationAndScheduleExpensiveTask = () => {
Animated.timing(animationValue.current, {
duration: 2000,
toValue: animationState ? 100 : 300,
useNativeDriver: false,
}).start(() => {
setAnimationState((prev) => !prev);
});
setExpensiveTaskState(ExpensiveTaskStates.scheduled);
InteractionManager.runAfterInteractions(() => {
setExpensiveTaskState(ExpensiveTaskStates.done);
});
};
return (
<View style={styles.container}>
{Platform.OS === "web" ? (
<Text style={styles.infoLabel}>
❗InteractionManager works only on native platforms. Open example on
iOS or Android❗
</Text>
) : (
<>
<Button
title="Start animation and schedule expensive task"
onPress={startAnimationAndScheduleExpensiveTask}
/>
<Animated.View
style={[styles.box, { width: animationValue.current }]}
>
<Text>Animated box</Text>
</Animated.View>
<Text style={styles.paragraph}>
Expensive task status: {expensiveTaskState}
</Text>
</>
)}
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
paddingTop: Constants.statusBarHeight,
padding: 8,
},
paragraph: {
margin: 24,
fontSize: 18,
textAlign: "center",
},
box: {
backgroundColor: "coral",
marginVertical: 20,
height: 50,
},
infoLabel: {
textAlign: "center",
},
});
export default App;
这个方便的 React Native 模块允许您在所有运行动画结束后执行任何代码。实际上,您可以显示一个占位符,等待动画结束,然后渲染实际的 UI。这将使您的 JavaScript 动画运行流畅,避免被其他操作打断。通常情况下,流畅性足以提供良好的体验。
让用户以 60 FPS 的速度享受流畅的动画和手势驱动界面
在 React Native 中制作动画并没有唯一正确的方法。生态系统中充满了不同的库和处理交互的方法。本节中提出的想法只是建议,目的是鼓励您不要想当然地认为界面流畅。
更重要的是在脑海中自上而下地描绘应用程序中的所有交互,并选择正确的方法来处理它们。在有些情况下,JavaScript 驱动的动画效果会很好。同时,在有些交互中,本地动画(或完全本地的视图)是使其流畅的唯一方法。
有了这种方法,您创建的应用程序将更加流畅和快捷。它不仅能让用户愉快地使用,还能让你在开发过程中调试并享受其中的乐趣。
如果您喜欢这篇文章,请查看 React Native Show 第 20 集,其中 Łukasz Chludziński 和 Burger 讨论了 React Native 中的 Remotion 和动画。