有效地实施动画对于创建出彰显自我特色的移动应用至关重要。动画有力量赋予屏幕的各个元素和部分生命力,提升整体用户体验,增强用户对我们应用的忠诚度。

为了无缝设置动画,我们需要准确捕捉元素上的用户手势,而不影响我们应用程序的性能或引入尴尬的动画。

我们可以在我们的应用中使用React Native Gesture Handler来捕获各种用户手势,但是这个包可能无法提供一些计算和其他操作。在这些情况下,React Native Redash包可以填补这些空白。

在这篇文章中,我们将比较 React Native Redash 和 React Native Gesture Handler,以理解每个包的不同用途。为了深化我们对React Native动画的理解,我们还将探讨相关概念,如原生手势处理器,React Native Reanimated和Animated API。

什么是React Native手势处理器?

React Native Gesture Handler 是一个强大的库,你可以在React Native应用程序中使用它来处理滑动、拖动、轻触等手势。

通常,这个库并不使用内置在React Native中的手势响应系统。相反,它使用 UIGestureRecognizer 用于iOS设备,并为Android设备实现了一个自定义的手势响应系统。

与其花时间讨论React Native Gesture Handler的内部工作机制,不如直接深入探索其关键特性。

探索React Native手势处理器的关键特性

简而言之,React Native Gesture Handler库为以下类型的手势提供了处理程序:

  • Pan gesture 平移手势
  • Tap gesture 点击手势
  • Long press gesture 长按手势
  • Rotation gesture 旋转手势
  • Fling gesture 甩动手势
  • Pinch gesture 捏合手势

要在React Native项目中使用任何处理程序,我们首先必须设置一个React Native项目。为了本教程的目的,你可以克隆我已经准备好的入门项目

一旦我们的代码准备好了,我们可以安装我们需要的依赖项 - 包括React Native Gesture HandlerReact Native Reanimated - 通过在终端运行以下任一命令:

//npm
npm install && npm install react-native-gesture-handler react-native-reanimated

//Yarn
yarn && npm install react-native-gesture-handler react-native-reanimated

在此刻,我们应确保我们的文件结构与我们下面所拥有的匹配:

react-native-gestures
 ┣ .expo
 ┃ ┣ README.md
 ┃ ┗ devices.json
 ┣ assets
 ┃ ┣ adaptive-icon.png
 ┃ ┣ favicon.png
 ┃ ┣ icon.png
 ┃ ┗ splash.png
 ┣ components
 ┃ ┗ examples-with-rngh-and-rnr
 ┃ ┃ ┣ FlingGesture.jsx
 ┃ ┃ ┣ ForceTouchGesture.jsx
 ┃ ┃ ┣ LongPressGesture.jsx
 ┃ ┃ ┣ ManualGesture.jsx
 ┃ ┃ ┣ NativeGesture.jsx
 ┃ ┃ ┣ PanGesture.jsx
 ┃ ┃ ┣ RotationGesture.jsx
 ┃ ┃ ┗ TapGesture.jsx
 ┣ .gitignore
 ┣ App.js
 ┣ app.json
 ┣ babel.config.js
 ┣ package-lock.json
 ┣ package.json
 ┗ yarn.lock

为了确保我们的应用程序能够捕获手势,我们需要用 GestureHandlerRootView 包裹根组件。在这种情况下, App.js 是根文件,它应该看起来类似于这样:

import { StatusBar } from 'expo-status-bar';
import { Platform, SafeAreaView, StyleSheet, View } from 'react-native';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import PanGesture from './components/examples-with-rngh-and-rnr/PanGesture';

export default function App() {
  return (
    <SafeAreaView style={{ flex: 1 }}>
      <GestureHandlerRootView style={{ flex: 1 }}>
        <View style={styles.container}>
          <StatusBar style='auto' />
          <PanGesture />
        </View>
      </GestureHandlerRootView>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    marginTop: Platform.OS === 'android' ? '10%' : '3%',
  },
});

我们还需要将 React Native Reanimated 插件添加到我们的 babel.config.js 文件中:

// babel.config.js
module.exports = function (api) {
  api.cache(true);
  return {
    presets: ['babel-preset-expo'],
    plugins: ['react-native-reanimated/plugin'],
  };
};

一旦我们完成了那个,我们就可以探索各种手势处理方法,以实际理解React Native手势处理器是如何工作的。

平移手势处理器

平移手势处理器捕获了在屏幕上拖动手指的动作。这包括在屏幕上的垂直、水平甚至对角线的移动。

接下来,我们将使用平移手势处理器在屏幕上设置三个可拖动的形状——一个正方形,一个圆形和一个矩形。我们还将使用React Native Reanimated库来处理这些对象在屏幕上的平滑移动。

