使用React Native Reanimated实现无缝的UI过渡

目录
文章目录隐藏
  1. 探索 React Native Reanimated v2
  2. Shared Values (共享值)
  3. useAnimatedStyle Hook
  4. 使用新的 React Native Reanimated 概念
  5. 介绍 React Native Reanimated v3 的新功能
  6. 总结

在使用应用程序时,对象、页面、模态框和其他组件的流畅移动可以提升我们的用户体验,并鼓励用户回归。没有人愿意使用出现故障且无法正常移动的应用程序。

对于前端开发人员来说,创建动画和对象过渡可能是一种负担,因为我们通常希望专注于为我们的应用编写代码,而不是去计算在哪里放置一个对象,或者当用户在我们的应用上触发一个事件时,应该将该对象移动到哪里。

与 UI 设计师合作也可能是一项挑战,尤其是当期望不一致时——例如,当设计师期望看到他们的复杂动画被原样重现。找到一个好的工具和包来解决这个问题也不那么容易——但这正是为什么 react-native-reanimated 包被构建的原因。

react-native-reanimated npm 包让我们能够轻松创建简单和复杂的,但流畅和互动的动画。一开始使用可能并不直观,但是随着我们在这篇文章中练习示例,我们应该能够理解如何使用这个包。

在我们继续之前,我假设你已经对 React Native 及其工作原理有所了解。此外,React Native Reanimated v3 只支持 React Native v0.64 或更高版本,因此请确保你更新或下载最新版本的 React Native 来配合这个教程。

探索 React Native Reanimated v2

如果你曾经使用过 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 线程中同步运行。

Shared Values (共享值)

在 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 Hook

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 概念

现在我们已经探索了一些在 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 的新功能

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 组件中,我们有一些图片,我们正在映射并在我们的屏幕上显示,如下所示:

使用 React Native Reanimated 实现无缝的 UI 过渡

当点击一个图片时,它会引导用户跳转到另一个屏幕—— 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 中引入的变化,核心特性,以及如何使用这些核心特性。我们还看到了如何使用我们在文章中学到的概念来创建平滑的过渡和动画。

“共享元素过渡”功能仍处于实验阶段,这意味着正在开发更稳定的版本。与此同时,你可以使用它并了解其工作原理,尽管你可能还不想在生产环境中使用它。

「点点赞赏,手留余香」

0

给作者打赏,鼓励TA抓紧创作!

微信微信 支付宝支付宝

还没有人赞赏,快来当第一个赞赏的人吧!

声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系maynote@foxmail.com处理
码云笔记 » 使用React Native Reanimated实现无缝的UI过渡

发表回复