用Expo和React-native写一个app

2,396 阅读16分钟

用Expo和React-native写一个app,来自Expo官方教程。Expo 是一个开源框架和平台,用于构建 React Native 应用程序。这篇笔记主要是记录学习的过程。

  • 第一章 创建一个应用
  • 第二章 增加导航
  • 第三章 构建一个屏幕
  • 第四章 添加一个图片选择器
第一章 创建一个应用

我们按照下面5个步骤开启我们的第一章

  1. 初始化一个新的 Expo 应用 (使用默认模板)
  2. 下载项目需要用到的资源(图片),放到项目里
  3. 运行npm run reset-project命令,移除模板代码
  4. 运行npx expo start命令把项目跑起来
  5. 编辑app/index.tsx,调整首页
初始化一个新的 Expo 应用
npx create-expo-app@latest StickerSmash && cd StickerSmash

上面代码就是初始化了一个StickerSmash的expo应用, 使用了默认的模版,使用默认模板的好处

• 创建一个带有 expo 包的新 React Native 项目。
• 包含推荐的工具,例如 Expo CLI
• 包含 Expo Router 的标签导航器,提供基本的导航系统。
• 自动配置为在多个平台上运行项目:Android、iOS 和 Web
• 默认配置了 TypeScript

image.png

下载项目需要用到的资源(图片)

下载后的解压,替换项目里的assets/images文件夹,这是替换后的样子

image.png

运行npm run reset-project命令

运行上述命令后,app 目录中只剩下两个文件(index.tsx 和 _layout.tsx)。脚本将 app 和其他目录(如 components、constants 和 hooks,这些目录中包含的模板代码)中的文件移动到了 app-example 目录中。我们将在开发过程中,根据需要创建自己的目录和组件文件。

image.png

运行npx expo start命令

image.png

按照提示打开http://localhost:8081/, 如果打不开,就执行一次npm i,我就是执行了npm i 后才可以正常打开

image.png

通过ios相机应用扫码打开的expo go页面和浏览器打开的页面

image.png

编辑首页屏幕

编辑app/index.tsx

  1. react-native 中导入 StyleSheet,并创建一个 styles 对象来定义自定义样式。
  2. <View> 添加 styles.container.backgroundColor 属性:
    • 属性值:#25292e
    • 功能:更改背景颜色。
  3. <Text> 的默认内容替换为:
    • "Home screen"
  4. <Text> 添加 styles.text.color 属性:
    • 属性值:#fff(白色)
    • 功能:更改文本颜色。
import { Text, View, StyleSheet } from 'react-native'

export default function Index() {
  return (
    <View style={styles.container}>
      <Text style={styles.text}>Home screen</Text>
    </View>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#25292e',
    justifyContent: 'center',
    alignItems: 'center',
  },
  text: {
    color: '#fff',
  },
})

image.png

第二章 增加导航

在本章节中,我们将学习 Expo Router 的基础知识,以创建堆栈导航和带有两个选项卡的底部标签栏。

Expo Router是一个用于React Native和Web应用的基于文件的路由框架。它管理屏幕之间的导航,并在多个平台上使用相同的组件。要开始使用,我们需要了解以下约定:

  • app 目录: 一个特殊的目录,仅包含路由及其布局。添加到此目录的任何文件都会成为原生应用中的一个屏幕或Web应用中的一个页面。
  • 根布局: app/_layout.tsx文件,用于定义共享的UI元素(例如标题栏和标签栏),以确保在不同路由之间保持一致性。
  • 文件命名约定: index文件(如index.tsx)的名称与其父目录相匹配,不会添加路径段。例如,app目录中的index.tsx文件对应/路由。
  • 路由文件:路由文件需要导出一个React组件作为默认值。可以使用.js、.jsx、.tx或.tsx扩展名。
  • 统一的导航结构:Android、iOS和Web共享统一的导航结构。
向堆栈中添加一个新屏幕

让我们在app目录中创建一个名为about.tsx的新文件。当用户导航到/about路由时,它将显示屏幕名称。

什么是Stack (堆栈)?
堆栈导航器是应用程序中用于在不同屏幕之间导航的核心机制。以下是它在不同平台上的工作方式:

  • 在Android上: 堆栈路由通过从底部滑入的动画效果显示在当前屏幕的上方。
  • 在iOS上: 堆栈路由通过从右侧滑入的动画效果显示,模仿iOS原生的导航行为。

