This documentation is available as Markdown for AI agents and LLMs. See the full Markdown index or append .md to any documentation URL.

モーダルを作成する

ページを編集

このチュートリアルでは、Expo UI の BottomSheet コンポーネントで絵文字ピッカーを作成し、選択した絵文字を React Native アプリにステッカーとして表示する方法を学びます。


モーダルは、アプリの他のコンテンツの上にコンテンツを表示します。一般的に、モーダルはユーザーの注意を重要な情報に向けたり、アクションを促したりするために使用されます。ボトムシートは、画面下からスライドして表示されるモーダルの一種です。

この章では、Expo UI の BottomSheet コンポーネントを使って絵文字ピッカーを作成します。Android と iOS では、プラットフォームのネイティブのボトムシートを表示します。web では、ページ下部にシートを表示します。

視聴:ユニバーサル Expo アプリでモーダルを作成する
視聴:ユニバーサル Expo アプリでモーダルを作成する

React Native の Modal API を使ってモーダルコンポーネントを構築し、絵文字ピッカーを表示してユーザー操作を処理します。


1

ボタンを表示するための状態変数を宣言する

ボトムシートを実装する前に、新しいボタンを 3 つ追加します。これらのボタンは、ユーザーがメディアライブラリから画像を選択したり、プレースホルダー画像を使ったりした後に表示されます。ボタンのうちの 1 つが絵文字ピッカーを開きます。

src/app/(tabs)/index.tsx で:

  1. 絵文字ピッカーを開くボタンや他のオプションの表示・非表示を切り替えるために、ブール値の状態変数 showAppOptions を宣言します。アプリ画面の読み込み時には false にして、画像を選択する前にはオプションを表示しないようにします。ユーザーが画像を選択したり、プレースホルダー画像を使ったりすると、true に設定します。
  2. pickImageAsync() 関数を更新して、ユーザーが画像を選択した後に showAppOptions の値を true に設定します。
  3. テーマが設定されていないボタンの onPress プロパティで、alert() を showAppOptions を true に設定する関数に置き換えます。
src/app/(tabs)/index.tsx
import { useState } from 'react'; import { Platform, View, StyleSheet } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import * as ImagePicker from 'expo-image-picker'; import Button from '@/components/button'; import ImageViewer from '@/components/image-viewer'; 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 () => { const 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 ( <SafeAreaView 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> )} </SafeAreaView> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#25292e', alignItems: 'center', }, imageContainer: { flex: 1, paddingTop: Platform.select({ web: 96, default: 20 }), }, footerContainer: { flex: 1 / 3, alignItems: 'center', }, });

上記のスニペットでは、showAppOptions の値に応じて Button コンポーネントをレンダリングし、ボタンを三項演算子ブロックの中に移動しています。showAppOptions の値が true の場合は、空の <View> コンポーネントをレンダリングします。この状態については次のステップで対応します。

2

ボタンを追加する

この章で実装するオプションボタンのレイアウトを分解してみましょう。デザインは次のようになっています。

親の <View> の中に、横一列に並んだ 3 つのボタンが含まれています。中央のプラスアイコン (+) のボタンは絵文字ピッカーを開くもので、他の 2 つとは異なるスタイルになっています。

第 3 章 の Button コンポーネントと同様に、オプションボタンは Android と iOS では Expo UI を、web では React Native のコンポーネントを使用します。各ボタンには、プラットフォームごとに 1 つずつファイルがあります。

src/components ディレクトリ内に、円形ボタン用の 3 つのファイルを作成します。

src/components/circle-button.android.tsx
import { StyleSheet, View } from 'react-native'; import { Host, FloatingActionButton, Icon } from '@expo/ui/jetpack-compose'; import { clip, Shapes, size } from '@expo/ui/jetpack-compose/modifiers'; type Props = { onPress: () => void; }; export default function CircleButton({ onPress }: Props) { return ( <View style={styles.circleButtonContainer}> <Host matchContents> <FloatingActionButton containerColor="#ffd33d" onClick={onPress} modifiers={[size(84, 84), clip(Shapes.Circle)]}> <FloatingActionButton.Icon> <Icon source={require('@expo/material-symbols/add.xml')} size={38} tint="#25292e" /> </FloatingActionButton.Icon> </FloatingActionButton> </Host> </View> ); } const styles = StyleSheet.create({ circleButtonContainer: { marginHorizontal: 60, }, });

