截图

在本教程中,了解如何使用第三方库和 Expo Media Library 捕获屏幕截图。


在本章中,我们将学习如何使用第三方库截取屏幕截图并将其保存在设备的媒体库中。我们将使用 react-native-view-shot 截取屏幕截图,并使用 expo-media-library 将图片保存在设备的媒体库中。

¥In this chapter, we'll learn how to take a screenshot using a third-party library and save it on the device's media library. We'll use react-native-view-shot to take a screenshot and expo-media-library to save an image on device's media library.

到目前为止,我们已经使用了第三方库,例如 react-native-gesture-handlerreact-native-reanimated。我们可以根据用例在 React Native 目录 上找到数百个其他第三方库。
Watch: Taking screenshots in your universal Expo app
Watch: Taking screenshots in your universal Expo app

1

安装库

¥Install libraries

要安装 react-native-view-shotexpo-media-library,请运行以下命令:

¥To install react-native-view-shot and expo-media-library, run the following commands:

Terminal
npx expo install react-native-view-shot expo-media-library

2

提示权限

¥Prompt for permissions

需要敏感信息(例如访问设备的媒体库)的应用必须提示允许或拒绝访问的权限。使用来自 expo-media-libraryusePermissions() 钩子,我们可以使用权限 statusrequestPermission() 方法来请求访问权限。

¥An app that requires sensitive information, such as accessing a device's media library, has to prompt permission to allow or deny access. Using usePermissions() hook from expo-media-library, we can use the permission status and requestPermission() method to ask for access.

当应用首次加载且权限状态既未授予也未拒绝时,status 的值为 null。当请求权限时,用户可以授予权限或拒绝权限。我们可以添加一个条件来检查是否是 null,如果是,则触发 requestPermission() 方法。获得访问权限后,status 的值将更改为 granted

¥When the app loads for the first time and the permission status is neither granted nor denied, the value of the status is null. When asked for permission, a user can either grant the permission or deny it. We can add a condition to check if it is null, and if it is, trigger the requestPermission() method. After getting the access, the value of the status changes to granted.

在 app/(tabs)/index.tsx 中添加以下代码片段:

¥Add the following code snippet inside the app/(tabs)/index.tsx:

app/(tabs)/index.tsx
import * as MediaLibrary from 'expo-media-library';

// ...rest of the code remains same

export default function Index() {
const [status, requestPermission] = MediaLibrary.usePermissions();
// ...rest of the code remains same

if (status === null) {
  requestPermission();
}

// ...rest of the code remains same
}

3

创建一个 ref 来保存当前视图

¥Create a ref to save the current view

我们将使用 react-native-view-shot 允许用户在应用内截取屏幕截图。此库使用 captureRef() 方法将 <View> 的屏幕截图捕获为图片。它返回捕获的屏幕截图图片文件的 URI。

¥We'll use react-native-view-shot to allow the user to take a screenshot within the app. This library captures the screenshot of a <View> as an image using the captureRef() method. It returns the URI of the captured screenshot image file.

  1. react-native-view-shot 导入 captureRef 并从 React 导入 useRef

    ¥Import captureRef from react-native-view-shot and useRef from React.

  2. 创建一个 imageRef 引用变量来存储捕获的屏幕截图图片的引用。

    ¥Create an imageRef reference variable to store the reference of the screenshot image captured.

  3. <ImageViewer><EmojiSticker> 组件封装在 <View> 中,然后将引用变量传递给它。

    ¥Wrap the <ImageViewer> and <EmojiSticker> components inside a <View> and then pass the reference variable to it.

app/(tabs)/index.tsx
import { useState, useRef } from 'react';
import { captureRef } from 'react-native-view-shot';

export default function Index() {
const imageRef = useRef<View>(null);

// ...rest of the code remains same

return (
  <GestureHandlerRootView style={styles.container}>
    <View style={styles.imageContainer}>
      <View ref={imageRef} collapsable={false}>
        <ImageViewer imgSource={PlaceholderImage} selectedImage={selectedImage} />
        {pickedEmoji && <EmojiSticker imageSize={40} stickerSource={pickedEmoji} />}
      </View>
    </View>
    {/* ...rest of the code remains same */}
  </GestureHandlerRootView>
);
}

在上面的代码片段中,collapsable prop 设置为 false。这允许 <View> 组件仅截取背景图片和表情符号贴纸的屏幕截图。

¥In the above snippet, the collapsable prop is set to false. This allows the <View> component to screenshot only of the background image and emoji sticker.

4

捕获屏幕截图并保存

¥Capture a screenshot and save it

我们可以通过在 onSaveImageAsync() 函数内从 react-native-view-shot 调用 captureRef() 方法来捕获视图的屏幕截图。它接受一个可选参数,我们可以在其中传递屏幕截图捕获区域的 widthheight。我们可以在 库的文档 中阅读有关可用选项的更多信息。

¥We can capture a screenshot of the view by calling the captureRef() method from react-native-view-shot inside the onSaveImageAsync() function. It accepts an optional argument where we can pass the width and height of the screenshot capturing area. We can read more about available options in the library's documentation.