Expo Router提供了一个Stack组件,用于创建导航堆栈,使您能够轻松添加和管理应用中的新路由。

在屏幕之间导航

我们将使用Expo Router提供的Link组件,从/index路由导航到/about路由。它是一个React组件,会渲染一个带有指定href属性的标签。

  1. 在index.tsx中引入Link组件,Link组件要从expo-router中导入
  2. 在Text组件后增加一个Link组件,给Link组件传递href属性,值为/about路由。
  3. 为Link组件添加样式,设置fontSize、textDecorationLine、color, Link组件接受与相同的属性
// about.tsx
import { Text, View, StyleSheet } from 'react-native';

export default function AboutScreen() {
  return (
    <View style={styles.container}>
      <Text style={styles.text}>About screen</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#25292e',
    justifyContent: 'center',
    alignItems: 'center',
  },
  text: {
    color: '#fff',
  },
});
// index.tsx
import { Text, View, StyleSheet } from 'react-native'
import { Link } from 'expo-router'

export default function Index() {
  return (
    <View style={styles.container}>
      <Text style={styles.text}>Home screen</Text>
      <Link href="/about" style={styles.button}>
        Go to About screen
      </Link>
    </View>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#25292e',
    alignItems: 'center',
    justifyContent: 'center',
  },
  text: {
    color: '#fff',
  },
  button: {
    fontSize: 20,
    textDecorationLine: 'underline',
    color: '#fff',
  },
})
import { Stack } from 'expo-router'

export default function RootLayout() {
  return (
    <Stack>
      <Stack.Screen name="index" options={{ title: 'Home' }} />
      <Stack.Screen name="about" options={{ title: 'About' }} />
    </Stack>
  )
}

新增一个about页面效果
localhost-8081.gif

添加一个not-found路由

当一个路由不存在时,我们可以使用+not-found路由来显示一个备用屏幕。这在移动端导航到无效路由时非常有用,可以显示一个自定义屏幕,而不是让应用崩溃或在web上显示404错误页面。Expo Router使用一个特殊的+not-found.tsx 文件来处理这种情况。

  1. 创建一个名为+not-found.tsx的新文件
    在app目录下创建,用于添加NotFoundScreen组件
  2. 添加options属性
    从Stack.Screen中添加options属性,为此路由设置一个自定义标题
  3. 添加一个Link组件 用于导航到/路由,这是我们的备用路由
// +not-found.tsx
import { View, StyleSheet } from 'react-native'
import { Link, Stack } from 'expo-router'

export default function NotFoundScreen() {
  return (
    <>
      <Stack.Screen options={{ title: 'Oops! Not Found' }} />
      <View style={styles.container}>
        <Link href="/" style={styles.button}>
          Go back to Home screen!
        </Link>
      </View>
    </>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#25292e',
    justifyContent: 'center',
    alignItems: 'center',
  },

  button: {
    fontSize: 20,
    textDecorationLine: 'underline',
    color: '#fff',
  },
})

给一个不存在的路由就会跳转到新建的+not-found.tsx这个组件 image.png

添加底部标签导航器

我们将为应用程序添加一个底部标签导航器,并复用现有的Home和About屏幕,来创建一个标签布局(这种导航模式在许多社交媒体应用中很常见,如 X 或 BlueSky)。同时,我们会在根布局中使用堆栈导航器,这样+not-found路由可以覆盖其他嵌套导航器。

实现步骤:

  1. 在app目录下添加一个(tabs)子目录 这个特殊的目录用于将路由分组,并在底部标签栏中显示它们。
  2. 在(tabs)目录中创建_layout.tsx文件
  • 这个文件将定义标签布局,独立于根布局(Root layout)
  • 用于设置底部标签栏的配置,例如标签内容、图表等
  1. 将现有的index.tsx和about.tsx文件移动到(tabs)目录中
  • 这样,它们就被视为底部标签中的两个页面

最终的文件目录如下

app/
├── +not-found.tsx
├── (tabs)/
│   ├── _layout.tsx  # 定义底部标签布局
│   ├── index.tsx    # 首页(Home)
│   └── about.tsx    # 关于页(About)
└── _layout.tsx       # 根布局

