创建模态

在本教程中,了解如何创建 React Native 模式来选择图片。


React Native 提供了 <Modal> 组件,将内容渲染在应用的其余部分之上。一般来说,模态框用于吸引用户对关键信息的注意或指导他们采取行动。例如,在 第三章 中,按下按钮后,我们使用 alert() 显示一些占位符文本。这就是模态组件显示叠加层的方式。

¥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.

在本章中,我们将创建一个显示表情符号选择器列表的模式。

¥In this chapter, we'll create a modal that shows an emoji picker list.

Watch: Creating a modal in your universal Expo app
Watch: Creating a modal in your universal Expo app

1

声明一个状态变量来显示按钮

¥Declare a state variable to show buttons

在实现模式之前,我们将添加三个新按钮。用户从媒体库中选择图片或使用占位符图片后,这些按钮可见。这些按钮之一将触发表情符号选择器模式。

¥Before implementing the modal, we are going to add three new buttons. These buttons are visible after the user picks an image from the media library or uses the placeholder image. One of these buttons will trigger the emoji picker modal.

在 app/(tabs)/index.tsx 中:

¥In app/(tabs)/index.tsx:

  1. 声明一个布尔状态变量 showAppOptions,以显示或隐藏打开模式的按钮,以及其他一些选项。当应用屏幕加载时,我们将其设置为 false,以便在选择图片之前不会显示选项。当用户选择图片或使用占位符图片时,我们将其设置为 true

    ¥Declare a boolean state variable, showAppOptions, to show or hide the buttons that open the modal, alongside a few other options. When the app screen loads, we'll set it to false so the options are not shown before picking an image. When the user picks an image or uses the placeholder image, we'll set it to true.

  2. 更新 pickImageAsync() 函数,在用户选择图片后将 showAppOptions 的值设置为 true

    ¥Update the pickImageAsync() function to set the value of showAppOptions to true after the user picks an image.

  3. 通过添加具有以下值的 onPress prop 更新没有主题的按钮。

    ¥Update the button with no theme by adding an onPress prop with the following value.

app/(tabs)/index.tsx
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> 组件。我们将在下一步中解决此状态。

¥In the above snippet, we're rendering the Button component based on the value of showAppOptions and moving the buttons in the ternary operator block. When the value of showAppOptions is true, render an empty <View> component. We'll address this state in the next step.

现在,我们可以在 Button 组件上删除 alert,并在渲染 components/Button.tsx 中的第二个按钮时更新 onPress prop:

¥Now, we can remove the alert on the Button component and update the onPress prop when rendering the second button in the components/Button.tsx:

components/Button.tsx
<Pressable style={styles.button} onPress={onPress} >

2

添加按钮

¥Add buttons

让我们分解一下本章中将实现的选项按钮的布局。设计如下:

¥Let's break down the layout of the option buttons we'll implement in this chapter. The design looks like this:

它包含一个父级 <View>,其中三个按钮排成一行。中间带有加号图标 (+) 的按钮将打开模式,其样式与其他两个按钮不同。

¥It contains a parent <View> with three buttons aligned in a row. The button in the middle with the plus icon (+) will open the modal and is styled differently than the other two buttons.

在 components 目录中,使用以下代码创建一个新的 CircleButton.tsx 文件:

¥Inside the components directory, create a new CircleButton.tsx file with the following code:

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> 图标集。

¥To render the plus icon, this button uses the <MaterialIcons> icon set from the @expo/vector-icons library.

另外两个按钮也使用 <MaterialIcons> 显示垂直对齐的文本标签和图标。在 components 目录中创建一个名为 IconButton.tsx 的文件。该组件接受三个 props:

¥The other two buttons also use <MaterialIcons> to display vertically aligned text labels and icons. Create a file named IconButton.tsx inside the components directory. This component accepts three props:

  • icon:与 MaterialIcons 库图标对应的名称。

    ¥icon: the name corresponding to the MaterialIcons library icon.

  • label:按钮上显示的文本标签。

    ¥label: the text label displayed on the button.

  • onPress:当用户按下按钮时,此函数将调用。

    ¥onPress: this function invokes when the user presses the button.

components/IconButton.tsx
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 中:

¥Inside app/(tabs)/index.tsx:

  1. 导入 CircleButtonIconButton 组件以显示它们。

    ¥Import the CircleButton and IconButton components to display them.

  2. 为这些按钮添加三个占位符函数。当用户按下重置按钮时,onReset() 函数会调用,导致图片选择器按钮再次出现。我们稍后将添加其他两个功能的功能。

    ¥Add three placeholder functions for these buttons. The onReset() function invokes when the user presses the reset button, causing the image picker button to appear again. We'll add the functionality for the other two functions later.

app/(tabs)/index.tsx
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 = () => {
  // we will implement this later
};

const onSaveImageAsync = async () => {
  // we will implement this later
};

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 上的应用:

¥Let's take a look at our app on Android, iOS and the web:

3

创建表情符号选择器模式

¥Create an emoji picker modal

该模式允许用户从可用表情符号列表中选择表情符号。在组件目录中创建一个 EmojiPicker.tsx 文件。该组件接受三个 props:

¥The modal allows the user to choose an emoji from a list of available emoji. Create an EmojiPicker.tsx file inside the components directory. This component accepts three props:

  • isVisible:一个布尔值来确定模式的可见性状态。

    ¥isVisible: a boolean to determine the state of the modal's visibility.

  • onClose:一个用于关闭模式的函数。

    ¥onClose: a function to close the modal.

  • children:稍后用于显示表情符号列表。

    ¥children: used later to display a list of emoji.

components/EmojiPicker.tsx
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 (
  <Modal animationType="slide" transparent={true} visible={isVisible}>
    <View style={styles.modalContent}>
      <View style={styles.titleContainer}>
        <Text style={styles.title}>Choose a sticker</Text>
        <Pressable onPress={onClose}>
          <MaterialIcons name="close" color="#fff" size={22} />
        </Pressable>
      </View>
      {children}
    </View>
  </Modal>
);
}

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,
},
});

我们来了解一下上面的代码做了什么:

¥Let's learn what the above code does:

  • <Modal> 组件显示标题和关闭按钮。

    ¥The <Modal> component displays a title and a close button.

  • 它的 visible prop 采用 isVisible 的值并控制模式是打开还是关闭。

    ¥Its visible prop takes the value of isVisible and controls whether the modal is open or closed.

  • 它的 transparent prop 是一个布尔值,它决定模式是否填充整个视图。

    ¥Its transparent prop is a boolean value, which determines whether the modal fills the entire view.

  • 它的 animationType 属性决定了它如何进入和离开屏幕。在本例中,它是从屏幕底部滑动的。

    ¥Its animationType prop determines how it enters and leaves the screen. In this case, it is sliding from the bottom of the screen.

  • 最后,当用户按下关闭 <Pressable> 时,<EmojiPicker> 会调用 onClose prop。

    ¥Lastly, the <EmojiPicker> invokes the onClose prop when the user presses the close <Pressable>.

现在,让我们修改 app/(tabs)/index.tsx:

¥Now, let's modify the app/(tabs)/index.tsx:

  1. 导入 <EmojiPicker> 组件。

    ¥Import the <EmojiPicker> component.

  2. 使用 useState 钩子创建一个 isModalVisible 状态变量。它的默认值是 false,它会隐藏模式,直到用户按下按钮将其打开。

    ¥Create an isModalVisible state variable with the useState hook. Its default value is false, which hides the modal until the user presses the button to open it.

  3. 替换 onAddSticker() 函数中的注释,以便在用户按下按钮时将 isModalVisible 变量更新为 true。这将打开表情符号选择器。

    ¥Replace the comment in the onAddSticker() function to update the isModalVisible variable to true when the user presses the button. This will open the emoji picker.

  4. 创建 onModalClose() 函数以更新 isModalVisible 状态变量。

    ¥Create the onModalClose() function to update the isModalVisible state variable.

  5. <EmojiPicker> 组件放在 Index 组件的底部。

    ¥Place the <EmojiPicker> component at the bottom of the Index component.

app/(tabs)/index.tsx
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 () => {
  // we will implement this later
};

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}>
      {/* A list of emoji component will go here */}
    </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',
},
});

这是这一步之后的结果:

¥Here is the result after this step:

4

显示表情符号列表

¥Display a list of emoji

让我们在模态框的内容中添加一个水平表情符号列表。我们将使用 React Native 中的 <FlatList> 组件。

¥Let's add a horizontal list of emoji in the modal's content. We'll use the <FlatList> component from React Native for it.

在 components 目录中创建一个 EmojiList.tsx 文件,并添加以下代码:

¥Create a EmojiList.tsx file inside the components directory and add the following code:

