This documentation is available as Markdown for AI agents and LLMs. See the full Markdown index or append .md to any documentation URL.
创建一个模态框
编辑页面
在本教程中,学习如何创建一个用于选择图片的 React Native 模态框。
React Native provides a <Modal> component that presents content above the rest of your app. In general, modals are used to draw a user's attention toward critical information or guide them to take action. For example, in the third chapter, after pressing the button, we used alert() to display some placeholder text. That's how a modal component displays an overlay.
在本章中,我们将创建一个显示表情选择器列表的模态框。

使用 React Native 的 Modal API 构建一个模态组件,用于显示表情选择器并处理用户交互。
1
声明一个用于显示按钮的状态变量
在实现模态框之前,我们先添加三个新按钮。这些按钮会在用户从媒体库中选择图片或使用占位图片后显示。其中一个按钮将触发表情选择器模态框。
在 app/(tabs)/index.tsx 中:
- 声明一个布尔类型的状态变量
showAppOptions,用于显示或隐藏打开模态框以及其他几个选项的按钮。当应用屏幕加载时,我们将其设置为false,这样在选择图片之前不会显示这些选项。当用户选择图片或使用占位图片时,我们会将其设置为true。 - 更新
pickImageAsync()函数,在用户选择图片后将showAppOptions的值设为true。 - 为没有主题的按钮添加
onPress属性,并使用以下值。
import { View, StyleSheet } from 'react-native'; import * as ImagePicker from 'expo-image-picker'; import { useState } from 'react'; import Button from '@/components/Button'; import ImageViewer from '@/components/ImageViewer'; 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 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.'); } }; return ( <View style={styles.container}> <View style={styles.imageContainer}> <ImageViewer imgSource={PlaceholderImage} selectedImage={selectedImage} /> </View> {showAppOptions ? ( <View /> ) : ( <View style={styles.footerContainer}> <Button theme="primary" label="Choose a photo" onPress={pickImageAsync} /> <Button label="Use this photo" onPress={() => setShowAppOptions(true)} /> </View> )} </View> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#25292e', alignItems: 'center', }, imageContainer: { flex: 1, }, footerContainer: { flex: 1 / 3, alignItems: 'center', }, });
在上面的代码片段中,我们根据 showAppOptions 的值来渲染 Button 组件,并将按钮移动到三元运算块中。当 showAppOptions 的值为 true 时,渲染一个空的 <View> 组件。我们将在下一步处理这个状态。
现在,我们可以移除 Button 组件上的 alert,并在 components/Button.tsx 中渲染第二个按钮时更新 onPress 属性:
<Pressable style={styles.button} onPress={onPress}>
2
添加按钮
让我们拆解本章要实现的选项按钮布局。设计如下:
它包含一个父级 <View>,其中有三个按钮横向排列。中间带有加号图标(+)的按钮会打开模态框,并且样式与另外两个按钮不同。
在 components 目录中,创建一个新的 CircleButton.tsx 文件,并添加以下代码:
import { View, Pressable, StyleSheet } from 'react-native'; import MaterialIcons from '@expo/vector-icons/MaterialIcons'; type Props = { onPress: () => void; }; export default function CircleButton({ onPress }: Props) { return ( <View style={styles.circleButtonContainer}> <Pressable style={styles.circleButton} onPress={onPress}> <MaterialIcons name="add" size={38} color="#25292e" /> </Pressable> </View> ); } const styles = StyleSheet.create({ circleButtonContainer: { width: 84, height: 84, marginHorizontal: 60, borderWidth: 4, borderColor: '#ffd33d', borderRadius: 42, padding: 3, }, circleButton: { flex: 1, justifyContent: 'center', alignItems: 'center', borderRadius: 42, backgroundColor: '#fff', }, });
为了渲染加号图标,这个按钮使用了 @expo/vector-icons 库中的 <MaterialIcons> 图标集。
另外两个按钮也使用 <MaterialIcons> 来显示垂直对齐的文本标签和图标。在 components 目录中创建一个名为 IconButton.tsx 的文件。这个组件接受三个 props:
icon:对应MaterialIcons图标库图标名称。label:按钮上显示的文本标签。onPress:用户按下按钮时调用的函数。
import { Pressable, StyleSheet, Text } from 'react-native'; import MaterialIcons from '@expo/vector-icons/MaterialIcons'; type Props = { icon: keyof typeof MaterialIcons.glyphMap; label: string; onPress: () => void; }; export default function IconButton({ icon, label, onPress }: Props) { return ( <Pressable style={styles.iconButton} onPress={onPress}> <MaterialIcons name={icon} size={24} color="#fff" /> <Text style={styles.iconButtonLabel}>{label}</Text> </Pressable> ); } const styles = StyleSheet.create({ iconButton: { justifyContent: 'center', alignItems: 'center', }, iconButtonLabel: { color: '#fff', marginTop: 12, }, });
在 app/(tabs)/index.tsx 中:
- 导入
CircleButton和IconButton组件以便显示它们。 - 为这些按钮添加三个占位函数。
onReset()函数会在用户按下重置按钮时调用,使图片选择按钮再次显示。其他两个函数的功能我们稍后再添加。
import { View, StyleSheet } from 'react-native'; import * as ImagePicker from 'expo-image-picker'; import { useState } from 'react'; import Button from '@/components/Button'; import ImageViewer from '@/components/ImageViewer'; import IconButton from '@/components/IconButton'; import CircleButton from '@/components/CircleButton'; 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 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 = () => { // 我们稍后会实现这个 }; const onSaveImageAsync = async () => { // 我们稍后会实现这个 }; return ( <View style={styles.container}> <View style={styles.imageContainer}> <ImageViewer imgSource={PlaceholderImage} selectedImage={selectedImage} /> </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> )} </View> ); } 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 和 Web 上的效果:
3
创建一个表情选择器模态框
这个模态框允许用户从可用表情列表中选择一个表情。请在 components 目录中创建一个 EmojiPicker.tsx 文件。这个组件接受三个 props:
isVisible:用于确定模态框可见状态的布尔值。onClose:用于关闭模态框的函数。children:稍后用于显示表情列表。
import { Modal, View, Text, Pressable, StyleSheet } from 'react-native'; import { PropsWithChildren } from 'react'; import MaterialIcons from '@expo/vector-icons/MaterialIcons'; type Props = PropsWithChildren<{ isVisible: boolean; onClose: () => void; }>; export default function EmojiPicker({ isVisible, children, onClose }: Props) { return ( <View> <Modal animationType="slide" transparent={true} visible={isVisible}> <View style={styles.modalContent}> <View style={styles.titleContainer}> <Text style={styles.title}>选择一个贴纸</Text> <Pressable onPress={onClose}> <MaterialIcons name="close" color="#fff" size={22} /> </Pressable> </View> {children} </View> </Modal> </View> ); } const styles = StyleSheet.create({ modalContent: { height: '25%', width: '100%', backgroundColor: '#25292e', borderTopRightRadius: 18, borderTopLeftRadius: 18, position: 'absolute', bottom: 0, }, titleContainer: { height: '16%', backgroundColor: '#464C55', borderTopRightRadius: 10, borderTopLeftRadius: 10, paddingHorizontal: 20, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', }, title: { color: '#fff', fontSize: 16, }, });
让我们了解以上代码的作用:
<Modal>组件会显示一个标题和一个关闭按钮。- 它的
visible属性取值为isVisible,用于控制模态框是打开还是关闭。 - 它的
transparent属性是一个布尔值,用于决定模态框是否填满整个视图。 - 它的
animationType属性决定模态框进入和离开屏幕的方式。在这里,它从屏幕底部滑入。 - 最后,当用户按下关闭的
<Pressable>时,<EmojiPicker>会调用onClose属性。
现在,让我们修改 app/(tabs)/index.tsx:
- 导入
<EmojiPicker>组件。 - 使用
useState钩子创建一个isModalVisible状态变量。它的默认值为false,这会在用户按下打开按钮之前隐藏模态框。 - 替换
onAddSticker()函数中的注释,当用户按下按钮时将isModalVisible变量更新为true。这将打开表情选择器。 - 创建
onModalClose()函数来更新isModalVisible状态变量。 - 将
<EmojiPicker>组件放在Index组件底部。
import { View, StyleSheet } from 'react-native'; import * as ImagePicker from 'expo-image-picker'; import { useState } from 'react'; 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'; 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 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 () => { // 我们稍后会实现这个 }; return ( <View style={styles.container}> <View style={styles.imageContainer}> <ImageViewer imgSource={PlaceholderImage} selectedImage={selectedImage} /> </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}> {/* 表情列表组件将放在这里 */} </EmojiPicker> </View> ); } 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', }, });
以下是这一步之后的结果:
4
显示表情列表
现在,让我们在模态框内容中添加一个横向的表情列表。为此我们将使用 React Native 的 <FlatList> 组件。
在 components 目录中创建一个 EmojiList.tsx 文件,并添加以下代码:
import { useState } from 'react'; import { ImageSourcePropType, StyleSheet, FlatList, Platform, Pressable } from 'react-native'; import { Image } from 'expo-image'; type Props = { onSelect: (image: ImageSourcePropType) => void; onCloseModal: () => void; }; export default function EmojiList({ onSelect, onCloseModal }: Props) { const [emoji] = useState<ImageSourcePropType[]>([ require("../assets/images/emoji1.png"), require("../assets/images/emoji2.png"), require("../assets/images/emoji3.png"), require("../assets/images/emoji4.png"), require("../assets/images/emoji5.png"), require("../assets/images/emoji6.png"), ]); return ( <FlatList horizontal showsHorizontalScrollIndicator={Platform.OS === 'web'} data={emoji} contentContainerStyle={styles.listContainer} renderItem={({ item, index }) => ( <Pressable onPress={() => { onSelect(item); onCloseModal(); }}> <Image source={item} key={index} style={styles.image} /> </Pressable> )} /> ); } const styles = StyleSheet.create({ listContainer: { borderTopRightRadius: 10, borderTopLeftRadius: 10, paddingHorizontal: 20, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', }, image: { width: 100, height: 100, marginRight: 20, }, });
让我们了解以上代码的作用:
- 上面的
<FlatList>组件使用Image组件渲染所有表情图片,并由<Pressable>包裹。稍后我们会改进它,使用户可以在屏幕上点击某个表情,将其作为贴纸显示在图片上。 - 它还接受由
emoji数组变量提供的项目数组,作为data属性的值。renderItem属性取出data中的项目并返回列表中的该项。最后,我们添加了Image和<Pressable>组件来显示该项目。 horizontal属性使列表水平而不是垂直渲染。showsHorizontalScrollIndicator使用 React Native 的Platform模块来检查该值,并在 Web 上显示横向滚动条。
现在,更新 app/(tabs)/index.tsx,导入 <EmojiList> 组件,并将 <EmojiPicker> 组件内部的注释替换为以下代码片段:
import { ImageSourcePropType, View, StyleSheet } from 'react-native'; import * as ImagePicker from 'expo-image-picker'; import { useState } from 'react'; 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'; 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<ImageSourcePropType | undefined>(undefined); 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 () => { // 我们稍后会实现这个 }; return ( <View style={styles.container}> <View style={styles.imageContainer}> <ImageViewer imgSource={PlaceholderImage} selectedImage={selectedImage} /> </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> </View> ); } 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', }, });
在 EmojiList 组件中,onSelect 属性用于选择表情,选择后,onCloseModal 会关闭模态框。
让我们看看应用在 Android、iOS 和 Web 上的效果:
5
显示选中的表情
现在,我们要把表情贴纸放到图片上。请在 components 目录中新建一个文件,命名为 EmojiSticker.tsx。然后添加以下代码:
import { ImageSourcePropType, View } from 'react-native'; import { Image } from 'expo-image'; type Props = { imageSize: number; stickerSource: ImageSourcePropType; }; export default function EmojiSticker({ imageSize, stickerSource }: Props) { return ( <View style={{ top: -350 }}> <Image source={stickerSource} style={{ width: imageSize, height: imageSize }} /> </View> ); }
这个组件接收两个 props:
imageSize:在Index组件内部定义的值。我们将在下一章中使用这个值来在点击时缩放图片大小。stickerSource:所选表情图片的来源。
在 app/(tabs)/index.tsx 文件中导入该组件,并更新 Index 组件以在图片上显示表情贴纸。我们会检查 pickedEmoji 状态是否不为 undefined:
import { ImageSourcePropType, View, StyleSheet } from 'react-native'; import * as ImagePicker from 'expo-image-picker'; import { useState } from 'react'; 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<ImageSourcePropType | undefined>(undefined); 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 () => { // 我们稍后会实现这个 }; return ( <View style={styles.container}> <View style={styles.imageContainer}> <ImageViewer imgSource={PlaceholderImage} selectedImage={selectedImage} /> {pickedEmoji && <EmojiSticker imageSize={40} stickerSource={pickedEmoji} />} </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> </View> ); } 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 和 Web 上的效果:
概要
Chapter 5: Create a modal
We've successfully created the emoji picker modal and implemented the logic to select an emoji and display it over the image.
In the next chapter, let's add user interactions with gestures to drag the emoji and scale the size by tapping it.