构建一个屏幕

在本教程中,了解如何使用 React Native 的 Pressable 和 Expo Image 等组件来构建屏幕。


在本章中,我们将创建 StickerSmash 应用的第一个屏幕。

¥In this chapter, we'll create the first screen of the StickerSmash app.

上面的屏幕显示一个图片和两个按钮。应用用户可以使用两个按钮之一选择图片。第一个按钮允许用户从他们的设备中选择图片。第二个按钮允许用户继续使用应用提供的默认图片。

¥The screen above displays an image and two buttons. The app user can select an image using one of the two buttons. The first button allows the user to select an image from their device. The second button allows the user to continue with a default image provided by the app.

一旦用户选择图片,他们就可以向其添加贴纸。那么,让我们开始创建这个屏幕。

¥Once the user selects an image, they can add a sticker to it. So, let's start creating this screen.

Watch: Building a screen in your universal Expo app
Watch: Building a screen in your universal Expo app

1

打破屏幕

¥Break down the screen

在通过编写代码构建此屏幕之前,让我们将其分解为一些基本元素。

¥Before we build this screen by writing code, let's break it down into some essential elements.

有两个基本元素:

¥There are two essential elements:

  • 屏幕中央显示一个大图片

    ¥There is a large image displayed at the center of the screen

  • 屏幕下半部分有两个按钮

    ¥There are two buttons in the bottom half of the screen

第一个按钮包含多个组件。父元素提供黄色边框,并在行内包含图标和文本组件。

¥The first button contains multiple components. The parent element provides a yellow border, and contains an icon and text components inside a row.

现在我们已经将 UI 分解为更小的块,我们准备开始编码。

¥Now that we've broken down the UI into smaller chunks, we're ready to start coding.

2

显示图片

¥Display the image

我们将使用 expo-image 库在应用中显示图片。它提供了一个跨平台的 <Image> 组件来加载和渲染图片。

¥We'll use expo-image library to display the image in the app. It provides a cross-platform <Image> component to load and render an image.

在终端中按 Ctrl + c 停止开发服务器。然后,安装 expo-image 库:

¥Stop the development server by pressing Ctrl + c in the terminal. Then, install the expo-image library:

Terminal
npx expo install expo-image

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

¥The npx expo install command will install the library and add it to the project's dependencies in package.json.

Image 组件将图片的来源作为其值。源可以是 静态资源 或 URL。例如,assets/images 目录中所需的源是静态的。它也可以作为 uri 属性来自 网络

¥The Image component takes the source of an image as its value. The source can be either a static asset or a URL. For example, the source required from assets/images directory is static. It can also come from Network as a uri property.

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

¥To use the Image component in app/(tabs)/index.tsx file:

  1. expo-image 库导入 Image

    ¥Import Image from the expo-image library.

  2. 创建一个 PlaceholderImage 变量,以将 assets/images/background-image.png 文件用作 Image 组件上的 source 属性。

    ¥Create a PlaceholderImage variable to use assets/images/background-image.png file as the source prop on the Image component.

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

3

将组件分成文件

¥Divide components into files

让我们将代码分成多个文件,因为我们会向此屏幕添加更多组件。在本教程中,我们将使用组件目录来创建自定义组件。

¥Let's divide the code into multiple files as we add more components to this screen. Throughout this tutorial, we'll use the components directory to create custom components.

  1. 创建一个顶层组件目录,并在其中创建 ImageViewer.tsx 文件。

    ¥Create a top-level components directory, and inside it, create the ImageViewer.tsx file.

  2. 移动代码以显示此文件中的图片以及 image 样式。

    ¥Move the code to display the image in this file along with the image styles.

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 是一个自定义组件,我们将其放在单独的目录中,而不是应用目录中。应用目录内的每个文件都是布局文件或路由文件。欲了解更多信息,请参阅 非路由文件

导入 ImageViewer 并在 app/(tabs)/index.tsx 中使用它:

¥Import ImageViewer and use it in the app/(tabs)/index.tsx:

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,
},
});
What is the @ in import statement?