更新根布局文件添加一个 (tabs) 路由: 注意⚠️我这里tabs用括号包起来了 在Expo Router中,文件或文件夹名称的括号()有特殊的含义,用于定义分组路由(group routes)

  • 括号()将文件夹标记为一个"分组",表示这个文件夹的内容会被视为一组相关的路由。
  • 分组文件夹本身不会生成对应的路由路径,它只是一个逻辑分组。
    • 例如,(tabs) 文件夹下的 index.tsx 和 about.tsx 分别映射为 / 和 /about,而不是 /(tabs) 和 /(tabs)/about。
import { Stack } from 'expo-router'

export default function RootLayout() {
  return (
    <Stack>
      <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
      <Stack.Screen name="+not-found" />
    </Stack>
  )
}

在 (tabs)/_layout.tsx 文件中,添加一个 Tabs 组件来定义底部标签布局:

import { Tabs } from 'expo-router';

export default function TabLayout() {
  return (
    <Tabs>
      <Tabs.Screen name="index" options={{ title: 'Home' }} />
      <Tabs.Screen name="about" options={{ title: 'About' }} />
    </Tabs>
  );
}

image.png

更新底部标签导航外观

目前,底部标签导航在所有平台上的样式都一样,但与我们应用的风格不匹配。例如,标签栏或标题没有显示自定义图标,底部标签的背景颜色也与应用到背景颜色不一致。

修改(tabs)/_layout.txs文件以添加标签栏图标:

  • 从@expo/vector-icons库中导入Ionicons图标集-这是一个包含流行图标集的库。
  • 为index和about路由添加tabBarIcon.此函数接受focused和color作为参数,并渲染图标组件。我们可以从图标集中提供自定义图标名称。
  • 在Tabs组件中添加screenOptions.tabBarActiveTintColor,并将其值设置为#ffd33d,这将更改标签栏图标和标签在激活状态下的颜色。
// (tabs)/_layout.txs
import { Tabs } from 'expo-router'

import Ionicons from '@expo/vector-icons/Ionicons'

export default function TabLayout() {
  return (
    <Tabs
      screenOptions={{
        tabBarActiveTintColor: '#ffd33d',
        headerStyle: {
          backgroundColor: '#25292e',
        },
        headerShadowVisible: false,
        headerTintColor: '#fff',
        tabBarStyle: {
          backgroundColor: '#25292e',
        },
      }}
    >
      <Tabs.Screen
        name="index"
        options={{
          title: 'Home',
          tabBarIcon: ({ color, focused }) => (
            <Ionicons
              name={focused ? 'home-sharp' : 'home-outline'}
              color={color}
              size={24}
            />
          ),
        }}
      />
      <Tabs.Screen
        name="about"
        options={{
          title: 'About',
          tabBarIcon: ({ color, focused }) => (
            <Ionicons
              name={
                focused ? 'information-circle' : 'information-circle-outline'
              }
              color={color}
              size={24}
            />
          ),
        }}
      />
    </Tabs>
  )
}

image.png

第三章 构建一个屏幕

在本教程中,学习如何使用React Native的Pressable和Expo的Image等组件来构建StickerSmash应用的第一个屏幕。

效果图 要实现的效果

有两个关键元素, 1. 屏幕中央显示一张大图片; 2. 屏幕下半部分有两个按钮

image.png

显示图片

我们将使用 expo-image 库在应用中显示图片。该库提供了一个跨平台的 组件,用于加载和渲染图片。
通过在终端中按下 Ctrl + c 停止开发服务器。然后,安装 expo-image 库:

npx expo install expo-image

npx expo install 命令将安装库并将其添加到项目的 package.json 依赖中。

Image 组件将图片的来源作为其值。来源可以是静态资源或 URL。例如,从 assets/images 目录中引入的资源是静态的。它也可以通过 uri 属性从网络中获取。

在 app/(tabs)/index.tsx 文件中使用 Image 组件:

  1. 从 expo-image 库中导入Image
  2. 创建一个 PlaceholderImage 变量,将 assets/images/background-image.png 文件作为 Image 组件的 source 属性使用
// app/(tabs)/index.tsx
import { View, StyleSheet } from 'react-native';
import { Image } from 'expo-image'; 