让我们从打开 components/examples-with-rngh-and-rnr/PanGesture.jsx 文件开始,并导入我们需要的组件:

// components/examples-with-rngh-and-rnr/PanGesture.jsx
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { GestureDetector, Gesture } from 'react-native-gesture-handler';
import Animated, { useAnimatedStyle, useSharedValue } from 'react-native-reanimated';

React Native Gesture Handler 提供了两个重要的组件:

  • GestureDetector — 包裹将要执行手势的元素
  • Gesture — 一个包含各种手势的类,包括平移手势

同样,React Native Reanimated 库提供了以下内容:

  • 一个 Animated 类,它使我们能够为组件配置动画功能
  • useAnimatedStyle 钩子,我们可以像使用原生的 StyleSheet.create({}) 一样来创建样式对象
  • useSharedValue 钩子,其行为类似于原生的 useRef 钩子,以避免在我们更新状态时重新渲染

我们将在三个代码块中使用 useSharedValue 钩子。在下面显示的第一个代码块中,我们使用它来初始化每个形状的背景颜色:

const PanGestureExample = () => {
  const squareColor = useSharedValue(styles.square.backgroundColor);
  const circleColor = useSharedValue(styles.circle.backgroundColor);
  const rectangleColor = useSharedValue(styles.rectangle.backgroundColor);
}

为了确保一定程度的控制,我们在 App.js 文件的 styles 块中引用了我们定义的背景颜色,例如 styles.square.backgroundColor

第二部分处理我们用来记住每个形状最后位置的状态集。这确保我们在尝试拖动每个形状时,防止它们回弹到初始位置。

  const squareLastOffset = useSharedValue({ horizontal: 0, vertical: 0 });
  const circleLastOffset = useSharedValue({ horizontal: 0, vertical: 0 });
  const rectangleLastOffset = useSharedValue({ horizontal: 0, vertical: 0 });

每个状态都包含一个对象,该对象具有 horizontalvertical 属性,初始值为 0

第三个模块与第二个模块相似。我们使用其中定义的状态来存储每个形状的当前位置。

 const squarePosition = useSharedValue({ horizontal: 0, vertical: 0 });
  const circlePosition = useSharedValue({ horizontal: 0, vertical: 0 });
  const rectanglePosition = useSharedValue({ horizontal: 0, vertical: 0 });

如前所述,我们可以使用 useAnimatedStyle 钩子来定义当我们将样式应用到组件时会被动画化的样式。因此,在我们的组件中,我们使用了三个 useAnimatedStyle 实例来为每个形状提供两种主要样式—— transformbackgroundColor

const squareAnimatedStyle = useAnimatedStyle(() => ({
    transform: [
      { translateX: squarePosition.value.horizontal },
      { translateY: squarePosition.value.vertical },
    ],
    backgroundColor: squareColor.value,
  }));

  const circleAnimatedStyle = useAnimatedStyle(() => ({
    transform: [
      { translateX: circlePosition.value.horizontal },
      { translateY: circlePosition.value.vertical },
    ],
    backgroundColor: circleColor.value,
  }));

  const rectangleAnimatedStyle = useAnimatedStyle(() => ({
    transform: [
      { translateX: rectanglePosition.value.horizontal },
      { translateY: rectanglePosition.value.vertical },
    ],
    backgroundColor: rectangleColor.value,
  }));

transform 样式接受一个包含样式及其对应值的对象数组,例如 translateX 。我们可以分别从 squareAnimatedStylecircleAnimatedStylerectangleAnimatedStyle 变量中传递适当的值。

我们将在下面创建的 styles 对象包含了用于包裹整个组件的 View 的样式,以及每个形状——正方形、圆形和矩形的样式。

在下一个代码块中,我们将使用 Pan 手势处理器。注意,要使用任何手势处理器,我们首先需要调用 Gesture 类,然后调用我们想要的处理器,如 Gesture.Pan()Gesture.Tap()

