在使用应用程序时,对象、页面、模态框和其他组件的流畅移动可以提升我们的用户体验,并鼓励用户回归。没有人愿意使用出现故障且无法正常移动的应用程序。
对于前端开发人员来说,创建动画和对象过渡可能是一种负担,因为我们通常希望专注于为我们的应用编写代码,而不是去计算在哪里放置一个对象,或者当用户在我们的应用上触发一个事件时,应该将该对象移动到哪里。
与UI设计师合作也可能是一项挑战,尤其是当期望不一致时——例如,当设计师期望看到他们的复杂动画被原样重现。找到一个好的工具和包来解决这个问题也不那么容易——但这正是为什么 react-native-reanimated
包被构建的原因。
react-native-reanimated
npm包让我们能够轻松创建简单和复杂的,但流畅和互动的动画。一开始使用可能并不直观,但是随着我们在这篇文章中练习示例,我们应该能够理解如何使用这个包。
在我们继续之前,我假设你已经对React Native及其工作原理有所了解。此外,React Native Reanimated v3只支持React Native v0.64或更高版本,因此请确保你更新或下载最新版本的React Native来配合这个教程。
如果你曾经使用过React Native Reanimated v1,请记住v2引入了一些新的概念和重大变化。现在让我们来探索一些这些变化。
Reanimated 2
中的重大变化包括一些需要记住的名称变化。例如, interpolate 方法现在是 interpolateNode
,而 Easing
方法现在是 EasingNode
。
在React Native Reanimated v2 中,动画是一等公民。动画以worklets 的形式用纯JS编写。
Worklets 是由 Reanimated Babel 插件从主 React Native 代码中选取的JavaScript代码片段。这些代码片段在一个单独的线程中运行,使用一个单独的JavaScript虚拟机上下文,并在UI线程上同步执行。
要创建一个工作集,你需要在函数顶部明确添加 worklet
指令。请参见下面的代码:
function someWorklet(greeting) {
'worklet';
console.log("Hey I'm running on the UI thread");
}
所有带有 worklet
指令的函数都被选中并在一个单独的JavaScript线程中运行。你也可以通过使用 runOnUI
来调用或执行函数,从而在UI线程中运行你的函数,就像这样:
function FirstWorklet(greeting) {
'worklet';
console.log("Hey I'm running on the UI thread");
}
function secondFunction() {
console.log("Hello World!");
}
function onPress() {
runOnUI(secondFunction)();
}
在上述代码中, FirstWorklet
是唯一的worklet函数,因此它将被Reanimated Babel插件选中并在一个单独的线程中运行。与此同时,其他函数将在主JavaScript线程中同步运行。
在Reanimated中,共享值是在JavaScript端编写但用于驱动UI线程动画的原始值。
在探索worklets时,我们了解到 Reanimated 在一个单独的线程中使用一个单独的JavaScript虚拟机上下文运行我们的动画。当我们在主线程或JavaScript中创建一个值时,共享值会保持对那个可变值的引用。
由于它是可变的,我们可以对共享值进行更改,并在UI线程中看到引用的更改,以及我们的动画中的更改。
参见下方的代码:
import React from 'react';
import {Button, View} from 'react-native';
import {useSharedValue} from 'react-native-reanimated';
function App() {
const sharedValue = useSharedValue(0);
return (
<View>
<Button title="Increase Shared Value" onPress={() => (sharedValue.value += 1)} />
</View>
);
}
如我们在上面的代码中可以看到,共享值对象作为对共享数据片段的引用,可以使用它们的 .value
属性进行访问。因此,要访问和修改我们的共享数据,请使用上面看到的 .value
属性。
我们使用 useSharedValue 钩子来在我们的共享值引用中创建和存储数据。
useAnimatedStyle
钩子使我们能够在共享值和 View
属性之间创建关联,以使用共享值来动画化我们的 View 样式。
让我们看一下下面的代码:
import { useAnimatedStyle } from 'react-native-reanimated';
const Animation = useAnimatedStyle(() => {
return {
...style animation code
};
});
useAnimatedStyle
钩子返回我们更新的动画样式,获取共享值,然后执行更新和样式。
就像 useEffect
钩子一样, useAnimatedStyle
钩子接受一个依赖项。当我们像上面那样省略依赖项时,只有在主体发生变化时才会调用更新。
然而,如果包含了依赖项, useAnimatedStyle
钩子会在依赖值发生变化时运行更新,如下所示:
import { useAnimatedStyle } from 'react-native-reanimated';
const Animation = useAnimatedStyle(() => {
return {
...style animation code
};
}, [ dependency] );
现在我们已经探索了一些在React Native Reanimated v2中引入的新概念,我们将使用这些新概念在我们的应用程序中创建动画。
要使用React Native Reanimated库,我们首先需要安装这个库。运行下面的任一命令来安装这个包:
// yarn
yarn add react-native-reanimated
// npm
npm i react-native-reanimated --save
接下来,进入你的 babel.config.js
文件,并按照下面所示添加插件:
module.exports = {
presets: [
...
],
plugins: [
...
'react-native-reanimated/plugin',
],
};
请注意,React Native Reanimated插件必须最后添加。
接下来,在你的 App.js 文件中,让我们创建一个带有一些标题和正文内容的简单页面:
import React from 'react';
import {View, Button, Text, StyleSheet} from 'react-native';
const App = () => {
return (
<View style={styles.parent}>
<Text style={styles.header}>React Native Reanimated Tutorial</Text>
<View style={styles.box}>
<Button title="View more" />
<View style={{marginTop: 20}}>
<Text style={styles.textBody}>
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Pariatur
magnam necessitatibus dolores qui sunt? Mollitia nostrum placeat
esse commodi modi quaerat, et alias minima, eligendi ipsa
perspiciatis, totam quod dolorum.
{'\n'}
{'\n'}
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Pariatur
magnam necessitatibus dolores qui sunt? Mollitia nostrum placeat
esse commodi modi quaerat, et alias minima, eligendi ipsa
perspiciatis, totam quod dolorum.
</Text>
</View>
</View>
</View>
);
};
export default App;
const styles = StyleSheet.create({
parent: {
flex: 1,
paddingTop: 40,
paddingHorizontal: 20,
},
header: {
fontSize: 24,
marginBottom: 20,
textAlign: 'center',
},
box: {
backgroundColor: '#000',
borderRadius: 15,
padding: 20,
},
textBody: {
fontSize: 20,
marginBottom: 20,
},
});
目标是创建一个动画下拉文本,当我们点击按钮时,它会显示出来
我们使用 useAnimatedStyle
Hook 和 Shared Values
来实现我们想要的动画,如下所示:
import React, {useState} from 'react';
import {View, Button, Text, StyleSheet} from 'react-native';
import Animated, {
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated';
const App = () => {
const boxHeight = useSharedValue(60);
const [maxLines, setMaxLines] = useState(2);
const truncatedAnimation = useAnimatedStyle(() => {
return {
height: withTiming(boxHeight.value, {duration: 1000}),
};
}, []);
function showText() {
setTimeout(() => {
maxLines === 2 ? setMaxLines(0) : setMaxLines(2);
}, 400);
boxHeight.value === 60 ? (boxHeight.value = 150) : (boxHeight.value = 60);
}
return (
<View style={styles.parent}>
<Text style={styles.header}>React Native Reanimated Tutorial</Text>
<View style={styles.box}>
<Button
title={maxLines === 2 ? 'View more' : 'Close Dropdown'}
onPress={showText}
/>
<Animated.View style={[{marginTop: 20}, truncatedAnimation]}>
<Text style={styles.textBody} numberOfLines={maxLines}>
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Pariatur
magnam necessitatibus dolores qui sunt? Mollitia nostrum placeat
esse commodi modi quaerat, et alias minima, eligendi ipsa
perspiciatis, totam quod dolorum.
{'\n'}
{'\n'}
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Pariatur
magnam necessitatibus dolores qui sunt? Mollitia nostrum placeat
esse commodi modi quaerat, et alias minima, eligendi ipsa
perspiciatis, totam quod dolorum.
</Text>
</Animated.View>
</View>
</View>
);
};
export default App;
const styles = StyleSheet.create({
parent: {
flex: 1,
paddingTop: 40,
paddingHorizontal: 20,
},
header: {
fontSize: 24,
marginBottom: 20,
textAlign: 'center',
},
box: {
backgroundColor: '#000',
borderRadius: 15,
padding: 20,
},
textBody: {
fontSize: 20,
marginBottom: 20,
},
});
在我们上面的代码中,我们首先导入了 react-native-reanimated
包。接下来,我们使用了 Animated 选项为我们的 View (我们从 react-native-reanimated
中导入的)包装了我们想要动画化的视图。
之后,我们使用 useSharedValue
将我们的盒子初始高度设置为 60
。我们还有一个 maxLines
状态,我们将其设置为 2
。 maxLines 状态决定了我们截断文本的行数,这种情况下最多为 2
行。
showText
函数在点击按钮时检查高度是否为 60
,如果是,它会将高度增加到 150
。如果高度已经是 150
,那么在点击按钮时,它会减少到 60
。
我们也将 maxLines
的状态设置为 0 ,意味着我们希望在点击“查看更多”按钮时显示完整的文本。
通过 useAnimatedStyle
钩子,我们在我们的共享值和我们正在进行的 View 之间创建了动画关联。
你可以在下面看到结果:
React Native Reanimated v3并未引入任何破坏性变化,就像v2版本一样。因此,所有在v2中编写的代码在v3中都能正常工作。然而,我们引入了一个额外的特性,我们将对其进行详细的查看。这个特性被称为共享元素过渡。
在v3中引入的这个新功能是一个过渡特性,允许你在导航屏幕之间进行视图动画。 sharedTransitionTag
是允许我们在导航之间为屏幕添加动画的属性。
要在屏幕之间创建共享过渡动画,只需将相同的 sharedTransitionTag
分配给两个组件。当你在屏幕之间导航时,共享过渡动画将自动播放。
要查看其工作原理,我们将会对一个图像屏幕进行动画处理。
首先,创建四个文件 — ImageDescription.js
, Home.js
, SharedElements.js
,以及我们的根文件, App.js
。复制并粘贴如下所示的每个文件的相关代码。
在你的 App.js
文件中:
import * as React from 'react';
import {NavigationContainer} from '@react-navigation/native';
import {createNativeStackNavigator} from '@react-navigation/native-stack';
import Home from './src/Home';
import ImageDescription from './src/ImageDescription';
const Stack = createNativeStackNavigator();
function App() {
return (
<NavigationContainer>
<Stack.Navigator initialRouteName="Home">
<Stack.Screen
name="Home"
component={Home}
options={{
title: 'Posts',
headerTintColor: '#000',
headerTitleAlign: 'center',
headerTitleStyle: {
fontWeight: 'bold',
},
}}
/>
<Stack.Screen
name="ImageDescription"
component={ImageDescription}
options={{
title: 'Image',
headerTintColor: '#000',
headerTitleAlign: 'center',
headerTitleStyle: {
fontWeight: 'bold',
},
}}
/>
</Stack.Navigator>
</NavigationContainer>
);
}
export default App;
App.js
文件存放我们的导航组件。我们有两个屏幕 - Home
和 ImageDescription
屏幕。我们也在这里自定义我们的导航头部,以符合我们偏好的样式。
接下来,在你的 Home.js
文件中:
import React from 'react';
import {
Image,
StyleSheet,
Dimensions,
SafeAreaView,
TouchableOpacity,
} from 'react-native';
import Animated from 'react-native-reanimated';
import {sharedElementTransition} from './SharedElements';
import Img1 from '../assets/image1.jpg';
import Img2 from '../assets/image2.jpg';
import Img3 from '../assets/image3.jpg';
import Img4 from '../assets/image4.jpg';
const imageArray = [
{
id: 1,
img: Img1,
description: `Image 1. To create a shared transition animation between two components on different screens, simply assign the same sharedTransitionTag to both components. When you navigate between screens, the shared transition animation will automatically play. The shared transition feature works by searching for two components that have been registered with the same sharedTransitionTag. If you want to use more than one shared view on the same screen, be sure to assign a unique shared tag to each component.`,
},
{
id: 2,
img: Img2,
description: `Image 2. "Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit..."
"There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain..."
`,
},
{
id: 3,
img: Img3,
description: `Image 3. "Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit..."
"There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain..."
`,
},
{
id: 4,
img: Img4,
description: `Image 4. "Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit..."
"There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain..."
`,
},
{
id: 5,
img: Img3,
description: `Image 5. "Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit..."
"There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain..."
`,
},
{
id: 8,
img: Img2,
description: `Image 8. "Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit..."
"There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain..."
`,
},
{
id: 7,
img: Img1,
description: `Image 7. "Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit..."
"There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain..."
`,
},
{
id: 6,
img: Img4,
description: `Image 6. "Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit..."
"There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain..."
`,
},
];
export default function Home({navigation}) {
return (
<SafeAreaView style={styles.container}>
{imageArray.map(image => (
<TouchableOpacity
key={image.id}
onPress={() =>
navigation.navigate('ImageDescription', {
image: image,
})
}>
<Animated.View
style={styles.imageContainer}
sharedTransitionTag="animateImageTag"
sharedTransitionStyle={sharedElementTransition}>
<Image source={image.img} style={styles.image} />
</Animated.View>
</TouchableOpacity>
))}
</SafeAreaView>
);
}
const windowWidth = Dimensions.get('window').width;
const styles = StyleSheet.create({
container: {
flex: 1,
flexWrap: 'wrap',
flexDirection: 'row',
alignItems: 'center',
},
imageContainer: {
height: 140,
width: windowWidth / 3,
},
image: {
flex: 1,
resizeMode: 'cover',
width: '100%',
},
});
在 Home
组件中,我们有一些图片,我们正在映射并在我们的屏幕上显示,如下所示:
当点击一个图片时,它会引导用户跳转到另一个屏幕—— ImageDescription
组件——该屏幕会显示被点击的图片,一个标题,以及图片下方的描述,如下所示:
要设置此组件,请将下面的内容复制到你的 ImageDescription.js
文件中:
import React from 'react';
import Animated from 'react-native-reanimated';
import {sharedElementTransition} from './SharedElements';
import {View, Text, StyleSheet, Image, Dimensions} from 'react-native';
export default function ImageDescription({route}) {
const {image} = route.params;
return (
<View style={styles.container}>
<Text style={styles.title}>Shared Elements Transition</Text>
<Animated.View
style={styles.imageContainer}
sharedTransitionTag="animateImageTag"
sharedTransitionStyle={sharedElementTransition}>
<Image source={image.img} style={styles.image} />
</Animated.View>
<Text style={styles.description}>{image.description}</Text>
</View>
);
}
const windowWidth = Dimensions.get('window').width;
const styles = StyleSheet.create({
container: {
flex: 1,
},
title: {
fontSize: 24,
fontWeight: 'bold',
color: '#000',
marginHorizontal: 10,
marginVertical: 5,
},
imageContainer: {
height: 500,
width: windowWidth,
},
image: {
height: '100%',
width: '100%',
},
description: {
fontSize: 20,
color: '#000',
marginHorizontal: 10,
marginVertical: 5,
},
});
接下来,我们将从 react-native-reanimated
中引入我们的动画视图。动画视图被包裹在你想要动画化的部分周围,在我们的情况下,是图片。 Animated.View
接受一些选项,如 sharedTransitionTag
和 sharedTransitionStyle
。
将以下内容添加到你的 SharedElements.js
文件中:
import {SharedTransition, withSpring} from 'react-native-reanimated';
const CONFIG = {
mass: 1,
stiffness: 100,
damping: 200,
};
export const sharedElementTransition = SharedTransition.custom(values => {
'worklet';
return {
height: withSpring(values.currentHeight, CONFIG),
width: withSpring(values.currentWidth, CONFIG),
originX: withSpring(values.currentOriginX, CONFIG),
originY: withSpring(values.currentOriginY, CONFIG),
};
});
在上述代码中, sharedTransitionTag
让Reanimated能够检测到需要动画的组件。因此,具有相同 sharedTransitionTag
的组件之间会共享动画。
与此同时, sharedTransitionStyle
允许你为动画组件的 height
, width
, originY
和 originX
编写自定义样式。
完整的应用程序将如下所示:
我们已经学习了在React Native Reanimated v2和v3中引入的变化,核心特性,以及如何使用这些核心特性。我们还看到了如何使用我们在文章中学到的概念来创建平滑的过渡和动画。
"共享元素过渡"功能仍处于实验阶段,这意味着正在开发更稳定的版本。与此同时,你可以使用它并了解其工作原理,尽管你可能还不想在生产环境中使用它。
首发于公众号 大迁世界,欢迎关注。📝 每周一篇实用的前端文章 🛠️ 分享值得关注的开发工具 ❓ 有疑问?我来回答
本文 GitHub https://github.com/qq449245884/xiaozhi 已收录,有一线大厂面试完整考点、资料以及我的系列文章。