const PlaceholderImage = require('@/assets/images/background-image.png');
export default function Index() {
  return (
    <View style={styles.container}>
      <View style={styles.imageContainer}>
        <Image source={PlaceholderImage} style={styles.image} />
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#25292e',
    alignItems: 'center',
  },
  imageContainer: {
    flex: 1,
  },
  image: {
    width: 320,
    height: 440,
    borderRadius: 18,
  },
});

image.png

将组件拆分到不同的文件中

随着我们向屏幕中添加更多组件,可以将代码拆分到多个文件中。在整个教程中,我们将使用 components 目录来创建自定义组件。

  1. 创建一个 components 目录,并在其中创建 ImageViewer.tsx 文件。
  2. 将用于显示图片的代码及其样式移动到这个文件中。
// components/ImageViewer.tsx
import { StyleSheet } from "react-native";
import { Image, type ImageSource } from "expo-image";

type Props = {
  imgSource: ImageSource;
};

export default function ImageViewer({ imgSource }: Props) {
  return <Image source={imgSource} style={styles.image} />;
}

const styles = StyleSheet.create({
  image: {
    width: 320,
    height: 440,
    borderRadius: 18,
  },
});

由于 ImageViewer 是一个自定义组件,我们将其放置在单独的目录中,而不是 app 目录中。app 目录中的每个文件都是布局文件或路由文件。有关更多信息,请参阅非路由文件(Non-route files)

// app/(tabs)/index.tsx:
import { StyleSheet, View } from 'react-native';
import ImageViewer from "@/components/ImageViewer"; 

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

export default function Index() {
  return (
    <View style={styles.container}>
      <View style={styles.imageContainer}>
        <ImageViewer imgSource={PlaceholderImage} />
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#25292e',
    alignItems: 'center',
  },
  imageContainer: {
    flex: 1,
  },
});

@ 符号是一个自定义路径别名,用于导入自定义组件和其他模块,替代相对路径。Expo CLI 会在 tsconfig.json 中自动配置这个别名。

使用 Pressable 创建按钮

React Native 提供了几种不同的组件来处理触摸事件,但推荐使用 ,因为它的灵活性。它可以检测单次点击、长按,以及在按钮按下和释放时触发不同的事件等功能。

在设计中,我们需要创建两个按钮,每个按钮都有不同的样式和标签。让我们从创建一个可重复使用的按钮组件开始。在 components 目录下创建一个 Button.tsx 文件,并添加以下代码:

// components/Button.tsx
import { StyleSheet, View, Pressable, Text } from 'react-native';

type Props = {
  label: string;
};

export default function Button({ label }: Props) {
  return (
    <View style={styles.buttonContainer}>
      <Pressable style={styles.button} onPress={() => alert('You pressed a button.')}>
        <Text style={styles.buttonLabel}>{label}</Text>
      </Pressable>
    </View>
  );
}

const styles = StyleSheet.create({
  buttonContainer: {
    width: 320,
    height: 68,
    marginHorizontal: 20,
    alignItems: 'center',
    justifyContent: 'center',
    padding: 3,
  },
  button: {
    borderRadius: 10,
    width: '100%',
    height: '100%',
    alignItems: 'center',
    justifyContent: 'center',
    flexDirection: 'row',
  },
  buttonLabel: {
    color: '#fff',
    fontSize: 16,
  },
});

当用户点击屏幕上的任意按钮时,应用会显示一个警告框。这是因为 在其 onPress 属性上调用了 alert() 方法。让我们将这个组件导入到 app/(tabs)/index.tsx 文件中,并为包含这些按钮的 添加样式:

// app/(tabs)/index.tsx
import { View, StyleSheet } from "react-native";

import Button from '@/components/Button'; 
import ImageViewer from '@/components/ImageViewer';

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

export default function Index() {
  return (
    <View style={styles.container}>
      <View style={styles.imageContainer}>
        <ImageViewer imgSource={PlaceholderImage} />
      </View>
      <View style={styles.footerContainer}>
        <Button label="Choose a photo" />
        <Button label="Use this photo" />
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#25292e',
    alignItems: 'center',
  },
  imageContainer: {
    flex: 1,
    paddingTop: 28,
  },
  footerContainer: {
    flex: 1 / 3,
    alignItems: 'center',
  },
});