上記のコードが何をしているのか確認しましょう。

  • Android では、FloatingActionButton が Material 3 のフローティングアクションボタンを表示します。size と clip モディファイアで 84 dp の円形にし、Icon が @expo/material-symbols の add.xml アイコンを表示します。
  • iOS では、Button は plain スタイルを使用し、そのラベルは SF Symbols の Image です。frame と background モディファイアで、アイコンの周りに 84 ポイントの黄色い円を描画します。
  • web では、第 3 章の button.tsx と同様に、circle-button.tsx は Pressable と SymbolView を使用します。

他の 2 つのボタンは、テキストラベルの上にアイコンを表示します。src/components ディレクトリ内に、アイコンボタン用の 3 つのファイルを作成します。このコンポーネントは 3 つのプロパティを受け取ります。

  • icon: プラットフォームごとのアイコンです。Android は @expo/material-symbols の XML アイコン、iOS は SF Symbols の名前、web は Material Symbols の名前を使用します。
  • label: ボタンに表示されるテキストラベルです。
  • onPress: ユーザーがボタンを押したときに実行される関数です。
src/components/icon-button.android.tsx
import type { ImageSourcePropType } from 'react-native'; import { Host, TextButton, Column, Icon, Text } from '@expo/ui/jetpack-compose'; import type { AndroidSymbol, SFSymbol } from 'expo-symbols'; type Props = { icon: { android: ImageSourcePropType; ios: SFSymbol; web: AndroidSymbol; }; label: string; onPress: () => void; }; export default function IconButton({ icon, label, onPress }: Props) { return ( <Host matchContents> <TextButton onClick={onPress} colors={{ contentColor: '#fff' }}> <Column horizontalAlignment="center" verticalArrangement={{ spacedBy: 12 }}> <Icon source={icon.android} size={24} tint="#fff" /> <Text color="#fff">{label}</Text> </Column> </TextButton> </Host> ); }

Android では、Column が Icon を Text の上に配置します。iOS では、VStack が同じ役割を果たします。第 3 章の Button のファイルと同様に、3 つのファイルはすべて同じプロパティを受け付けます。

src/components ディレクトリには、各ボタンに 3 つずつファイルが含まれるようになりました。

src
 components
  button.android.tsxAndroid で使用
  button.ios.tsxiOS で使用
  button.tsxweb で使用
  circle-button.android.tsxAndroid で使用
  circle-button.ios.tsxiOS で使用
  circle-button.tsxweb で使用
  icon-button.android.tsxAndroid で使用
  icon-button.ios.tsxiOS で使用
  icon-button.tsxweb で使用
  image-viewer.tsx

src/app/(tabs)/index.tsx 内で:

  1. CircleButton と IconButton コンポーネントをインポートして表示できるようにします。
  2. これらのボタン用に 3 つのプレースホルダー関数を追加します。onReset() 関数はユーザーがリセットボタンを押したときに実行され、画像ピッカーのボタンが再度表示されるようにします。残りの 2 つの関数の機能は後で実装します。
  3. 各 IconButton の icon プロパティに、プラットフォームごとのアイコンを渡します。