components/EmojiList.tsx
import { useState } from 'react';
import { StyleSheet, FlatList, Platform, Pressable } from 'react-native';
import { Image, type ImageSource } from 'expo-image';

type Props = {
onSelect: (image: ImageSource) => void;
onCloseModal: () => void;
};

export default function EmojiList({ onSelect, onCloseModal }: Props) {
const [emoji] = useState<ImageSource[]>([
  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,
},
});

我们来了解一下上面的代码做了什么:

¥Let's learn what the above code does:

  • 上面的 <FlatList> 组件使用 Image 组件渲染所有表情符号图片,并由 <Pressable> 封装。稍后,我们将对其进行改进,以便用户可以点击屏幕上的表情符号,使其显示为图片上的贴纸。

    ¥The <FlatList> component above renders all the emoji images using the Image component, wrapped by a <Pressable>. Later, we will improve it so that the user can tap an emoji on the screen to make it appear as a sticker on the image.

  • 它还将 emoji 数组变量提供的项目数组作为 data prop 的值。renderItem 属性从 data 中获取项目并返回列表中的项目。最后,我们添加了 Image<Pressable> 组件来显示此项目。

    ¥It also takes an array of items provided by the emoji array variable as the value of the data prop. The renderItem prop takes the item from the data and returns the item in the list. Finally, we added Image and the <Pressable> components to display this item.

  • horizontal 属性水平渲染列表而不是垂直渲染。showsHorizontalScrollIndicator 使用 React Native 的 Platform 模块检查值并在网页上显示水平滚动条。

    ¥The horizontal prop renders the list horizontally instead of vertically. The showsHorizontalScrollIndicator uses React Native's Platform module to check the value and display the horizontal scroll bar on web.

现在,更新 app/(tabs)/index.tsx 以导入 <EmojiList> 组件,并用以下代码片段替换 <EmojiPicker> 组件内的注释:

¥Now, update the app/(tabs)/index.tsx to import the <EmojiList> component and replace the comments inside the <EmojiPicker> component with the following code snippet:

app/(tabs)/index.tsx
import { View, StyleSheet } from 'react-native';
import * as ImagePicker from 'expo-image-picker';
import { useState } from 'react';
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';

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 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 () => {
  // we will implement this later
};

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 prop 选择表情符号,选择后,onCloseModal 关闭模式。

¥In the EmojiList component, the onSelect prop selects the emoji and after selecting it, the onCloseModal closes the modal.

让我们看看我们在 Android、iOS 和 Web 上的应用:

¥Let's take a look at our app on Android, iOS and the web:

5

显示选定的表情符号

¥Display the selected emoji

现在,我们将表情符号贴纸放在图片上。在组件目录中创建一个新文件并将其命名为 EmojiSticker.tsx。然后,添加以下代码:

¥Now, we'll put the emoji sticker on the image. Create a new file in the components directory and call it EmojiSticker.tsx. Then, add the following code:

components/EmojiSticker.tsx
import { View } from 'react-native';
import { Image, type ImageSource } from 'expo-image';

type Props = {
imageSize: number;
stickerSource: ImageSource;
};

export default function EmojiSticker({ imageSize, stickerSource }: Props) {
return (
  <View style={{ top: -350 }}>
    <Image source={stickerSource} style={{ width: imageSize, height: imageSize }} />
  </View>
);
}

该组件接收两个 props:

¥This component receives two props:

  • imageSizeIndex 组件内部定义的值。我们将在下一章中使用这个值来缩放点击时图片的大小。

    ¥imageSize: a value defined inside the Index component. We will use this value in the next chapter to scale the image's size when tapped.

  • stickerSource:所选表情符号图片的来源。

    ¥stickerSource: the source of the selected emoji image.

在 app/(tabs)/index.tsx 文件中导入此组件并更新 Index 组件以在图片上显示表情符号贴纸。我们将检查 pickedEmoji 状态是否不是 undefined

¥Import this component in the app/(tabs)/index.tsx file and update the Index component to display the emoji sticker on the image. We'll check if the pickedEmoji state is not undefined:

app/(tabs)/index.tsx
import { View, StyleSheet } from 'react-native';
import * as ImagePicker from 'expo-image-picker';
import { useState } from 'react';
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 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 () => {
  // we will implement this later
};

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 上的应用:

¥Let's take a look at our app on Android, iOS and the web:

概括

¥Summary

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.

Next: Add gestures