带有标签“Use this photo”(使用此照片)的第二个按钮与设计中的实际按钮非常相似。然而,第一个按钮需要更多样式来匹配设计。

多了2个按钮,下了一个iOS模拟器

image.png

增强可复用的按钮组件

"选择一张照片"按钮的样式与"使用这张照片"按钮不同,因此我们将添加一个新的按钮theme属性,以便我们可以应用一个新主题(primary theme).此外,该按钮在标签前面还有一个图标,我们将使用@expo/vector-icons中的图标。

为了在按钮上加载并显示图标,我们将使用库中的FontAwesome.修改components/Button.tsx文件,添加以下代码片段:

import { StyleSheet, View, Pressable, Text } from 'react-native'
import FontAwesome from '@expo/vector-icons/FontAwesome'

type Props = {
  label: string
  theme?: 'primary'
}

export default function Button({ label, theme }: Props) {
  if (theme === 'primary') {
    return (
      <View
        style={[
          styles.buttonContainer,
          { borderWidth: 4, borderColor: '#ffd33d', borderRadius: 18 },
        ]}
      >
        <Pressable
          style={[styles.button, { backgroundColor: '#fff' }]}
          onPress={() => alert('You pressed a button.')}
        >
          <FontAwesome
            name="picture-o"
            size={18}
            color="#25292e"
            style={styles.buttonIcon}
          />
          <Text style={[styles.buttonLabel, { color: '#25292e' }]}>
            {label}
          </Text>
        </Pressable>
      </View>
    )
  }
  return (
    <View style={styles.buttonContainer}>
      <Pressable
        style={styles.button}
        onPress={() => alert('You pressed a button.')}
      >
        <Text style={styles.buttonLabel}>{label}</Text>
      </Pressable>
    </View>
  )
}

const styles = StyleSheet.create({
  buttonContainer: {
    width: 320,
    height: 68,
    marginHorizontal: 20,
    alignItems: 'center',
    justifyContent: 'center',
    padding: 3,
  },
  button: {
    borderRadius: 10,
    width: '100%',
    height: '100%',
    alignItems: 'center',
    justifyContent: 'center',
    flexDirection: 'row',
  },
  buttonIcon: {
    paddingRight: 8,
  },
  buttonLabel: {
    color: '#fff',
    fontSize: 16,
  },
})

更新主页

// app/(tabs)/index.tsx
import { View, StyleSheet } from 'react-native'
import Button from '@/components/Button'
import ImageViewer from '@/components/ImageViewer'

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

export default function Index() {
  return (
    <View style={styles.container}>
      <View style={styles.imageContainer}>
        <ImageViewer imgSource={PlaceholderImage} />
      </View>
      <View style={styles.footerContainer}>
        <Button theme="primary" label="Choose a photo" />
        <Button label="Use the photo" />
      </View>
    </View>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#25292e',
    alignItems: 'center',
  },
  imageContainer: {
    flex: 1,
  },
  footerContainer: {
    flex: 1 / 3,
    alignItems: 'center',
  },
})

image.png

第四章 使用图片选择器

React Native 提供了一些内置组件作为标准构建模块,例如 、 和 。我们正在开发一个功能,让用户可以从设备的媒体库中选择图片。由于核心组件无法实现这个功能,我们需要一个库来在应用中添加这个功能。

我们将使用 expo-image-picker,这是 Expo SDK 提供的一个库。

安装expo-image-picker
npx expo install expo-image-picker

提示:每次在项目中安装新库时,请先通过在终端中按下 Ctrl + C 停止开发服务器,然后运行安装命令。安装完成后,可以通过在同一终端窗口中运行 npx expo start 来重新启动开发服务器。

从设备的媒体库中选择图片

expo-image-picker 提供了 launchImageLibraryAsync() 方法,用于显示系统界面,选择设备媒体库中的图片或视频。我们将使用前一章中创建的主主题按钮来从设备的媒体库中选择图片,并创建一个函数来启动设备的图片库以实现此功能。

在 app/(tabs)/index.tsx 文件中,导入 expo-image-picker 库,并在 Index 组件中创建一个 pickImageAsync() 函数:

// ...其他导入代码保持不变
 import * as ImagePicker from 'expo-image-picker';

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

    if (!result.canceled) {
      console.log(result);
    } else {
      alert('You did not select any image.');
    }
  };

  // ...其他代码保持不变
}

让我们来了解上述代码的作用:

  • launchImageLibraryAsync() 方法接收一个对象,用于指定不同的选项。这个对象是 ImagePickerOptions 对象,我们在调用该方法时传递了这个对象。
  • 当 allowsEditing 设置为 true 时,用户在选择图片的过程中可以在 Android 和 iOS 上裁剪图片。
更新按钮组件

在按下主按钮时,我们将调用 pickImageAsync() 函数。请更新 components/Button.tsx 文件中的 onPress 属性:

... // import 保持不变
type Props = {
  ...
  onPress?: () => void;
};
export default function Button({ label, theme, onPress }: Props) {
  if (theme === 'primary') {
    return (
       ... // onPress事件调用了onPress方法
        <Pressable
          style={[styles.button, { backgroundColor: '#fff' }]}
          onPress={onPress}
        >
        ...
        </Pressable>
    )
  }

在 app/(tabs)/index.tsx 文件中,将 pickImageAsync() 函数添加到第一个 的 onPress 属性中。

// app/(tabs)/index.tsx
import { View, StyleSheet } from 'react-native';
import * as ImagePicker from 'expo-image-picker';

import Button from '@/components/Button';
import ImageViewer from '@/components/ImageViewer';

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

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

    if (!result.canceled) {
      console.log(result);
    } else {
      alert('You did not select any image.');
    }
  };

  return (
    <View style={styles.container}>
      <View style={styles.imageContainer}>
        <ImageViewer imgSource={PlaceholderImage} />
      </View>
      <View style={styles.footerContainer}>
        <Button theme="primary" label="Choose a photo" onPress={pickImageAsync} />
        <Button label="Use this photo" />
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#25292e',
    alignItems: 'center',
  },
  imageContainer: {
    flex: 1,
  },
  footerContainer: {
    flex: 1 / 3,
    alignItems: 'center',
  },
});

pickImageAsync() 函数调用了 ImagePicker.launchImageLibraryAsync() 并处理返回的结果。launchImageLibraryAsync() 方法会返回一个对象,该对象包含有关所选图像的信息。

以下是结果对象及其包含的属性的示例:

image.png

使用选定的图片

结果对象提供了一个 assets 数组,其中包含所选图片的 uri。我们将从图片选择器中获取这个值,并在应用中显示选定的图片。

修改 app/(tabs)/index.tsx 文件:

  • 使用 React 的 useState 钩子声明一个名为 selectedImage 的状态变量。我们将使用此状态变量来存储选定图片的 URI。
  • 更新 pickImageAsync() 函数,将图片的 URI 保存到 selectedImage 状态变量中。
  • 将 selectedImage 作为一个 prop 传递给 ImageViewer 组件。
// 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 pickImageAsync = async () => {
    let result = await ImagePicker.launchImageLibraryAsync({
      mediaTypes: ['images'],
      allowsEditing: true,
      quality: 1,
    });

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

  return (
    <View style={styles.container}>
      <View style={styles.imageContainer}>
        <ImageViewer imgSource={PlaceholderImage} selectedImage={selectedImage} />
      </View>
      <View style={styles.footerContainer}>
        <Button theme="primary" label="Choose a photo" onPress={pickImageAsync} />
        <Button label="Use this photo" />
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#25292e',
    alignItems: 'center',
  },
  imageContainer: {
    flex: 1,
  },
  footerContainer: {
    flex: 1 / 3,
    alignItems: 'center',
  },
});

将selectedImage属性传递给ImageViewer组件,以显示选定的图片,而不是占位图片。

  • 修改components/ImageViewer.tsx文件以接受selectedImage属性
  • 由于图片的来源变得较长,我们可以将其移到一个单独的变量中,命名为imageSource.
  • 将imageSource作为Image组件source属性的值进行传递

在上面的代码片段中,Image 组件使用了条件运算符来加载图片的来源。选中的图片是一个 uri 字符串,而不是像占位图片那样的本地资源。
现在让我们看看我们的应用:

原本想用GIF展示的,GIF一直无法保存,只能截图代替 image.png