captureRef() 方法还返回一个使用屏幕截图 URI 实现的 promise。我们将此 URI 作为参数传递给 MediaLibrary.saveToLibraryAsync(),并将屏幕截图保存到设备的媒体库。

¥The captureRef() method also returns a promise that fulfills with the screenshot's URI. We will pass this URI as a parameter to MediaLibrary.saveToLibraryAsync() and save the screenshot to the device's media library.

在 app/(tabs)/index.tsx 中,使用以下代码更新 onSaveImageAsync() 函数:

¥Inside app/(tabs)/index.tsx, update the onSaveImageAsync() function with the following code:

app/(tabs)/index.tsx
import { View, StyleSheet } from 'react-native';
import * as ImagePicker from 'expo-image-picker';
import { useState, useRef } from 'react';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import * as MediaLibrary from 'expo-media-library';
import { captureRef } from 'react-native-view-shot';
import { type ImageSource } from 'expo-image';

import Button from '@/components/Button';
import ImageViewer from '@/components/ImageViewer';
import IconButton from '@/components/IconButton';
import CircleButton from '@/components/CircleButton';
import EmojiPicker from '@/components/EmojiPicker';
import EmojiList from '@/components/EmojiList';
import EmojiSticker from '@/components/EmojiSticker';

const PlaceholderImage = require('@/assets/images/background-image.png');

export default function Index() {
const [selectedImage, setSelectedImage] = useState<string | undefined>(undefined);
const [showAppOptions, setShowAppOptions] = useState<boolean>(false);
const [isModalVisible, setIsModalVisible] = useState<boolean>(false);
const [pickedEmoji, setPickedEmoji] = useState<ImageSource | undefined>(undefined);
const [status, requestPermission] = MediaLibrary.usePermissions();
const imageRef = useRef<View>(null);

if (status === null) {
  requestPermission();
}

const pickImageAsync = async () => {
  let result = await ImagePicker.launchImageLibraryAsync({
    mediaTypes: ['images'],
    allowsEditing: true,
    quality: 1,
  });

  if (!result.canceled) {
    setSelectedImage(result.assets[0].uri);
    setShowAppOptions(true);
  } else {
    alert('You did not select any image.');
  }
};

const onReset = () => {
  setShowAppOptions(false);
};

const onAddSticker = () => {
  setIsModalVisible(true);
};

const onModalClose = () => {
  setIsModalVisible(false);
};

const onSaveImageAsync = async () => {
  try {
    const localUri = await captureRef(imageRef, {
      height: 440,
      quality: 1,
    });

    await MediaLibrary.saveToLibraryAsync(localUri);
    if (localUri) {
      alert('Saved!');
    }
  } catch (e) {
    console.log(e);
  }
};

return (
  <GestureHandlerRootView style={styles.container}>
    <View style={styles.imageContainer}>
      <View ref={imageRef} collapsable={false}>
        <ImageViewer imgSource={PlaceholderImage} selectedImage={selectedImage} />
        {pickedEmoji && <EmojiSticker imageSize={40} stickerSource={pickedEmoji} />}
      </View>
    </View>
    {showAppOptions ? (
      <View style={styles.optionsContainer}>
        <View style={styles.optionsRow}>
          <IconButton icon="refresh" label="Reset" onPress={onReset} />
          <CircleButton onPress={onAddSticker} />
          <IconButton icon="save-alt" label="Save" onPress={onSaveImageAsync} />
        </View>
      </View>
    ) : (
      <View style={styles.footerContainer}>
        <Button theme="primary" label="Choose a photo" onPress={pickImageAsync} />
        <Button label="Use this photo" onPress={() => setShowAppOptions(true)} />
      </View>
    )}
    <EmojiPicker isVisible={isModalVisible} onClose={onModalClose}>
      <EmojiList onSelect={setPickedEmoji} onCloseModal={onModalClose} />
    </EmojiPicker>
  </GestureHandlerRootView>
);
}

const styles = StyleSheet.create({
container: {
  flex: 1,
  backgroundColor: '#25292e',
  alignItems: 'center',
},
imageContainer: {
  flex: 1,
},
footerContainer: {
  flex: 1 / 3,
  alignItems: 'center',
},
optionsContainer: {
  position: 'absolute',
  bottom: 80,
},
optionsRow: {
  alignItems: 'center',
  flexDirection: 'row',
},
});

现在,选择一张照片并在应用中添加贴纸。然后点击 "保存" 按钮。我们应该在 Android 和 iOS 上看到以下结果:

¥Now, choose a photo and add a sticker in the app. Then tap the "Save" button. We should see the following result on Android and iOS:

概括

¥Summary

Chapter 7: Take a screenshot

We've successfully used react-native-view-shot and expo-media-library to capture a screenshot and save it on the device's library.

In the next chapter, let's learn how to handle the differences between mobile and web platforms to implement the same functionality on web.

Next: Handle platform differences