const squareGesture = Gesture.Pan()
    .onStart((e) => {
      squareColor.value = '#000080';
    })
    .onUpdate((e) => {
      squarePosition.value = {
        horizontal: squareLastOffset.value.horizontal + e.translationX,
        vertical: squareLastOffset.value.vertical + e.translationY,
      };
    })
    .onEnd((e) => {
      squareLastOffset.value = {
        horizontal: squarePosition.value.horizontal,
        vertical: squarePosition.value.vertical,
      };
    })
    .onFinalize((e) => {
      squareColor.value = styles.square.backgroundColor;
    });

  const circleGesture = Gesture.Pan()
    .onStart((e) => {
      circleColor.value = '#228C22';
    })
    .onUpdate((e) => {
      circlePosition.value = {
        horizontal: circleLastOffset.value.horizontal + e.translationX,
        vertical: circleLastOffset.value.vertical + e.translationY,
      };
    })
    .onEnd((e) => {
      circleLastOffset.value = {
        horizontal: circlePosition.value.horizontal,
        vertical: circlePosition.value.vertical,
      };
    })
    .onFinalize((e) => {
      circleColor.value = styles.circle.backgroundColor;
    });

  const rectangleGesture = Gesture.Pan()
    .onStart((e) => {
      rectangleColor.value = '#C0362C';
    })
    .onUpdate((e) => {
      rectanglePosition.value = {
        horizontal: rectangleLastOffset.value.horizontal + e.translationX,
        vertical: rectangleLastOffset.value.vertical + e.translationY,
      };
    })
    .onEnd((e) => {
      rectangleLastOffset.value = {
        horizontal: rectanglePosition.value.horizontal,
        vertical: rectanglePosition.value.vertical,
      };
    })
    .onFinalize((e) => {
      rectangleColor.value = styles.rectangle.backgroundColor;
    });

每种方法都有一般的回调,如 onStartonBeginonEnd 等。此外,还有一些特定于每种方法的回调。对于 Gesture.Pan() 方法,这些包括 minDistancemaxPointers 等等。

通常, onStartonUpdateonEndonFinalize 是最常用的回调函数:

  • onStart 回调在手势被识别时被调用。在我们的情况下,我们正在改变受影响项目的 backgroundColor
  • onUpdate 回调中,我们正在计算并设置每个项目的 positionlastOffset 状态,例如使用 rectangleLastOffsetrectanglePosition ,以及事件中的当前 translateXtranslateY
  • onEnd 回调中,我们正在设置 lastOffset 状态,例如用 rectanglePosition 设置 rectangleLastOffset ,以便我们可以记住每个项目的最后位置
  • 在每个 onFinalize 回调中,我们都将每个项目的背景颜色重置为它们的初始值

最后,我们可以在 return 语句中将我们写的所有内容呈现在屏幕上。我们首先用 View 包裹整个组件。然后,用相应的 gesture 包裹每个形状的 GestureDetector ,围绕 Animated.View

return (
    <View style={styles.container}>
      <GestureDetector gesture={squareGesture}>
        <Animated.View style={[styles.square, squareAnimatedStyle]} />
      </GestureDetector>
      <GestureDetector gesture={circleGesture}>
        <Animated.View style={[styles.circle, circleAnimatedStyle]} />
      </GestureDetector>
      <GestureDetector gesture={rectangleGesture}>
        <Animated.View style={[styles.rectangle, rectangleAnimatedStyle]} />
      </GestureDetector>
    </View>
  );
};
export default PanGestureExample;

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  square: {
    height: 50,
    width: 50,
    backgroundColor: 'blue',
  },
  circle: {
    height: 50,
    width: 50,
    borderRadius: 25,
    backgroundColor: 'green',
    marginTop: 10,
  },
  rectangle: {
    height: 50,
    width: 100,
    backgroundColor: 'orange',
    marginTop: 10,
  },
});

请注意,每个 Animated.View 都包含每种形状的适当 styleanimatedStyle ,例如圆形的 styles.circlecircleAnimatedStyle

在此刻,我们的应用应该像下图中那样运行:

你可以在GitHub上查看这个示例的完整源代码。

点击手势处理器

我们可以使用轻触手势处理器来捕获屏幕上的不同种类的短按,包括对一个对象的单击、双击和三击。我们将以与平移手势文件类似的方式设置我们的 components/examples-with-rngh-and-rnr/TapGesture.jsx 文件:

// components/examples-with-rngh-and-rnr/TapGesture.jsx

import { Button, StyleSheet, Text, View } from 'react-native';
import React from 'react';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
  useAnimatedStyle,
  useSharedValue,
} from 'react-native-reanimated';

水龙头手势处理器使用的API与平移手势处理器类似。我们只需要将 Gesture.Pan() 方法替换为 Gesture.Tap() 方法。

在这种情况下,我们使用 useSharedValue 钩子来定义三种状态—— singleTapCountdoubleTapCounttripleTapCount ——用于保存点击次数:

