在本教程中,了解在创建通用应用时如何处理原生和 Web 之间的平台差异。
Android、iOS 和 Web 具有不同的功能。在我们的例子中,Android 和 iOS 都可以使用 react-native-view-shot
库捕获屏幕截图。但是,Web 浏览器不能。
¥Android, iOS, and the web have different capabilities. In our case, both Android and iOS can capture a screenshot with the react-native-view-shot
library. However, web browsers cannot.
在本章中,我们将学习如何处理 Web 浏览器的屏幕截图,以便我们的应用在所有平台上具有相同的功能。
¥In this chapter, we'll learn how to handle capturing screenshots for web browsers so our app has the same functionality on all platforms.
1
¥Install and import dom-to-image
要在网络上捕获屏幕截图并将其保存为图片,我们将使用名为 dom-to-image
的第三方库。它截取任何 DOM 节点的屏幕截图并将其转换为矢量(SVG)或光栅(PNG 或 JPEG)图片。
¥To capture a screenshot on the web and save it as an image, we'll use a third-party library called dom-to-image
. It takes a screenshot of any DOM node and turns it into a vector (SVG) or raster (PNG or JPEG) image.
停止开发服务器并运行以下命令来安装库:
¥Stop the development server and run the following command to install the library:
-
npm install dom-to-image
安装完成后,请确保重新启动开发服务器并在终端中按 w。
¥After installing it, make sure to restart the development server and press w in the terminal.
2
¥Add platform-specific code
使用来自 React Native 的 Platform
模块,我们可以实现特定于平台的行为。在 app/(tabs)/index.tsx 中:
¥Using Platform
module from React Native, we can implement platform-specific behavior. Inside app/(tabs)/index.tsx:
从 react-native
导入 Platform
模块。
¥Import the Platform
module from react-native
.
从 dom-to-image
导入 domtoimage
库。
¥Import the domtoimage
library from dom-to-image
.
更新 onSaveImageAsync()
函数,使用 Platform.OS
属性检查当前平台是否为 'web'
。如果是 'web'
,我们将使用 domtoimage.toJpeg()
方法将当前的 <View>
转换并捕获为 JPEG 图片。否则,我们将继续使用为原生平台添加的相同逻辑。
¥Update the onSaveImageAsync()
function to check whether the current platform is 'web'
with the Platform.OS
property. If it is 'web'
, we'll use the domtoimage.toJpeg()
method to convert and capture the current <View>
as a JPEG image. Otherwise, we'll keep using the same logic added for native platforms.
import { View, StyleSheet, Platform } 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 { type ImageSource } from 'expo-image';
import { captureRef } from 'react-native-view-shot';
import domtoimage from 'dom-to-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 () => {
if (Platform.OS !== 'web') {
try {
const localUri = await captureRef(imageRef, {
height: 440,
quality: 1,
});
await MediaLibrary.saveToLibraryAsync(localUri);
if (localUri) {
alert('Saved!');
}
} catch (e) {
console.log(e);
}
} else {
try {
const dataUrl = await domtoimage.toJpeg(imageRef.current, {
quality: 0.95,
width: 320,
height: 440,
});
let link = document.createElement('a');
link.download = 'sticker-smash.jpeg';
link.href = dataUrl;
link.click();
} 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',
},
});
dom-to-image
TypeScript module error由于我们使用的是 TypeScript,因此我们需要在导入 domtoimage
库后添加类型定义。我们可以通过在项目目录的根目录中创建一个 types.d.ts 文件并添加声明语句来实现这一点:
¥We need to add a type definition after importing the domtoimage
library since we're using TypeScript. We can do this by creating a file types.d.ts in the root of our project directory and adding the declaration statement:
declare module 'dom-to-image';
在网络浏览器中运行应用时,我们现在可以保存屏幕截图:
¥On running the app in a web browser, we can now save a screenshot:
¥Summary
Chapter 8: Handle platform differences
The app does everything we set out for it to do, so it's time to shift our focus toward the purely aesthetic..
In the next chapter, we will customize the app's status bar, splash screen, and app icon.