@ 符号是用于导入自定义组件和其他模块的自定义 路径别名,而不是相对路径。Expo CLI 会自动在 tsconfig.json 中对其进行配置。

¥The @ symbol is a custom path alias for importing custom components and other modules instead of relative paths. Expo CLI automatically configures it in tsconfig.json.

4

使用 Pressable 创建按钮

¥Create buttons using Pressable

React Native 包含一些用于处理触摸事件的不同组件,但建议使用 <Pressable> 因为它的灵活性。它可以检测单击、长按、按下和释放按钮时触发单独的事件等等。

¥React Native includes a few different components for handling touch events, but <Pressable> is recommended for its flexibility. It can detect single taps, long presses, trigger separate events when the button is pushed in and released, and more.

在设计中,我们需要创建两个按钮。每个都有不同的风格和标签。让我们从为这些按钮创建一个可重用的组件开始。在 components 目录中创建一个 Button.tsx 文件,代码如下:

¥In the design, there are two buttons we need to create. Each has a different style and label. Let's start by creating a reusable component for these buttons. Create a Button.tsx file inside the components directory with the following code:

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

当用户点击屏幕上的任何按钮时,应用都会显示警报。这是因为 <Pressable> 在其 onPress prop 上调用 alert()。让我们将此组件导入 app/(tabs)/index.tsx 文件,并为封装这些按钮的 <View> 添加样式:

¥The app displays an alert when the user taps any of the buttons on the screen. It happens because <Pressable> calls alert() on its onPress prop. Let's import this component into app/(tabs)/index.tsx file and add styles for the <View> that encapsulates these buttons:

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

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

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

带有标签 "使用这张照片" 的第二个按钮类似于设计中的实际按钮。但是,第一个按钮需要更多样式来匹配设计。

¥The second button with the label "Use this photo" resembles the actual button from the design. However, the first button needs more styling to match the design.

5

增强可重用按钮组件

¥Enhance the reusable button component

"选择一张照片" 按钮需要与 "使用这张照片" 按钮不同的样式,因此我们将添加一个新的按钮主题属性,该属性将允许我们应用 primary 主题。该按钮在标签之前还有一个图标。我们将使用 @expo/vector-icons 库中的图标。

¥The "Choose a photo" button requires different styling than the "Use this photo" button, so we will add a new button theme prop that will allow us to apply a primary theme. This button also has an icon before the label. We will use an icon from the @expo/vector-icons library.

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

¥To load and display the icon on the button, let's use FontAwesome from the library. Modify components/Button.tsx to add the following code snippet:

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

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

¥Let's learn what the above code does:

  • 主主题按钮使用内联样式,它使用直接在 style prop 中传递的对象覆盖 StyleSheet.create() 中定义的样式。

    ¥The primary theme button uses inline styles, which overrides the styles defined in StyleSheet.create() with an object directly passed in the style prop.

  • 主主题中的 <Pressable> 组件使用值为 #fffbackgroundColor 属性将按钮的背景设置为白色。如果我们将此属性添加到 styles.button,则将为主要主题和无样式主题设置背景颜色值。

    ¥The <Pressable> component in the primary theme uses a backgroundColor property with a value #fff to set the button's background to white. If we add this property to the styles.button, the background color value will be set for both the primary theme and the unstyled one.

  • 内联样式使用 JavaScript 并覆盖特定值的默认样式。

    ¥Inline styles use JavaScript and override the default styles for a specific value.

现在,修改 app/(tabs)/index.tsx 文件以在第一个按钮上使用 theme="primary" 属性。

¥Now, modify the app/(tabs)/index.tsx file to use the theme="primary" prop on the first button.

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 this photo" />
    </View>
  </View>
);
}

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

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

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

概括

¥Summary

Chapter 3: Build a screen

We've successfully implemented the initial design to start building our app's first screen.

In the next chapter, we'll add the functionality to pick an image from the device's media library.

Next: Use an image picker