const TapGestures = () => {
  const singleTapCount = useSharedValue(0);
  const doubleTapCount = useSharedValue(0);
  const tripleTapCount = useSharedValue(0);

然后,我们使用 useAnimatedStyle 钩子来定义每个对象在点击其 numberOfTaps 变量中定义的次数时将移动的距离。我们只需简单地调整每个对象的 marginLeft 值即可。

const singleTapAnimatedStyles = useAnimatedStyle(() => ({
    marginLeft: singleTapCount.value * 30,
  }));
  const doubleTapAnimatedStyles = useAnimatedStyle(() => ({
    marginLeft: doubleTapCount.value * 30,
  }));
  const tripleTapAnimatedStyles = useAnimatedStyle(() => ({
    marginLeft: tripleTapCount.value * 30,
  }));

  const singleTap = Gesture.Tap()
    // .maxDuration(250)
    .numberOfTaps(1)
    .onStart(() => {
      singleTapCount.value = singleTapCount.value + 1;
    });
  const doubleTap = Gesture.Tap()
    // .maxDuration(250)
    .numberOfTaps(2)
    .onStart(() => {
      doubleTapCount.value = doubleTapCount.value + 1;
    });
  const tripleTap = Gesture.Tap()
    // .maxDuration(250)
    .numberOfTaps(3)
    .onStart(() => {
      tripleTapCount.value = tripleTapCount.value + 1;
    });

请注意,我们也可以为轻击手势处理器设置其他配置,例如上述交互集的 maxDuration

我们也有一个 clearState 功能可以使用。在这种情况下,我们将这个功能连接到一个 button ,用于重置状态的值:

  const clearState = () => {
    singleTapCount.value = 0;
    doubleTapCount.value = 0;
    tripleTapCount.value = 0;
  };

接下来,我们在我们的 return 语句中渲染每个组件,并更新 App.js 文件:

return (
    <View style={styles.container}>
      <GestureDetector gesture={singleTap}>
        <View style={[styles.itemWrapper]}>
          <Text>Single Tap to move </Text>
          <Animated.View style={[styles.square, singleTapAnimatedStyles]} />
        </View>
      </GestureDetector>
      <GestureDetector gesture={doubleTap}>
        <View style={styles.itemWrapper}>
          <Text>Double Tap to move</Text>
          <Animated.View style={[styles.circle, doubleTapAnimatedStyles]} />
        </View>
      </GestureDetector>
      <GestureDetector gesture={tripleTap}>
        <View style={styles.itemWrapper}>
          <Text>Triple Tap to move</Text>
          <Animated.View style={[styles.rectangle, tripleTapAnimatedStyles]} />
        </View>
      </GestureDetector>
      <Button title='Clear state' onPress={clearState} />
    </View>
  );
};
export default TapGestures;

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  itemWrapper: {
    borderWidth: 1,
    margin: 20,
  },
  square: {
    height: 50,
    width: 50,
    backgroundColor: 'blue',
  },
  circle: {
    height: 50,
    width: 50,
    borderRadius: 25,
    backgroundColor: 'green',
    marginTop: 10,
  },
  rectangle: {
    height: 50,
    width: 100,
    backgroundColor: 'orange',
    marginTop: 10,
  },
});

一旦我们完成了上述所有操作,我们应该能看到一个如下图所示的功能屏幕:

长按处理器

我们可以使用 Gesture.LongPress() 方法配置长按处理器。它接受 minDurationmaxDistance 值,我们需要正确地配置它。

在下面的示例中,我们将构建一个简单的组件,它会渲染一个 Animated.Text 组件和两个按钮。

当用户按下第一个按钮并至少持续 3000 毫秒,或三秒钟,我们将把 isLongPressed useSharedValue 改为 true ,然后显示 Animated.Text 组件。

我们将设置最后一个按钮,以便在点击时重置 isLongPressed 状态的值。

参见下方的代码:

// components/examples-with-rngh-and-rnr/LongPressGesture.jsx

import { Button, StyleSheet, Text, View } from 'react-native';
import React from 'react';
import Animated, {
  useAnimatedStyle,
  useSharedValue,
} from 'react-native-reanimated';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';

const LongPressGesture = () => {
  const isLongPressed = useSharedValue(false);
  const animatedStyle = useAnimatedStyle(() => ({
    display: isLongPressed.value ? 'flex' : 'none',
  }));

  const longPressHandler = Gesture.LongPress()
    .minDuration(3000)
    .maxDistance(10)
    .onStart((e) => {})
    .onEnd((e) => {
      isLongPressed.value = true;
      // User has pressed for the minimum duration(3000 milliseconds or 3 seconds) we set
    });

  const clearStates = () => {
    isLongPressed.value = false;
  };

  return (
    <View style={styles.container}>
      <GestureDetector gesture={longPressHandler}>
        <View style={{ marginVertical: 50 }}>
          <Animated.Text style={[animatedStyle]}>
            Yay!!! You pressed and held for 3 seconds
          </Animated.Text>
          <Button title='Press and hold me' />
        </View>
      </GestureDetector>
      <Button title='Clear' onPress={clearStates} />
    </View>
  );
};
export default LongPressGesture;
const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
});