src/app/(tabs)/index.tsx
import { useState } from 'react'; import { Platform, View, StyleSheet } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import * as ImagePicker from 'expo-image-picker'; import Button from '@/components/button'; import ImageViewer from '@/components/image-viewer'; import IconButton from '@/components/icon-button'; import CircleButton from '@/components/circle-button'; 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 () => { const 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 ( <SafeAreaView style={styles.container}> <View style={styles.imageContainer}> <ImageViewer imgSource={PlaceholderImage} selectedImage={selectedImage} /> </View> {showAppOptions ? ( <View style={styles.footerContainer}> <View style={styles.optionsRow}> <IconButton icon={{ android: require('@expo/material-symbols/refresh.xml'), ios: 'arrow.counterclockwise', web: 'refresh', }} label="Reset" onPress={onReset} /> <CircleButton onPress={onAddSticker} /> <IconButton icon={{ android: require('@expo/material-symbols/download.xml'), ios: 'square.and.arrow.down', web: 'download', }} 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> )} </SafeAreaView> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#25292e', alignItems: 'center', }, imageContainer: { flex: 1, paddingTop: Platform.select({ web: 96, default: 20 }), }, footerContainer: { flex: 1 / 3, alignItems: 'center', }, optionsRow: { alignItems: 'center', flexDirection: 'row', }, });

オプションボタンは、Choose a photo ボタンと Use this photo ボタンと同じ footerContainer スタイルを使用します。そのため、画面上の同じ位置、つまりタブバーの上に表示されます。

Android、iOS、web でアプリを確認してみましょう。

3

ボトムシートで絵文字ピッカーを作成する

ボトムシートでは、利用可能な絵文字のリストからユーザーが絵文字を選択できます。src/components ディレクトリ内に emoji-picker.tsx ファイルを作成します。このコンポーネントは 3 つのプロパティを受け取ります。

  • isVisible: ボトムシートを表示するかどうかを決定するブール値です。
  • onClose: ボトムシートを閉じる関数です。
  • children: 後で絵文字のリストを表示するために使用します。
src/components/emoji-picker.tsx
import { PropsWithChildren } from 'react'; import { StyleSheet, Text, View } from 'react-native'; import { BottomSheet, RNHostView } from '@expo/ui'; type Props = PropsWithChildren<{ isVisible: boolean; onClose: () => void; }>; export default function EmojiPicker({ isVisible, children, onClose }: Props) { return ( <BottomSheet isPresented={isVisible} onDismiss={onClose} snapPoints={['half']} containerColor="#25292e"> <RNHostView> <View> <Text style={styles.title}>Choose a sticker</Text> {children} </View> </RNHostView> </BottomSheet> ); } const styles = StyleSheet.create({ title: { color: '#fff', fontSize: 16, marginBottom: 20, }, });

上記のコードが何をしているのか確認しましょう。

  • BottomSheet コンポーネントは、デフォルトテンプレートにすでに含まれている @expo/ui から提供されます。Android では Material 3 のボトムシートを表示します。iOS では SwiftUI のシートを表示します。web では、ページ下部にシートを表示します。
  • isPresented プロパティは isVisible の値を受け取り、ボトムシートが開いているか閉じているかを制御します。
  • onDismiss プロパティは、ユーザーがボトムシートを閉じたときに onClose を呼び出します。ユーザーは、ボトムシートを下にスワイプするか、ボトムシートの外側をタップして閉じることができます。Android では、戻るボタンでも閉じます。web では、Esc キーでも閉じます。
  • snapPoints プロパティは、ボトムシートが停止できる高さを設定します。'half' の値は、画面の高さの約半分でボトムシートを開きます。
  • containerColor プロパティは、アプリの背景に合わせてボトムシートの背景色を設定します。
  • RNHostView コンポーネントは、<View> や <Text> などの React Native のビューをボトムシート内に表示します。
RNHostView とは何ですか?

Android と iOS では、BottomSheet は Jetpack Compose と SwiftUI のネイティブビューです。その中に React Native のビューを表示するには、それらを RNHostView でラップします。これは Host と逆方向に機能します。Host は、第 3 章 で React Native のビュー内に Expo UI コンポーネントを表示するために使用しました。BottomSheet は独自の Host を作成するため、Host でラップする必要はありません。詳細については、RNHostView を参照してください。

それでは、src/app/(tabs)/index.tsx を変更しましょう。

  1. <EmojiPicker> コンポーネントをインポートします。
  2. useState フックで isModalVisible 状態変数を作成します。デフォルト値は false で、ユーザーがボタンを押すまでボトムシートは非表示のままです。
  3. onAddSticker() 関数内のコメントを置き換えて、ユーザーがボタンを押したときに isModalVisible 変数を true に更新します。これで絵文字ピッカーが開きます。
  4. isModalVisible 状態変数を更新する onModalClose() 関数を作成します。
  5. Index コンポーネントの末尾に <EmojiPicker> コンポーネントを配置します。
src/app/(tabs)/index.tsx
import { useState } from 'react'; import { Platform, View, StyleSheet } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import * as ImagePicker from 'expo-image-picker'; import Button from '@/components/button'; import ImageViewer from '@/components/image-viewer'; import IconButton from '@/components/icon-button'; import CircleButton from '@/components/circle-button'; import EmojiPicker from '@/components/emoji-picker'; 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 () => { const 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 ( <SafeAreaView style={styles.container}> <View style={styles.imageContainer}> <ImageViewer imgSource={PlaceholderImage} selectedImage={selectedImage} /> </View> {showAppOptions ? ( <View style={styles.footerContainer}> <View style={styles.optionsRow}> <IconButton icon={{ android: require('@expo/material-symbols/refresh.xml'), ios: 'arrow.counterclockwise', web: 'refresh', }} label="Reset" onPress={onReset} /> <CircleButton onPress={onAddSticker} /> <IconButton icon={{ android: require('@expo/material-symbols/download.xml'), ios: 'square.and.arrow.down', web: 'download', }} 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}> {/* Emoji list component will go here */} </EmojiPicker> </SafeAreaView> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#25292e', alignItems: 'center', }, imageContainer: { flex: 1, paddingTop: Platform.select({ web: 96, default: 20 }), }, footerContainer: { flex: 1 / 3, alignItems: 'center', }, optionsRow: { alignItems: 'center', flexDirection: 'row', }, });

このステップ完了後の結果は次のとおりです。

4

絵文字のリストを表示する

ボトムシートのコンテンツに絵文字のグリッドを追加しましょう。これには React Native の <FlatList> コンポーネントを使用します。

src/components ディレクトリ内に emoji-list.tsx ファイルを作成し、次のコードを追加します。

src/components/emoji-list.tsx
import { FlatList, Pressable, StyleSheet } from 'react-native'; import { Image, type ImageSource } from 'expo-image'; type Props = { onSelect: (image: ImageSource) => void; onCloseModal: () => void; }; const emoji: 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'), ]; export default function EmojiList({ onSelect, onCloseModal }: Props) { return ( <FlatList data={emoji} numColumns={3} columnWrapperStyle={styles.row} renderItem={({ item }) => ( <Pressable onPress={() => { onSelect(item); onCloseModal(); }}> <Image source={item} style={styles.image} /> </Pressable> )} /> ); } const styles = StyleSheet.create({ row: { justifyContent: 'space-around', marginBottom: 20, }, image: { width: 100, height: 100, }, });

上記のコードが何をしているのか確認しましょう。

  • emoji 配列は絵文字画像を保持します。値が変わることはないため、コンポーネントの外側に置いています。
  • <FlatList> コンポーネントは、data プロパティの値として emoji 配列を受け取ります。renderItem プロパティは data から各アイテムを受け取り、<Pressable> でラップした Image コンポーネントを返します。ユーザーが絵文字を押すと、onSelect がその絵文字を受け取り、onCloseModal がボトムシートを閉じます。
  • numColumns プロパティは、リストを 3 列のグリッドとして表示します。columnWrapperStyle プロパティは、グリッドの各行のスタイルを設定します。

次に、src/app/(tabs)/index.tsx を更新して <EmojiList> コンポーネントをインポートし、<EmojiPicker> コンポーネント内のコメントを次のコードスニペットに置き換えます。

src/app/(tabs)/index.tsx
import { useState } from 'react'; import { Platform, View, StyleSheet } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import * as ImagePicker from 'expo-image-picker'; import type { ImageSource } from 'expo-image'; import Button from '@/components/button'; import ImageViewer from '@/components/image-viewer'; import IconButton from '@/components/icon-button'; import CircleButton from '@/components/circle-button'; import EmojiPicker from '@/components/emoji-picker'; import EmojiList from '@/components/emoji-list'; 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 () => { const 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 ( <SafeAreaView style={styles.container}> <View style={styles.imageContainer}> <ImageViewer imgSource={PlaceholderImage} selectedImage={selectedImage} /> </View> {showAppOptions ? ( <View style={styles.footerContainer}> <View style={styles.optionsRow}> <IconButton icon={{ android: require('@expo/material-symbols/refresh.xml'), ios: 'arrow.counterclockwise', web: 'refresh', }} label="Reset" onPress={onReset} /> <CircleButton onPress={onAddSticker} /> <IconButton icon={{ android: require('@expo/material-symbols/download.xml'), ios: 'square.and.arrow.down', web: 'download', }} 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> </SafeAreaView> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#25292e', alignItems: 'center', }, imageContainer: { flex: 1, paddingTop: Platform.select({ web: 96, default: 20 }), }, footerContainer: { flex: 1 / 3, alignItems: 'center', }, optionsRow: { alignItems: 'center', flexDirection: 'row', }, });

EmojiList コンポーネントでは、onSelect プロパティで絵文字を選択し、選択後に onCloseModal でボトムシートを閉じます。

Android、iOS、web でアプリを確認してみましょう。

5

選択した絵文字を表示する

次に、画像の上に絵文字ステッカーを配置します。src/components ディレクトリに新しいファイルを作成し、emoji-sticker.tsx という名前にします。そして次のコードを追加します。

src/components/emoji-sticker.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> ); }

このコンポーネントは 2 つのプロパティを受け取ります。

  • imageSize: Index コンポーネント内で定義された値です。次の章で、タップ時に画像サイズを変更するために使用します。
  • stickerSource: 選択された絵文字画像のソースです。

このコンポーネントを src/app/(tabs)/index.tsx ファイルでインポートし、Index コンポーネントを更新して画像上に絵文字ステッカーを表示します。pickedEmoji 状態が undefined でないかをチェックします。

src/app/(tabs)/index.tsx
import { useState } from 'react'; import { Platform, View, StyleSheet } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import * as ImagePicker from 'expo-image-picker'; import type { ImageSource } from 'expo-image'; import Button from '@/components/button'; import ImageViewer from '@/components/image-viewer'; import IconButton from '@/components/icon-button'; import CircleButton from '@/components/circle-button'; import EmojiPicker from '@/components/emoji-picker'; import EmojiList from '@/components/emoji-list'; import EmojiSticker from '@/components/emoji-sticker'; 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 () => { const 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 ( <SafeAreaView style={styles.container}> <View style={styles.imageContainer}> <ImageViewer imgSource={PlaceholderImage} selectedImage={selectedImage} /> {pickedEmoji && <EmojiSticker imageSize={40} stickerSource={pickedEmoji} />} </View> {showAppOptions ? ( <View style={styles.footerContainer}> <View style={styles.optionsRow}> <IconButton icon={{ android: require('@expo/material-symbols/refresh.xml'), ios: 'arrow.counterclockwise', web: 'refresh', }} label="Reset" onPress={onReset} /> <CircleButton onPress={onAddSticker} /> <IconButton icon={{ android: require('@expo/material-symbols/download.xml'), ios: 'square.and.arrow.down', web: 'download', }} 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> </SafeAreaView> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#25292e', alignItems: 'center', }, imageContainer: { flex: 1, paddingTop: Platform.select({ web: 96, default: 20 }), }, footerContainer: { flex: 1 / 3, alignItems: 'center', }, optionsRow: { alignItems: 'center', flexDirection: 'row', }, });

Android、iOS、web でアプリを確認してみましょう。

まとめ

第 5 章:モーダルを作成する

絵文字ピッカーのモーダルを作成し、絵文字を選択して画像の上に表示するロジックの実装が完了しました。

次の章では、絵文字をドラッグしたり、タップでサイズを拡大縮小したりする、ジェスチャーを使ったユーザー操作を追加します。

次へ:ジェスチャーを追加する