这段代码的结果应该看起来像下面的GIF图:

旋转手势处理器

旋转手势处理器捕获了一种特殊的手势,这需要用户在一个对象上按住两个或更多的手指,并在保持手指触屏的状态下,围绕一个轴移动。你可以在下面的图片中看到这种行为的示例:

我们看看如何生成上述屏幕。与我们处理其他组件的方式类似,我们需要用 GestureDetector 将一个简单的 Animated.View 组件包裹起来。

我们将用一个 Gesture.Rotation() 方法初始化 rotationHandler ,该方法也接受我们在上面的平移手势处理器示例中讨论的回调。我们还将初始化两个 useSharedValue 状态: rotationPointlastRotation

然而,在 onUpdate 回调中,我们将 rotationPoint 设置为来自事件的 lastRotation 和旋转值。然后,我们在 onEnd 回调中将 rotationPoint 设置为 lastRotation 。这确保我们在旋转对象时永远不会看到突兀的动作。

我们还定义了一个 animatedStyle 变量,它只是通过将 rotationPoint 除以 Pi 并乘以 180deg 来设置 rotationZtransform 样式属性中的值。最后,我们将 animatedStyle 传递给 Animated.View 并旋转我们的组件。

请参阅下面的完整代码:

// components/examples-with-rngh-and-rnr/RotationGesture.jsx

import { StyleSheet, Text, View } from 'react-native';
import React from 'react';
import Animated, {
  useAnimatedStyle,
  useSharedValue,
} from 'react-native-reanimated';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';

const RotationGesture = () => {

  const rotationPoint = useSharedValue(0);
  const lastRotation = useSharedValue(0);

  const rotationHandler = Gesture.Rotation()
    .onBegin((e) => {
      console.log({ begin: e });
    })
    .onStart((e) => {
      console.log({ start: e });
    })
    .onUpdate((e) => {
      rotationPoint.value = lastRotation.value + e.rotation;
    })
    .onEnd(() => {
      lastRotation.value = rotationPoint.value;
    });

  const animatedStyle = useAnimatedStyle(() => ({
    transform: [{ rotateZ: `${(rotationPoint.value / Math.PI) * 180}deg` }],
  }));

  return (
    <View style={{ borderWidth: 2 }}>
      <GestureDetector gesture={rotationHandler}>
        <Animated.View style={[styles.square, animatedStyle]} />
      </GestureDetector>
    </View>
  );
};
export default RotationGesture;
const styles = StyleSheet.create({
  square: {
    width: 200,
    height: 200,
    backgroundColor: 'green',
    marginLeft: 'auto',
    marginRight: 'auto',
    marginVertical: 50,
  },
});

甩动手势处理器

这个处理程序捕获对象上的快速或突然的拖动动作。与上面讨论的其他处理程序不同,甩动手势处理程序需要设置一个方向属性,可以是 Right(右)、Left(左)、Top(上)或 Bottom(下)。如果必要,我们可以在每个参数中传递多个方向。

我们看一个示例,说明甩动手势处理程序是如何工作的:

// components/examples-with-rngh-and-rnr/FlingGesture.jsx

import { StyleSheet, Text, View } from 'react-native';
import React from 'react';
import Animated, {
  useAnimatedStyle,
  useSharedValue,
  withTiming,
} from 'react-native-reanimated';

import {
  Directions,
  Gesture,
  GestureDetector,
} from 'react-native-gesture-handler';

const FlingGesture = () => {
  const position = useSharedValue(0);

  const flingGesture = Gesture.Fling()
    .direction(Directions.RIGHT)
    .onStart((e) => {
      position.value = withTiming(position.value + e.x, { duration: 100 });
    });

  const animatedStyle = useAnimatedStyle(() => ({
    transform: [{ translateX: position.value }],
  }));

  return (
    <View style={{ borderWidth: 2 }}>
      <GestureDetector gesture={flingGesture}>
        <Animated.Image
          source={{ uri: 'https://image.pngaaa.com/46/2182046-middle.png' }}
          style={[styles.box, animatedStyle]}
          width={100}
          height={50}
        />
      </GestureDetector>
    </View>
  );
};
export default FlingGesture;
const styles = StyleSheet.create({
  square: {
    width: 40,
    height: 40,
    objectFit: 'contain',
  },
});

需要注意的一点是,我们正在使用 Gesture.Fling() 方法,并传递一个必须的 direction 值。另一点是,与其他手势处理器不同,此处理器不包含 onUpdate 回调。相反,它使用 onStart 方法。

这是一个很好的提醒,在某些情况下,可能会有针对每种方法的特定回调。确保你检查有关正在使用的方法接受哪些回调以及它们产生哪些事件的文档。

要执行实际的快速滑动,我们简单地使用 Reanimated 库中的 withTiming 方法来动态计算和设置对象的位移。最后,我们可以在 animatedStyle 中设置样式,并将其传递到我们在屏幕上渲染的 Animated.Image 中。

可以在下面看到结果:

捏合手势处理器

捏合手势处理器使用 Gesture.Pinch() 方法。它捕捉放在元素上的两个手指的连续移动,并将它们移动得更近或更远。这种移动会将元素放大或缩小。

对于这个演示,我们将简单地构建一个可以从中间放大或缩小的组件。请注意,我们的示例遵循了我们上面构建的旋转手势处理器的相同逻辑模式。

// components/examples-with-rngh-and-rnr/PinchGesture.jsx

import { StyleSheet, Text, View } from 'react-native';
import React from 'react';
import Animated, {
  useAnimatedStyle,
  useSharedValue,
} from 'react-native-reanimated';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';

const PinchGestureExample = () => {
  const scalePoint = useSharedValue(1);
  const savedScale = useSharedValue(1);

  const pinchHandler = Gesture.Pinch()
    .onUpdate((e) => {
      scalePoint.value = savedScale.value * e.scale;
    })
    .onEnd(() => {
      savedScale.value = scalePoint.value;
    });

  const animatedStyle = useAnimatedStyle(() => ({
    transform: [{ scale: scalePoint.value }],
  }));

  return (
    <View style={{ borderWidth: 2 }}>
      <GestureDetector gesture={pinchHandler}>
        <Animated.Image
          source={{
            uri: 'https://images.unsplash.com/photo-1670184900611-434255d918ac?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2942&q=80',
          }}
          style={[styles.square, animatedStyle]}
        />
      </GestureDetector>
    </View>
  );
};
export default PinchGestureExample;
const styles = StyleSheet.create({
  square: {
    width: 400,
    height: 200,
    marginLeft: 'auto',
    marginRight: 'auto',
    marginVertical: 50,
    objectFit: 'cover',
  },
});

上述代码产生了以下结果:

什么是React Native Redash?

React Native Redash 库是一个用于React Native Gesture Handler和React Native Reanimated的实用库。它包含了一些工具,使得创建、控制和自定义动画和手势变得非常容易。

通常,它包含可以用来处理的辅助函数:

  • Animations 动画
  • Coordinates 坐标
  • Math 数学
  • String 字符串
  • Transitions 过渡
  • Vectors 向量
  • Paths 路径
  • Physics 物理学
  • Colors 颜色

通常,上述列出的每一个辅助函数都处理无法单独使用的动画处理器。相反,它们应与React Native手势处理器或React Native Reanimated一起使用。我们在下面更详细地探讨这个问题。

探索 React Native Redash 的关键特性

我们设置一个动画视图和两个按钮,以便在实际示例中看到如何使用React Native Redash的功能。我们将从导入我们需要的组件开始:

import React, { useEffect } from 'react';
import Animated, {
  Easing,
  useAnimatedStyle,
  useSharedValue,
  withRepeat,
  withTiming,
} from 'react-native-reanimated';

import { Button, StyleSheet, View } from 'react-native';
import { withPause } from 'react-native-redash';

我们将为一个 rotation 变量定义一个 useSharedValue ,该变量保存了盒子的旋转状态。同时, paused 状态保存了开始或停止动画的状态,这是在React Native Redash的帮助下实现的。

const duration = 2000;
const easing = Easing.bezier(0.25, -0.5, 0.25, 1);

const WithPauseHelper = () => {
  const rotation = useSharedValue(0);
  const paused = useSharedValue(false);

  const animatedStyle = useAnimatedStyle(() => ({
    transform: [{ rotate: `${rotation.value * 360}deg` }],
  }));

然后,我们将创建 stopAnimationstartAnimation 函数,在其中我们将适当地改变 paused 的值:

  const stopAnimation = () => {
    paused.value = true;
  };
  const startAnimation = () => {
    paused.value = false;
  };

通常,我们可以仅使用 withRepeat() 在React Native Reanimated 中定义一个循环动画。然而,我们可以使用Redash中的 withPause 方法来增强这种行为,如下面的 useEffect 钩子所示,以启动或停止动画:

  useEffect(() => {
    rotation.value = withPause(
      withRepeat(withTiming(1, { duration, easing }), -1),
      paused
    );
  }, []);

这基本上意味着,有了React Native Redash,我们可以随心所欲地开始或停止动画。剩下的只是我们的 return 声明:

  return (
    <View style={styles.container}>
      <Animated.View style={[styles.box, animatedStyle]} />
      <Button title='Cancel' onPress={stopAnimation} />
      <Button title='Start' onPress={startAnimation} />
    </View>
  );
};
export default WithPauseHelper;
const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    height: '100%',
    gap: 70,
  },
  box: {
    height: 100,
    width: 100,
    backgroundColor: 'green',
    borderRadius: 10,
  },
});

如果一切按计划进行,我们应该能看到下面的结果:

React Native Redash 提供了其他对于平滑动画非常重要的数学函数。例如,我们可以使用 bin() 将布尔值转换为数字,使用 toDeg() 将弧度转换为度数。

它还包含像 useTiming() 这样的函数来处理过渡和 useVector() 用于创建向量。在接下来的代码示例中,我们将探索如何使用 useTiming() 方法来过渡一个 useState 值:

// /components/examples-with-rngh-rnredash-reanimated/ButtonTranslate.jsx

import { Button, StyleSheet, Text, View } from 'react-native';
import React, { useState } from 'react';
import { useTiming } from 'react-native-redash';
import Animated, { useAnimatedStyle } from 'react-native-reanimated';

const ButtonTranslate = () => {
  const [open, setOpen] = useState(false);
  const transition = useTiming(open, { duration: 4000 });

  const animatedStyle = useAnimatedStyle(() => ({
    opacity: transition.value,
  }));

  const handleOpen = () => {
    setOpen((prev) => !prev);
  };

  return (
    <View>
      <Animated.View style={[styles.square, animatedStyle]} />
      <Button title='Toggle' onPress={handleOpen} />
    </View>
  );
};
export default ButtonTranslate;

const styles = StyleSheet.create({
  square: {
    width: 200,
    height: 200,
    backgroundColor: 'blue',
    alignSelf: 'center',
  },
});

默认情况下,你不能在React中对状态的改变进行动画处理。然而,我们可以借助 React Native Redash的 useTiming 辅助函数来实现这一点。

在上述代码中,我们有一个 open 状态,我们可以使用 handleOpen 函数来改变其值。我们还有一个 transition 变量 - 它实现了 useTiming 函数 - 以及一个 animatedStyle 变量,该变量将 transition 变量设置为 opacity 值。

然后,在 return 部分,我们渲染了一个 Animated.View 组件和一个 Button ,当被按下时会触发一个过渡。这个过渡会在 4000 毫秒,或四秒的时间跨度内,慢慢隐藏或显示 Animated.View ,如下图所示:

React Native Gesture Handler与React Native Redash:差异

如前所述,React Native Redash 主要提供辅助函数,使创建流畅的动画和过渡以及处理手势变得更容易。相比之下,React Native Gesture Handler 通过与平台的原生代码交互,严格处理对象上的手势和触摸。

由于这些库有不同的用途,实际上无法直接进行比较。然而,以下是React Native Gesture Handler和React Native Redash之间的差异总结,以帮助阐明它们各自的独特功能:

React Native Gesture HandlerReact Native Redash
目的用于处理手势一个用于React Native手势处理器和React Native重新动画的辅助库
动画处理不处理动画,但可以提供事件来控制动画可以用来控制和修改动画和过渡
特性提供已实现动画和过渡的组件 - 例如, Swipeable 和 DrawerLayout提供实用函数来处理数学运算,如toRad
UI状态处理没有用React Native Reanimated的 useSharedValue 实现UI状态处理器实现用户界面状态处理器 — 例如, useTiming 函数

React Native Reanimated vs. React Native Gesture Handler:差异

Reanimated库专门设计用来处理动画。相比之下,React Native Gesture Handler库旨在替代React Native中的原生手势响应系统,为处理手势的组件提供更多的控制和更好的性能。

尽管 Reanimated 库是为处理动画而构建的,但它也附带了一些其他的辅助方法,使动画渲染更加流畅。下表提供了关于这两个库之间差异的一些见解:

React Native ReanimatedReact Native Gesture Handler
核心功能提供了来自 React Native 的 Animated 库的低级实现提供了React Native的手势处理系统的低级实现
主要使用场景可以用来处理动画可以用来处理对象上的手势和动作
提供辅助函数以增强动画
触发机制它的事件可以自动触发;并不依赖于用户操作它依赖于用户的手势操作
触发机制它的事件可以自动触发;并不依赖于用户操作它依赖于用户的手势操作

React Native Reanimated 如何与 Animated API交互

在React Native Reanimated 库的第一个版本中,其API是为了模仿React Native 的 Animated 库而构建的。而最新版本则使用worklets 来处理实际运行在UI线程上的动画,从而确保高性能的动画。

AnimatedReact Native Reanimated
Animated.ParallelParallel
Animated.Value()useSharedValue()
Animated.decay()withSpring()
Animated.timing()withTiming()
Animated.sequence()withSequence()

React Native Reanimated 也兼容 Animated API。这意味着Reanimated 库和 Animated 库可以在同一个项目中使用,而不会导致任何部分的代码崩溃或无法工作。

Reanimated库还为React Native Gesture Handler提供了非常好的支持,以处理如抛掷、长按等手势。然而,Animated库可以与React Native提供的Gesture Handler 系统非常好地配合工作。

React Native手势处理器如何与手势响应系统交互

React Native 自带的原生手势处理器使用手势响应系统来处理交互和手势,而 React Native 手势处理器包则不使用这个系统。相反,它使用 iOS 的 UIGestureRecognizer 和为 Android 定制建造的协调器。

尽管这两个库有不同的实现机制,但它们可以在一个项目中一起使用。让我们看看如何操作。

响应者生命周期提供了两种方法,我们可以将这些方法传递给组件,以在没有原生支持的情况下手动实现手势。这两个方法是 onStartShouldSetResponderonMoveShouldSetResponder

这些方法中的每一个都是我们期望返回布尔值的函数。当它们返回 true 值时,意味着该对象已经成为了响应者。我们也可以使用 onResponderGrantonResponderReject 方法来检查对象是否已经成为响应者。

一旦对象成为响应者,我们可以使用适当的方法访问响应者的事件,例如 onResponderMoveonResponderRelease 。有了这个事件,我们可以设置我们自己的手势处理器。

这与React Native Gesture Handler库并不相同,因为它提供了 GestureHandlerRootViewGestureDetector ,使我们能够处理手势。

我们可以通过以下代码演示如何使用原生手势响应系统设置手势处理,使 View 响应按压和移动手势:

import { StyleSheet, Text, View } from 'react-native';
import React from 'react';

const GRSSample = () => {
  return (
    <View>
      <View
        style={styles.square}
        onStartShouldSetResponder={(event) => true}
        onMoveShouldSetResponde={(event) => true}
        onResponderGrant={(event) => {
          console.log({ responderGrantEvent: event });
        }}
        onResponderReject={(event) => {
          console.log({ responderRejectEvent: event });
        }}
        onResponderMove={(event) => {
          console.log({ responderMoveEvent: event });
        }}
        onResponderRelease={(event) => {
          console.log({ responderReleaseEvent: event });
        }}
        onResponderTerminationRequest={(event) => {
          console.log({ responderReleaseEvent: event });
        }}
        onResponderTerminate={(event) => {
          console.log({ responderTerminateEvent: event });
        }}
      />
    </View>
  );
};
export default GRSSample;

const styles = StyleSheet.create({
  square: {
    width: 200,
    height: 200,
    backgroundColor: 'blue',
    alignSelf: 'center',
  },
});

请注意,上述样本中的每种方法都会产生一个包含以下属性的原生事件: changedTouchesidentifierlocationXlocationYpageXpageYtargettimestamp ,以及 touches

应该使用哪个库进行动画制作

实现动画的库是React Native Reanimated库。同时,React Native Gesture Handler提供了手势处理能力,而React Native Redash则提供了处理动画的辅助函数。

因此,用于动画的库是React Native Reanimated。然而,如果你想让你的动画更加流畅和互动,你可以使用React Native Redash和React Native Gesture Handler来整合手势处理和辅助函数。

总结

在这篇文章中,我们比较了React Native Redash和React Native Gesture Handler的功能。我们还探讨了React Native Reanimated和React Native Gesture Handler之间的差异。

为了更好地理解每个库的目的和用途,我们讨论了Reanimated库如何与Animated API交互,以及React Native Gesture Handler如何与React Native中的原生手势处理器交互。最后,我们讨论了应该使用哪个库进行动画。

你可以在GitHub上查看我们在这个教程中实现的示例的完整源代码。

首发于公众号 大迁世界,欢迎关注。📝 每周一篇实用的前端文章 🛠️ 分享值得关注的开发工具 ❓ 有疑问?我来回答

本文 GitHub https://github.com/qq449245884/xiaozhi 已收录,有一线大厂面试完整考点、资料以及我的系列文章。


王大冶
68.1k 声望105k 粉丝