React Native

文章目录


前言

react native learning


一、快速搭建RN开发环境

其中给了我们两个选择:
如果是作为学习阶段,想要快速体验 RN 开发,那么可以直接使用简易沙盒环境。
如果是要做完整的上线应用开发,那么可以搭建完整的原生环境。
沙盒(英语:sandbox,又译为沙箱),计算机术语,在计算机安全领域中是一种安全机制,为运行中的程序提供的隔离环境。通常是作为一些来源不可信、具破坏力或无法判定程序意图的程序提供实验之用。 沙盒通常严格控制其中的程序所能访问的资源,比如,沙盒可以提供用后即回收的磁盘及内存空间。在沙盒中,网络访问、对真实系统的访问、对输入设备的读取通常被禁止或是严格限制。从这个角度来说,沙盒属于虚拟化的一种。 沙盒中的所有改动对操作系统不会造成任何损失。通常,这种技术被计算机技术人员广泛用于测试可能带毒的程序或是其他的恶意代码。
首先第一步,我们需要安装 expo-cli,这是一个脚手架工具,可以帮助我们快速搭建一个 RN 的项目。

npm install -g expo-cli
expo init AwesomeProject
- cd rn_demo
- yarn start 
- yarn android
- yarn ios # requires an iOS device or macOS for access to an iOS simulator
- yarn web

除此之外,我们需要在手机上安装一个 expo-client 应用。
之后在手机上进行安装,安装完毕后确保手机和电脑是连接的同一个网络,打开该应用,点击 Scan QR code 进行扫码,也可以输入url。
如果是苹果手机,直接使用自带的相机应用进行扫码。

二、RN基础知识

2.1.样式与布局:

在 RN 中,所有组件都接受名为 style 的属性,属性值为一个对象,用来书写 CSS 样式。
书写样式时需要注意的是要按照 JavaScript 语法来使用驼峰命名法,例如将 background-color 改为 backgroundColor。
还有就是在 RN 中无法使用缩写样式,例如 border:1px solid 这样的样式是无法使用的,只能分成两条样式来写 borderWidth:1,borderStyle:‘solid’
在 RN 中提供了一个 StyleSheet.create 方法来集中定义组件的样式,如果要复用 StyleSheet.create 中所定义的样式,可以传入一个数组,但是要注意在数组中位置居后的样式对象比居前的优先级更高,这样你可以间接实现样式的继承。

const styles = StyleSheet.create({
  box: {
    width: 45,
    height: 28,
    backgroundColor: '#999',
    borderRadius: 45,
    alignItems: 'flex-end',
    alignItems: 'flex-start',
  }
});

在 RN 中设置样式时,如果涉及到尺寸,默认都是不给单位的,表示的是与设备像素密度无关的逻辑像素点。
在进行移动端开发时,最推荐的布局方案就是使用 flexbox 弹性盒布局。flexbox 可以在不同屏幕尺寸上提供一致的布局结构。
RN 中的 flexbox 的工作原理和 Web 上的 CSS 基本一致,当然也存在少许差异。首先是默认值不同:flexDirection 的默认值是 column 而不是 row,而 flex 也只能指定一个数字值。

2.2.图片:

在 RN 中,提供了一个名为 Image 的组件来显示图片。
<Image source={require(‘./my-icon.png’)} />
require 中的图片名字必须是一个静态字符串,不能使用变量!因为 require 是在编译时期执行,而非运行时期执行!。

// 正确
<Image source={require('./my-icon.png')} />;

// 错误
const icon = this.props.active
  ? 'my-icon-active'
  : 'my-icon-inactive';
<Image source={require('./' + icon + '.png')} />;

// 正确
const icon = this.props.active
  ? require('./my-icon-active.png')
  : require('./my-icon-inactive.png');
<Image source={icon} />;

本地图片在引入时会包含图片的尺寸(宽度,高度)信息,但是如果是网络图片,则必须手动指定图片的尺寸。

// 正确
<Image source={{uri: 'https://facebook.github.io/react/logo-og.png'}}
       style={{width: 400, height: 400}} />

// 错误
<Image source={{uri: 'https://facebook.github.io/react/logo-og.png'}} />

2.3.文本与输入按钮:

2.3.1.TextInput

RN 中提供了一个 TextInput 组件,该组件是一个允许用户输入文本的基础组件。它有一个名为 onChangeText 的属性,此属性接受一个函数,而此函数会在文本变化时被调用。另外还有一个名为 onSubmitEditing 的属性,会在文本被提交后(用户按下软键盘上的提交键)调用。

2.3.2.Button

按钮也是一个应用中最基本的需求,在 RN 中提供了 Button 组件来渲染按钮,这是一个简单的跨平台的按钮组件,会调用原生环境中对应的按钮组件。
在 Android 设备中,Button 组件显示为一个按钮,而在 IOS 设备中,则显示为一行文本。
在这里插入图片描述
该组件需要传递两个必须的属性,一个是 onPress,对应点击后的事件,另一个是 title,用来指定按钮内的文本信息。

<Button title="这是一个测试按钮" onPress={onPressLearnMore}></Button>

由于 Button 组件是调用原生代码,因此不同的平台显示的外观是不同的,如果想要各个平台显示的外观都相同,则可以使用 Touchable 系列组件。
在这里插入图片描述
Touchable 系列组件一共有 4 个,其中跨平台的有 3 个:
想要安卓和IOS的按钮样式相同可以选择跨平台的这3个自定义样式按钮

  • TouchableHighlight
    Touchable 系列组件中比较常用的一个,它是在 TouchableWithoutFeedback 的基础上添加了一些 UI 上的扩展,即当手指按下的时候,该视图的不透明度会降低,同时会看到视图变暗或者变亮,该标签可以添加 style 样式属性。
  • TouchableOpacity(常用)
    完全和 TouchableHighlight 相同,只是不可以修改颜色,只能修改透明度。
  • TouchableWithoutFeedback
    最基本的一个 Touchable 组件,只响应用户的点击事件,不会做任何 UI 上的改变,所以不用添加 style 样式属性,加了也没效果。
    另外在 Android 平台上支持一个叫 TouchableNativeFeedback 的组件:
  • TouchableNativeFeedback:
    为了支持 Android 5.0 的触控反馈而新增的组件。该组件在 TouchableWithoutFeedback 所支持的属性的基础上增加了触摸的水波纹效果。可以通过 background 属性来自定义原生触摸操作反馈的背景。(仅限 Android 平台,IOS 平台使用会报错)
 例子:<TouchableHighlight
        onPress={() => {
          console.log("触摸效果");
        }}
        onLongPress={() => {
          console.log("长按效果");
        }}
        disabled={false} // 默认是 false,如果是 true 表示关闭该组件的触摸功能
        onPressIn={() => {
          console.log("触摸开始");
        }}
        onPressOut={() => {
          console.log("触摸结束");
        }}
      >
        <View
          style={{
            width: 260,
            height: 50,
            alignItems: "center",
            backgroundColor: "#2196F3",
          }}
        >
          <Text
            style={{
              lineHeight: 50,
              color: "white",
            }}
          >
            Touch Here
          </Text>
        </View>
      </TouchableHighlight>

2.4.使用滚动视图:

到目前为止,我们的应用能够显示文字、图片,也能够和用户进行简单的互动。但是还有一个很重要的需求,那就是滑屏操作。
之前在 WebApp 课程里面,我们的滑屏操作是需要禁用默认事件后自己来写的,当然我们也可以选择使用第三方库,例如 Swiper.js 来实现滑屏效果。
而在 RN 中,则直接为我们提供了滚动视图的组件 ScrollView。
ScrollView 是一个通用的可滚动的容器,你可以在其中放入多个组件和视图,而且这些组件并不需要是同类型的。ScrollView 不仅可以垂直滚动,还能水平滚动(通过 horizontal 属性来设置)。

2.5.使用长列表:

2.5.1.FlatList

FlatList 组件用于显示一个垂直的滚动列表,其中的元素之间结构近似而仅数据不同。
FlatList 更适于长列表数据,且元素个数可以增删。和 ScrollView 不同的是,FlatList 并不立即渲染所有元素,而是优先渲染屏幕上可见的元素。
FlatList 组件必须的两个属性是 data 和 renderItem。data 是列表的数据源,而 renderItem 则从数据源中逐个解析数据,然后返回一个设定好格式的组件来渲染。
下面的例子创建了一个简单的 FlatList,并预设了一些模拟数据。首先是初始化 FlatList 所需的 data,其中的每一项(行)数据之后都在 renderItem 中被渲染成了 Text 组件,最后构成整个 FlatList。

 <FlatList
          data={data}
          keyExtractor={(index: any,item: any) => index}
          renderItem={({item, index}: any) => {
            return <Text>{item.xxx}</Text>
          }}
        />

此函数用于为给定的 item 生成一个不重复的 key。Key 的作用是使 React 能够区分同类元素的不同个体,以便在刷新时能够确定其变化的位置,减少重新渲染的开销。若不指定此函数,则默认抽取item.key作为 key 值。若item.key也不存在,则使用数组下标。

2.5.2.SectionList

如果要渲染的是一组需要分组的数据,也许还带有分组标签的,那么 SectionList 将是个不错的选择。

<View style={styles.container}>
      <SectionList
        sections={[
          { title: "D", data: ["Devin", "Dan", "Dominic"] },
          {
            title: "J",
            data: [
              "Jackson",
              "James",
              "Jillian",
              "Jimmy",
              "Joel",
              "John",
              "Julie",
            ],
          },
        ]}
        renderItem={({ item }) => <Text style={styles.item}>{item}</Text>}
        renderSectionHeader={({ section }) => (
          <Text style={styles.sectionHeader}>{section.title}</Text>
        )}
        keyExtractor={(item, index) => index}
      />
</View>

在上面的示例中,我们使用到了 SectionList 组件的 4 个属性,分别是
sections(必填):用来渲染的数据,类似于 FlatList 中的 data 属性。
renderItem(必填):用来渲染每一个 section 中的每一个列表项的默认渲染器。必须返回一个 react 组件。
renderSectionHeader:在每个 section 的头部渲染。在 IOS 上,这些 headers 是默认粘接在 ScrollView 的顶部的。
keyExtractor:此函数用于为给定的 item 生成一个不重复的 key。
Key 的作用是使 react 能够区分同类元素的不同个体,以便在刷新时能够确定其变化的位置,减少重新渲染的开销。
若不指定此函数,则默认抽取 item.key 作为 key 值。若 item.key 也不存在,则使用数组下标。注意这只设置了每行(item)的 key,对于每个组(section)仍然需要另外设置 key。

2.6.网络连接:

开发应用时,我们往往还需要从服务器上面获取数据。
在 RN 中,支持 fetchAPI 以及传统的 Ajax 的形式来发送网络请求,但是这里推荐使用最新的 fetch 形式来发送请求。
下面我们来看一个在 RN 中使用 fetch 发送请求的示例:
注:默认情况下 iOS 会阻止所有 http 的请求,以督促开发者使用 https。从 Android9 开始,也会默认阻止 http 请求。

  function getInfo() {
    return fetch("https://cnodejs.org/api/v1/topics")
      .then((res) => res.json())
      .then((res) => {
        console.log(res, "res");
      })
      .catch((error) => {
        console.error(error);
      });
  }
  return (
    <View style={styles.container}>
      <Button onPress={getInfo} title="点击按钮获取数据" color="skyblue" />
    </View>
  );

2.7.实战案例:

经过前面小节的学习,我们已经掌握了 RN 中最基本的知识,也算是快速入门了。接下来,为了增加一点小小的成就感,我们来做一个简单的实战案例——照片分享应用。

import React, { useState } from "react";
import {
  Image,
  StyleSheet,
  Text,
  View,
  TouchableOpacity,
  Platform,
} from "react-native";
import logo from "./assets/logo.png";
import * as ImagePicker from "expo-image-picker";
import * as Sharing from "expo-sharing";

export default function App() {
  const [localUri, setLocalUri] = useState("");
  // 异步获取图片
  let openImagePickerAsync = async () => {
    // 首先获取权限
    let permissionResult =
      await ImagePicker.requestMediaLibraryPermissionsAsync();

    // 如果权限获取失败
    if (permissionResult.granted === false) {
      alert("需要访问相机胶卷的权限!");
      return;
    }

    // 没有进入上面的 if,说明权限获取成功
    // 异步打开图片选择器,并且返回用户的图片选择结果
    let pickerResult = await ImagePicker.launchImageLibraryAsync();

    // 如果用户没有选择图片
    if (pickerResult.cancelled === true) {
      // 进入此 if,说明用户没有选择图片
      return;
    }

    // 没有进入上面的 if,说明用户选择了图片
    setLocalUri(pickerResult.uri);
  };

  // 分享图片
  let openShareDialogAsync = async () => {
    if (Platform.OS === "web") {
      alert(`Uh oh, sharing isn't available on your platform`);
      return;
    }

    await Sharing.shareAsync(localUri);
  };

  // 返回首页
  let goBack = () => {
    setLocalUri("");
  };

  // 如果 selectedImage 里面有内容,就显示图片
  if (localUri) {
    return (
      <View style={styles.container}>
        <Image source={{ uri: localUri }} style={styles.thumbnail} />
        
        <TouchableOpacity onPress={openShareDialogAsync} style={styles.button}>
          <Text style={styles.buttonText}>分享照片</Text>
        </TouchableOpacity>
        
        <TouchableOpacity onPress={goBack} style={styles.button}>
          <Text style={styles.buttonText}>重新选择</Text>
        </TouchableOpacity>
      </View>
    );
  }
  return (
    <View style={styles.container}>
      <Image source={logo} style={styles.logo} />

      <Text style={styles.instructions}>
        按下按钮,与朋友分享手机中的照片!
      </Text>

      <TouchableOpacity onPress={openImagePickerAsync} style={styles.button}>
        <Text style={styles.buttonText}>选择照片</Text>
      </TouchableOpacity>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "center",
  },
  logo: {
    width: 305,
    height: 159,
    marginBottom: 10,
  },
  instructions: {
    color: "#888",
    fontSize: 18,
    marginHorizontal: 15,
    textAlign: "center",
    marginBottom: 10,
  },
  button: {
    backgroundColor: "blue",
    paddingLeft: 20,
    paddingRight: 20,
    paddingTop: 10,
    paddingBottom: 10,
    borderRadius: 10,
    marginTop: 10,
  },
  buttonText: {
    fontSize: 16,
    color: "#fff",
  },
  thumbnail: {
    width: 300,
    height: 300,
    resizeMode: "contain",
  },
});

三、RN—内置基础组件

3.1.Image组件:

Image 是一个图片展示组件,其作用类似于 Andriod 的 ImageView 或者 iOS 的 UIImageView。Image 组件支持多种类型的图片显示,包括网络图片、静态资源、base64 图片格式。
要使用 Image 组件加载图片,只需要设置 source 属性即可,如果加载的是网络图片,还需要添加 uri 标识以及手动指定图像的尺寸。
目前,Image 组件支持的图片格式有 PNG、JPG、JPEG、BMP、GIF、WebP 以及 PSD。不过,在默认情况下 Andriod 是不支持 GIF 和 WebP 格式图片的,如果需要添加这两种图片格式的支持,需要在 android/app/build.gradle 文件中添加以下的依赖:

dependencies {
  // 如果你需要支持Android4.0(API level 14)之前的版本
  implementation 'com.facebook.fresco:animated-base-support:1.3.0'

  // 如果你需要支持GIF动图
  implementation 'com.facebook.fresco:animated-gif:2.5.0'

  // 如果你需要支持WebP格式,包括WebP动图
  implementation 'com.facebook.fresco:animated-webp:2.5.0'
  implementation 'com.facebook.fresco:webpsupport:2.5.0'

  // 如果只需要支持WebP格式而不需要动图
  implementation 'com.facebook.fresco:webpsupport:2.5.0'
}

API 文档地址:https://reactnative.cn/docs/image#resizemethod-android
使用 Image 组件时,有一个常用的属性 resizeMode,此属性用于控制当组件和图片尺寸不成比例时以何种方式调整图片的大小,对应的值有 5 种:

  • cover:在保持图片宽高比的前提下缩放图片,直到宽度和高度都大于等于容器视图的尺寸。
  • contain:在保持图片宽高比的前提下缩放图片,直到宽度和高度都小于等于容器视图的尺寸。
  • stretch:拉伸图片且不维持图片的宽高比,直到宽度和高度都刚好填满容器。
  • repeat:在维持原始尺寸的前提下,重复平铺图片直到填满容器。
  • center:居中且不拉伸的显示图片。
<Image style={[styles.image,{resizeMode:'cover'}]} source={imageSource}/>

3.2.TextInput 组件:

TextInput 是一个输入框组件,用于将文本内容输入到 TextInput 组件上。作为一个高频使用的组件,TextInput 支持自动拼写、自动大小写切换、占位默认字符设置以及多种键盘设置功能。
需要注意的是,TextInput 在 Andriod 中默认有一个底边框且存在内边距。如果想让它看起来和 iOS 上的效果尽量一致,则需要将 padding 的值设置为 0。
API 文档地址:https://reactnative.cn/docs/textinput
Props:
针对用户需输入的类型提供便捷,无需手动切换(更多类型查询官网文档):
keyboardType:决定弹出何种软键盘类型,譬如numeric(纯数字键盘)。

3.3. Button组件:

Button 是一个最基本的按钮组件,可以在跨平台上很好地呈现,支持最低级别的定制。
API 文档地址:https://reactnative.cn/docs/button

3.4.Switch组件:

Switch 是 RN 提供的一个状态切换的组件,俗称开关组件,主要用来对开和关两个状态进行切换。
Switch 组件的用法比较简单,只需要给组件绑定 value 属性即可,这样它就是一个受控组件。如果需要改变组件的状态,则必须使用 onValueChange 方法来更新 value 的值。
API 文档地址:https://reactnative.cn/docs/switch

const [isEnabled, setIsEnabled] = useState(false);
const toggleSwitch = () => setIsEnabled(previousState => !previousState);
 <Switch
        trackColor={{ false: "#767577", true: "#81b0ff" }}
        thumbColor={isEnabled ? "#f5dd4b" : "#f4f3f4"}
        ios_backgroundColor="#3e3e3e"
        onValueChange={toggleSwitch}
        value={isEnabled}
      />

四、RN—容器组件

本小节我们来学习 RN 内置组件中的容器组件。容器组件大致如下:

  • View 组件
  • Text 组件
  • ScrollView 组件
  • Touchable 组件

4.1.View组件:

在 RN 中,View 容器组件支持 Flexbox 布局、样式、触摸事件处理和一些无障碍功能,它可以被放到其他容器组件里面,也可以包含任意多个子组件。
无论是 iOS 还是 Andriod,View 组件都会直接对应平台的原生视图,其作用等同于 iOS 的 UIView 或者 Andriod 的 ViewGroup。
API 文档地址:https://reactnative.cn/docs/view

4.2.Text组件:

在 RN 中,Text 是一个用来显示文本内容的组件,也是使用频率极高的组件,它支持文本和样式的嵌套以及触摸事件的处理。
从布局上讲,Text 组件没有类似于 CSS 行内元素这样的概念,所以单个 Text 组件也是独占一行(因为它相当于网页中的 p 元素),但它属于 Flex 布局范畴,可以使用 flexDirection 属性设置行内并列的效果。
Text 的嵌套主要是为了满足文本某些特定场景的需求。例如在一些信息展示类的场景中,通常需要将同一段落的部分文字的字号,颜色另外设置值,以达到视觉上的区分。
以前在 PC 端书写网页时,我们是通过嵌套 span 标签来处理此需求的,而在 RN 中则是使用 Text 的嵌套来实现。

  • Text组件如果外面套一个Text组件,那么会在一行显示,直接无需flex设一行显示
 <Text>
        <Text style={{fontSize:28,color:'#999'}}>First part</Text>
        <Text>and</Text>
 </Text>
  • 不过 RN 中的 Text 嵌套写法也存在以下的问题
    (1) 被嵌套组件与位置相关的 style 样式几乎都不生效。
 <View style={{ marginTop: 20 }}>
      <Text style={{ fontSize: 28 }}>
          我是一段普通文字
          <Text style={{ paddingLeft: 10, borderWidth: 1 }}>左Padding10</Text>
          <Text style={{ marginLeft: 10, borderWidth: 1 }}>左Margin10</Text>
      </Text>
</View>

(2) 内嵌 Text 的 numberOfLines 属性会失效。

<View style={{ marginTop: 20 }}>
      <Text style={{ fontSize: 28, borderWidth: 1 }}>
        1.{" "}
        <Text numberOfLines={2} ellipsizeMode={"tail"}>
          我是一段普通文字我是一段普通文字我是一段普通文字我是一段普通文字我是一段普通文字
        </Text>
      </Text>
</View>

如果使用不同的 Text 组件设置不同的字号,那么对齐的方式仍然是使用 Flex 布局对齐。
在这里插入图片描述
不过需要注意的是,由于字号大小不一,小字号文字的上边距会略小,例如将上例中 alignItems 值修改为 flex-start,但是由于不同的字体大小可以明显的看到上边距是不同的。如果想要不同字体大小的文字边距相同,可以利用 padding 进行微调。

常用api:
onLongPress:当文本被长按以后调用此回调函数。
onPress:当文本被点击以后调用此回调函数。
numberOfLines:用来当文本过长的时候裁剪文本。包括折叠产生的换行在内,总的行数不会超过这个属性的限制。此属性一般和ellipsizeMode搭配使用。
ellipsizeMode:当 Text 组件无法全部显示需要显示的字符串时如何用省略号进行修饰。
head - 从文本内容头部截取显示省略号。例如: “…efg”
middle - 在文本内容中间截取显示省略号。例如: “ab…yz”
tail - 从文本内容尾部截取显示省略号。例如: “abcd…”
clip - 不显示省略号,直接从尾部截断。

API 文档地址:https://reactnative.cn/docs/text

4.3. ScrollView组件:

ScrollView 是一个支持横向或竖向的滚动组件,几乎所有页面都会用到。
ScrollView 组件类似于 Web 中的 html 或 body 标签,浏览器中的页面之所以能上下滚动,就是因为 html 或 body 标签默认有一个 overflow-y: scroll 的属性,如果你把标签的属性设置为 overflow-y: hidden,页面就不能滚动了。
ReactNative 的 ScrollView 组件在 Android 的底层实现用的是 ScrollView 和 HorizontalScrollView,在 iOS 的底层实现用的是 UIScrollView。
使用 ScrollView 组件时,必须要有一个确定的高度才能正常工作。如果不知道容器的准确高度,可以将 ScrollView 组件的样式设置为 {flex: 1},让其自动填充父容器的空余空间。
ScrollView 通常包裹在视图的外面,用于控制视图的滚动,并且很多时候我们并不直接给 ScrollView 设置固定高度或宽度,而是给其父组件设置固定高度或宽度。
后期我们会使用 ScrollView 组件来封装一个轮播图的自定义组件。
API 文档地址:https://reactnative.cn/docs/scrollview

4.4.Touchable组件:

在 RN 应用开发中,点击和触摸都是比较常见的交互行为,不过并不是所有的组件都支持点击事件。为了给这些不具备点击响应的组件绑定点击事件,RN 提供了 Touchable 系列组件。
正如前面所述,Touchable 系列组件并不是单指某一个组件,一共有 4 个,其中跨平台的有 3 个:

  • TouchableHighlight

Touchable 系列组件中比较常用的一个,它是在 TouchableWithoutFeedback 的基础上添加了一些 UI 上的扩展,即当手指按下的时候,该视图的不透明度会降低,同时会看到视图变暗或者变亮,该标签可以添加 style 样式属性。

  • TouchableOpacity(常用)

完全和 TouchableHighlight 相同,只是不可以修改颜色,只能修改透明度。
TouchableWithoutFeedback
最基本的一个 Touchable 组件,只响应用户的点击事件,不会做任何 UI 上的改变,所以不用添加 style 样式属性,加了也没效果。

另外在 Android 平台上支持一个叫 TouchableNativeFeedback 的组件:
TouchableNativeFeedback:为了支持 Android 5.0 的触控反馈而新增的组件。该组件在 TouchableWithoutFeedback 所支持的属性的基础上增加了触摸的水波纹效果。可以通过 background 属性来自定义原生触摸操作反馈的背景。(仅限 Android 平台,IOS 平台使用会报错)

五、RN—Pressable组件

5.1.Pressable组件:

通过前面的学习,我们已经知道在 RN 中提供了 Button 和 Touchable 这两个交互组件来处理用户的点击操作。但是到了 RN 0.63 版本,官方又提供了新的交互组件:Pressable。
新的交互组件在未来将替代目前可以进行交互的组件:Button, TouchableWithoutFeedback,TouchableHighlight,TouchableOpacity,TouchableNativeFeedback。
新核心组件 Pressable,可用于检测各种类型的交互。提供的 API 可以直接访问当前的交互状态,而不必在父组件中手动维护状态。它还可以使用各平台的所有功能,包括悬停,模糊,聚焦等。RN 希望开发者利用 Pressable 去设计组件,而不是使用带有默认效果的组件。如:TouchableOpacity。
那么在这里,我们就要对这几代不同的交互组件做一个总结。
首先,开发者在开发时会用到点按组件,那么它的功能越简单开发者用起来就越轻松;但是与其相对的,应用最后开发出来是给用户使用的,对于用户来讲,则是希望功能越丰富就越能满足各种场景的需求。
那是让开发者简单易用好,还是用丰富的功能去满足用户,有没有两全其美之计?
实际上,RN 的点按组件经历了三个版本的迭代,才找到了两全其美的答案。等你了解了这个三个版本的迭代思路后,你就能很好明白优秀通用组件应该如何设计,才能同时在用户体验 UX 和开发者体验 DX 上找到平衡。

<Pressable onPress={onPressFunction}>
  <Text>I'm pressable!</Text>
</Pressable>

在被 Pressable 包装的元素上:

 1. onPressIn 在按压时被调用。
 2. onPressOut 在按压动作结束后被调用。

在按下 onPressIn 后,将会出现如下两种情况的一种:

 1. 用户移开手指,依次触发 onPressOut 和 onPress 事件。
 2. 按压持续 500 毫秒以上,触发 onLongPress 事件。(onPressOut 在移开手后依旧会触发。)

 <Pressable
        onPress={onPressHandle}
        onPressIn={onPressInHandle}
        onPressOut={onPressOutHandle}
        onLongPress={onLongPressHandle}
      >
        <Text style={{textAlign: 'center'}}>Press Me</Text>
      </Pressable>
      
关于点按时的样式,也是可以自定义的。来看下面的示例:
<Pressable
        style={({ pressed }) => {
          if (pressed) {
            return styles.pressdStyle;
          } else {
            return styles.unPressdStyle;
          }
        }}
      >
 {({ pressed }) => {
          // 根据是否点按返回不同的子组件
          if (pressed) {
            return (
              <Text
                style={{ textAlign: "center", color: "white", lineHeight: 100 }}
              >
                Pressd
              </Text>
            );
          } else {
            return (
              <Text style={{ textAlign: "center", color: "white" }}>
                Press Me
              </Text>
            );
          }
        }}
</Pressable>

Pressable 组件有一个可触发区域 HitRect,默认情况下,可触发区域 HitRect 就是盒模型中的不透明的
可见区域。你可以通过修改 hitSlop 的值,直接扩大可触发区域。
<Pressable
    hitSlop={{ top: 20, bottom: 20, left: 20, right: 20 }}
>
    ...
</Pressable>
另外,在 Pressable 组件中还有一个可保留区域 PressRect 的概念。
点按事件可保留区域的偏移量(Press Retention Offset)默认是 0,也就是说默认情况下可见区域就是可保
留区域。你可以通过设置 pressRetentionOffset 属性,来扩大可保留区域 PressRect。

举一个例子,当你在购物 App 点击购买按钮时,你已经点到购买按钮了,突然犹豫,开始进行心理博弈,想点
又不想点。手指从按钮上挪开了,又挪了进去,然后又挪开了,如此反复。这时还要不要触发点击事件呢?要不
要触发,其实是根据你手指松开的位置来判断的,如果你松手的位置在可保留区域内那就要触发,如果不是那
就不触发。

六、RN—列表组件

6.1.FlatList组件:

在 FlatList 组件出现之前,RN 使用 ListView 组件来实现列表功能,不过在列表数据比较多的情况下,ListView 组件的性能并不是很好,所以在 0.43.0 版本中,RN 引入了 FlatList 组件。相比 ListView 组件,FlatList 组件适用于加载长列表数据,而且性能也更佳。
和 ListView 组件类似,FlatList 组件的使用也非常的简单,只需要给 FlatList 组件提供 datarenderItem 两个属性即可,如下所示:

<FlatList
    data={[{key:"a"},{key:"b"}]}
    renderItem={({item})=><Text>{item.key}</Text>}
>

其中,data 表示数据源,一般为数组格式,renderItem 表示每行的列表项。
除此之外,FlatList 组件还有如下一些使用频率比较高的属性和方法:

  • item 的 key:使用 FlatList 组件实现列表效果时,系统要求给每一行子组件设置一个 key,key 是列表项的唯一标识,目的是当某个子视图的数据发生改变时可以快速地重绘改变的子组件。一般,我们使用 FlatList 组件提供的 keyExtractor 属性来达到此效果。
    <FlatList ... keyExtractor={(item) => item.id} />
  • 分割线 seperator:FlatList 组件本身的分割线并不是很明显,如果要实现分割线,主要有两种策略:设置 borderBottom 或者 ItemSeperatorComponent 属性。如果只是一条简单的分割线,在 Item 组件里面添加 borderBottom 相关属性即可。
<View style={{borderTopWidth: 0, borderBottomWidth: 1}}>
    ...
</View>

需要注意的是,使用 borderBottom 实现分割线时,列表顶部和底部的组件是不需要绘制的。
当然,更简单的方式是使用 ItemSeparatorComponent 属性,具体使用方式可以参阅官方文档:
https://reactnative.dev/docs/flatlist

6.1.1.下拉刷新:

下拉刷新是一个常见的需求,当用户已经处于列表的最顶端,此时继续往下拉动页面的话,就会有一个数据刷新的操作。
在 FlatList 中,提供了下拉刷新的功能,我们只需要设置 onRefresh 和 refreshing 这两个属性值即可。

  • onRefresh:下拉刷新操作触发时要进行的动作,对应是一个函数
  • refreshing:是否显示下拉刷新的等待图标,对应一个布尔值
    下面来看一个具体的示例。代码片段如下:
 <FlatList
        data={movieList}
        renderItem={renderItem}
        keyExtractor={(item) =>
          item.id + new Date().getTime() + Math.floor(Math.random() * 99999 + 1)
        }
        onRefresh={beginHeaderRefresh}
        refreshing={isHeaderRefreshing}
      />

在上面的代码中,当用户下拉刷新时,触发 onRefresh 所对应的 beginHeaderRefresh 函数,此函数对应的操作如下:

// 下拉刷新
function beginHeaderRefresh() {
    setIsHeaderRefreshing(true);
    // 模拟刷新了两条数据
    const newMovie = randomRefreshMovies();
    const data = [...movieList];
    data.unshift(newMovie[0], newMovie[1]);
    setTimeout(() => {
      setMovieList(data);
      setIsHeaderRefreshing(false)
    }, 1000);
}

首先我们将 isHeaderRefreshing 设置为 true,以便出现下拉等待图标,之后调用 randomRefreshMovies 方法随机获取两条电影数据,之后模拟异步场景在一秒钟后更新 movieList 并且关闭 isHeaderRefreshing。
其中 randomRefreshMovies 是从其他文件导入的,代码如下:

// 随机刷新两部电影
export function randomRefreshMovies() {
  let randomStartIndex = Math.floor(Math.random() * (moviesData.length - 2));
  return moviesData.slice(randomStartIndex, randomStartIndex + 2);
}

至此,一个模拟的下拉刷新效果就做完了,每次下拉都会随机刷新两部电影在最前面。

6.1.2.上拉加载更多:

上拉加载也是列表中一个常见的操作,上拉加载其实质就是以前 PC 端的分页效果。因为数据量过多,所以一般我们不会一次性加载所有的数据,此时就会进行一个分页的显示。而在移动端,分页显示变成了上拉加载的形式,当用户到达列表底部时,自动获取下一页的数据,并且拼接到原有数据的后面。
这里我们会用到两个属性,分别是:

  • onEndReached:上拉加载操作触发时要进行的动作,对应是一个函数
  • onEndReachedThreshold:表示距离底部多远时触发 onEndReached
    下面来看一个具体的示例。代码片段如下:
 <FlatList
        data={movieList}
        renderItem={renderItem}
        keyExtractor={(item) =>
          item.id + new Date().getTime() + Math.floor(Math.random() * 99999 + 1)
        }
        onRefresh={beginHeaderRefresh}
        refreshing={isHeaderRefreshing}
        ListFooterComponent={() =>
          !loading && dataList?.currentPage === dataList?.pageCount ? (
            <View style={styles.allDataTips}>
              <Text style={{color: '#BFBFBF'}}>已加载全部数据</Text>
            </View>
         	) : (
            <Loading loading={loading} />
          	)
        }
        refreshControl={
          <RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
        }
        onEndReached={beginFooterRefresh}
        onEndReachedThreshold={0.1} // 这里取值0.1,可以根据实际情况调整,取值尽量小
      />

beginFooterRefresh 函数对应内容如下:

// 上拉加载
function beginFooterRefresh() {
    setIsFooterLoad(true);
    if (currentPage < totalPage) {
      currentPage++;
      const newMovie = queryMovies(currentPage, 10);
      const data = [...movieList];
      data.push(...newMovie);
      setTimeout(() => {
        setMovieList(data);
        setIsFooterLoad(false);
      }, 1000);
    }
}

在 onEndReached 对应的 beginFooterRefresh 函数中,我们首先设置 isFooterLoad 值为 true,这样就能显示下拉加载的等待画面,对应的代码如下:

function renderFooterLoad() {
    if (isFooterLoad) {
      return (
        <View style={styles.footerStyle}>
          <ActivityIndicator animating={true} size="small" />
          <Text style={{ color: "#666", paddingLeft: 10 }}>努力加载中</Text>
        </View>
      );
    }
}

return (
    <SafeAreaView style={styles.flex}>
      {/* 标题区域 */}
      {renderTitle()}
      {/* 加载条 */}
      {renderLoad()}
      {/* 列表区域 */}
      {renderList()}
      {/* 根据 isFooterLoad 的值决定是否渲染下拉加载的等待画面 */}
      {renderFooterLoad()}
    </SafeAreaView>
);

之后仍然是在 setTimeout 中调用 queryMovies 函数来模拟异步请求,拿到数据后拼接到原来的 movieList 后面,并且关闭下拉加载的等待画面。
至此,一个模拟的上拉加载效果就做完了,每次上拉的时候都会加载 10 条新的电影数据在后面。

6.2. SectionList组件:

和 FlatList 一样,SectionList 组件也是由 VirtualizedList 组件扩展来的。不同于 FlatList 组件,SectionList 组件主要用于开发列表分组、吸顶悬浮等功能。
SectionList 组件的使用方法也非常简单,只需要提供 renderItemrenderSectionHeadersections 等必要的属性即可。

<SectionList
    renderItem={({item})=> <ListItem title={item.title}/>}
    renderSectionHeader={({section})=><Header title={section.key}/>}
    sections={[
        {data:[...],title:...},
        {data:[...],title:...},
        {data:[...],title:...},
    ]}
/>

常用的属性如下:

  • keyExtractor:和 FlatList 组件一样,表示项目的唯一标识
  • renderSectionHeader:用来渲染每个 section 的头部视图
  • renderItem:用来渲染每一个 section 中的每一个列表项视图
  • sections:用来渲染视图的数据源,类似于 FlatList 中的 data 属性
  • stickySectionHeadersEnabled:当 section 把它前一个 section 的可视区推离屏幕时,这个 section 的 header 是否粘连在屏幕顶端
    有关 SectionList 组件更多的属性,可以参阅官方文档:https://reactnative.cn/docs/sectionlist

七、RN—功能组件

7.1.ActivityIndicator(Loading):

ActivityIndicator 组件常用于发送请求时所显示的等待圆圈,两个常见的属性 sizecolor 分别用于设置等待圆圈的尺寸和颜色。

<ActivityIndicator size="small" color="#0000ff" />

官方 API 文档地址:https://reactnative.cn/docs/activityindicator

7.2. KeyboardAvoidingComponent:

我们在开发的时候,经常会遇到手机上弹出的键盘常常会挡住当前的视图,所以该组件的功能就是解决这个常见问题的,它可以自动根据手机上键盘的位置,调整自身的 position 或底部的 padding,以避免被遮挡。
常用属性:

  • behavior 该参数的可选值为:height、position、padding,来定义其自适应的方式
  • contentContainerStyle 如果设定 behavior 值为 position,则会生成一个 View 作为内容容器。此属性用于指定此内容容器的样式。
  • keyboardVerticalOffset 视图离屏幕顶部有一定距离时,利用这个属性来补偿修正这段距离(键盘在竖直方向上的偏移量)
<KeyboardAvoidingView style={styles.container} 
behavior="padding" 
keyboardVerticalOffset={-150}  enabled>
  ... 在这里放置需要根据键盘调整位置的组件 ...
</KeyboardAvoidingView>

官方 API 文档地址:https://reactnative.cn/docs/keyboardavoidingview
开发中使用的是RN的APIs的Keyboard,后续应该会提到,官方 API 文档地址:https://reactnative.cn/docs/next/keyboard

7.3.Modal:

Modal 组件用来显示一个弹出框,弹出框常用于用户点击了某一个按钮后弹出一段提示信息。
下面是官方所提供的一个关于 Modal 组件的基本示例:

const [modalVisible, setModalVisible] = useState(false);
 <View style={styles.centeredView}>
      <Modal
        animationType="slide"
        transparent={true}
        visible={modalVisible}
        onRequestClose={() => {
          Alert.alert("Modal has been closed.");
          setModalVisible(!modalVisible);
        }}
      >
        <View style={styles.centeredView}>
          <View style={styles.modalView}>
            <Text style={styles.modalText}>Hello World!</Text>
            <Pressable
              style={[styles.button, styles.buttonClose]}
              onPress={() => setModalVisible(!modalVisible)}
            >
              <Text style={styles.textStyle}>Hide Modal</Text>
            </Pressable>
          </View>
        </View>
      </Modal>
      <Pressable
        style={[styles.button, styles.buttonOpen]}
        onPress={() => setModalVisible(true)}
      >
        <Text style={styles.textStyle}>Show Modal</Text>
      </Pressable>
    </View>
import {StyleSheet, View, Modal, Dimensions} from 'react-native';
export default function EditInquiryView({visible, content}: any) {
  return (
    <Modal animationType="slide" transparent={true} visible={visible}>
      <View style={{flex: 1, backgroundColor: 'rgba(0,0,0,.6)'}}> 
        <View style={styles.box}>
          <View style={styles.contentBox}>{content}</View>
        </View>
      </View>
    </Modal>
  );
}

const styles = StyleSheet.create({
  box: {
    flex: 1,
    justifyContent: 'flex-end',
    alignItems: 'center',
    backgroundColor: 'rgba(0,0,0,.6)',
  },
  contentBox: {
    width: Dimensions.get('window').width / 1,
    backgroundColor: '#fff',
    borderTopLeftRadius: 12,
    borderTopRightRadius: 12,
    alignItems: 'center',
  },
});

官方 API 文档地址:https://reactnative.cn/docs/modal

7.4.RefreshControl:

该组件在 ScrollView 或 ListView 中用于添加拉动刷新功能。当 ScrollView 在 scrollY: 0 时,向下滑动会触发 onRefresh 事件。
下面是官方所提供的一个关于 RefreshControl 组件的基本示例:

 <SafeAreaView style={styles.container}>
      <ScrollView
        contentContainerStyle={styles.scrollView}
        refreshControl={
          <RefreshControl
            refreshing={refreshing}
            onRefresh={onRefresh}
          />
        }
      >
        <Text>Pull down to see RefreshControl indicator</Text>
      </ScrollView>
</SafeAreaView>

官方 API 文档地址:https://reactnative.cn/docs/refreshcontrol

7.5.StatusBar(屏幕顶部状态栏):

StatusBar 是用来控制应用程序状态栏的组件。 状态栏是显示当前时间、Wi-Fi 和蜂窝网络信息、电池电量和/或其他状态图标的区域,通常位于屏幕顶部。
下面是官方所提供的一个关于 StatusBar 组件的基本示例:

const STYLES = ['default', 'dark-content', 'light-content'];
const TRANSITIONS = ['fade', 'slide', 'none'];
const [hidden, setHidden] = useState(false);
const [statusBarStyle, setStatusBarStyle] = useState(STYLES[0]);
const [statusBarTransition, setStatusBarTransition] = useState(TRANSITIONS[0]);
  <StatusBar
        animated={true}
        backgroundColor="#61dafb"
        barStyle={statusBarStyle}
        showHideTransition={statusBarTransition}
        hidden={hidden} />

官方 API 文档地址:https://reactnative.cn/docs/next/statusbar

八、第三方组件库

  • NativeBase 组件库
    NativeBase 是一个广受欢迎的 UI 组件库,为 RN 提供了数十个跨平台组件。在使用 NativeBase 时,你可以使用任意开箱即用的第三方原生库,而这个项目本身也拥有一个丰富的生态系统,从有用的入门套件到可定制的主题模板。
    NativeBase 官网地址:https://nativebase.io/
  • Ant Design Mobile RN 组件库
    Ant Design Mobile RN 是由蚂蚁金服推出的 RN 组件库,如果是 React 的开发者都会对 React 的常用组件库 Ant Design 有所耳闻,而 Ant Design Mobile RN 则是蚂蚁金服在 RN 方向的延伸。
    特点如下:
    UI 样式高度可配置,拓展性更强,轻松适应各类产品风格
    基于 React Native 的 iOS / Android / Web 多平台支持,组件丰富、能全面覆盖各类场景 (antd-mobile)
    提供 “组件按需加载” / “Web 页面高清显示” / “SVG Icon” 等优化方案,一体式开发
    使用 TypeScript 开发,提供类型定义文件,支持类型及属性智能提示,方便业务开发
    全面兼容 react
    Ant Design Mobile RN 官网地址:https://rn.mobile.ant.design/index-cn
  • React Native Elements 组件库
    React Native Elements 是一个高度可定制的跨平台 UI 工具包,完全用 Javascript 构建。该库的作者声称“React Native Elements 的想法更多的是关于组件结构而不是设计,这意味着在使用某些元素时可以减少样板代码,但可以完全控制它们的设计”,这对于开发新手和经验丰富的老手来说都很有吸引力。
    React Native Elements 官网地址:https://reactnativeelements.com/
  • React Native Material 组件库
    React Native Material UI 是一组高度可定制的 UI 组件,实现了谷歌的 Material Design。请注意,这个库使用了一个名为 uiTheme 的 JS 对象,这个对象在上下文间传递,以实现最大化的定制化能力。
    React Native Material 官网地址:https://www.react-native-material.com/
  • Nachos UI 组件库
    Nachos UI 是一个 RN 组件库,提供了 30 多个可定制的组件,这些组件也可以通过 react-native-web 在 Web 上运行。它通过了快照测试,支持格式化和 yarn,提供了热火的设计和全局主题管理器。
    Nachos UI 官网地址:https://avocode.com/nachos-ui
  • React Native Paper 组件库
    React Native Paper 是一个跨平台的 UI 组件库,它遵循 Material Design 指南,提供了全局主题支持和可选的 babel 插件,用以减少捆绑包大小。
    React Native Paper 官网地址:https://callstack.github.io/react-native-paper/

NativeBase 使用示例:
上面罗列了很多 RN 的第三方组件库,但并不是说每一个我们都需要去学习,在开发时选择一个自己用的惯的来使用即可。
这里我们以第一个 NativeBase 为例来演示如何使用第三方组件库。
要使用第三方组件库,首先第一步需要进行安装。
官方提供了安装指南:https://docs.nativebase.io/installation
可以看到,在安装指南中,官方根据开发者不同形式搭建的 RN 项目,提供了对应的安装方式。
由于我们目前的 RN 项目是使用 expo 搭建的,因此选择对应的安装指南。
上面分为了“新项目”和“已有项目”,选择已有项目,然后根据指南输入下面的指令:
npm install native-base
expo install react-native-svg
expo install react-native-safe-area-context

注:安装过程中可能会涉及到科学上网,请自行解决网络问题
当然,你也可以选择基于 NativeBase 组件库创建一个全新的项目,命令为:
expo init my-app --template @native-base/expo-template

九、自定义组件案例

9.1.弹框组件

import {StyleSheet, View, Modal, Dimensions} from 'react-native';

export default function ModalView({visible, content}: any) {
  return (
    <Modal animationType="slide" transparent={true} visible={visible}>
      <View style={{flex: 1, backgroundColor: 'rgba(0,0,0,.6)'}}>
        <View style={styles.box}>
          <View style={styles.contentBox}>{content}</View>
        </View>
      </View>
    </Modal>
  );
}

const styles = StyleSheet.create({
  box: {
    flex: 1,
    justifyContent: 'flex-end',
    alignItems: 'center',
    backgroundColor: 'rgba(0,0,0,.6)',
  },
  contentBox: {
    width: Dimensions.get('window').width / 1,
    backgroundColor: '#fff',
    borderTopLeftRadius: 12,
    borderTopRightRadius: 12,
    alignItems: 'center',
  },
});

9.2.Loading组件

import {
  StyleSheet,
  View,
  Text,
  ActivityIndicator,
  ColorValue,
  ViewStyle,
  StyleProp, Platform
} from "react-native";
import React from 'react';
import Lottie from "lottie-react-native";

type Props = {
  size?: number | 'small' | 'large' | undefined;
  color?: ColorValue | undefined;
  loadingText?: string;
  mask?: boolean;
  style?: StyleProp<ViewStyle>;
  loadingStyle?: StyleProp<ViewStyle>;
};

/**
 *
 * @param size loading大小, default: 'large'
 * @param color loading颜色, default: '#4E83FD'
 * @param loadingText 加载提示文本
 * @param mask 遮罩层
 * @param style 容器自定义样式
 * @param loadingStyle loading自定义样式
 * @constructor
 */
const Loading: React.FC<Props> = ({
  size = 'large',
  color = '#4E83FD',
  loadingText,
  mask,
  style,
  loadingStyle,
}): React.ReactElement => {
  return (
    <View
      style={[
        styles.activityIndicatorView,
        {backgroundColor: mask ? 'rgba(255,255,255,0.6)' : 'rgba(0, 0, 0, 0)'},
        style,
      ]}>
      <View style={[styles.contentBox, loadingStyle]}>
        {Platform.OS === 'ios' ? (
          <Lottie
            source={require('../../static/json/loading.json')}
            loop
            autoPlay
            style={{width: 60, height: 60}}
          />
        ) : (
          <ActivityIndicator size={size} color={color} />
        )}
        {loadingText && <Text style={styles.loadingText}>{loadingText}</Text>}
      </View>
    </View>
  );
};
export default Loading;
const styles = StyleSheet.create({
  activityIndicatorView: {
    width: '100%',
    height: '100%',
    position: 'absolute',
    top: 0,
    left: 0,
    zIndex: 999,
    marginTop: 48,
    justifyContent: 'center',
  },
  contentBox: {
    width: '100%',
    height: '100%',
    marginBottom: 48,
    alignItems: 'center',
    justifyContent: 'center',
  },
  loadingText: {
    marginTop: 5,
    color: '#333',
    fontSize: 16,
  },
});

9.3.单选组件

import {StyleSheet, View} from 'react-native';
import Ionicons from 'react-native-vector-icons/Ionicons';

export default function Radio({isChecked}: any) {
  return (
    <>
      {isChecked ? (
        <Ionicons
          name="checkmark-outline"
          style={[styles.checkedBox]}></Ionicons>
      ) : (
        <View style={[styles.unCheckedBox]}></View>
      )}
    </>
  );
}

const styles = StyleSheet.create({
  checkedBox: {
    width: 12,
    height: 12,
    borderRadius: 45,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#4E83FD',
    color: '#fff',
    textAlign: 'center',
    lineHeight: 12,
    fontSize: 8,
  },
  unCheckedBox: {
    width: 12,
    height: 12,
    borderRadius: 45,
    borderColor: '#ddd',
    borderWidth: 1,
  },
});

9.4.多选组件

9.5.成功失败提示组件

import {StyleSheet, Text, View, Modal, Dimensions, Image} from 'react-native';
import React, {useContext} from 'react';
import {SafeAreaView} from 'react-native-safe-area-context';
export default function TipsModal({
  modalVisible,
  tipsText,
  marginTopVlaue,
  type,
}: any) {
  return (
    <Modal animationType="fade" transparent={true} visible={modalVisible}>
      <SafeAreaView
        style={[
          styles.container,
          {height: '100%'},
        ]}>
        <View style={[styles.box, marginTopVlaue && {marginTop: 45}]}>
          <Image
            source={
              type
                ? require('../../../static/icon/right.png')
                : require('../../../static/icon/leftIcon.png')
            }
            style={styles.icon}
          />
          <Text style={styles.text}>{tipsText}</Text>
        </View>
      </SafeAreaView>
    </Modal>
  );
}

const styles = StyleSheet.create({
  container: {
    alignItems: 'center',
    width: Dimensions.get('window').width,
  },
  box: {
    maxWidth: 360,
    padding: 10,
    marginTop: 15,
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'center',
    backgroundColor: '#fff',
    borderRadius: 6,
    shadowColor: '#000',
    shadowOpacity: 0.25,
    shadowOffset: {width: 2, height: 6},
    elevation: 10,
  },
  icon: {
    width: 22,
    height: 22,
    marginRight: 9,
  },
  text: {
    color: '#000',
    fontSize: 14,
  },
});

9.6.图片预览组件

  • ImageView.tsx——第三方库:react-native-image-viewing
import {
  StyleSheet,
  Text,
  View,
  TouchableOpacity,
  Dimensions,
  ToastAndroid,
  Platform,
} from 'react-native';
import React, {useContext, useState} from 'react';
import Ionicons from 'react-native-vector-icons/Ionicons';
import ImageViewer from 'react-native-image-viewing';

import savePicture from '../../../utils/savePicture';
import {GlobalContext} from '../../../context';

export default function ImageView({item, showModal, index, close}: any) {
  const {statusBarHeight} = useContext(GlobalContext);

  const [savedIndexList, setSavedIndexList]: any = useState([]);
  const [currentIndex, setCurrentIndex] = useState(index);
  const saveImage = () => {
    savePicture(item[currentIndex]?.uri, item[currentIndex]?.imageName)
      .then(() => {
        ToastAndroid.show('保存成功', 1);
        const newList = [...savedIndexList];
        newList.push(currentIndex);
        setSavedIndexList([...newList]);
      })
      .catch(() => {
        ToastAndroid.show('保存失败', 1);
      });
  };
  const renderHeader = () => {
    return (
      <View
        style={[
          styles.headerBox,
          {paddingTop: Platform.OS === 'ios' ? statusBarHeight : 5},
        ]}>
        <View style={{padding: 20}} />
        <Text style={{color: '#fff'}}>
          {currentIndex + 1}/{item.length}
        </Text>
        <TouchableOpacity onPress={() => close(false)} style={{padding: 20}}>
          <Ionicons name="close-outline" color={'#fff'} size={20} />
        </TouchableOpacity>
      </View>
    );
  };
  const renderFooter = () => {
    return (
      <View>
        {!savedIndexList?.includes(currentIndex) && (
          <View style={styles.bottomOptions}>
            <TouchableOpacity style={[styles.btnBox, {}]} onPress={saveImage}>
              <Ionicons name="download-outline" color={'#fff'}></Ionicons>
              <Text style={styles.btnText}>保存</Text>
            </TouchableOpacity>
          </View>
        )}
      </View>
    );
  };
  return (
    <ImageViewer
      images={item}
      imageIndex={index}
      onImageIndexChange={(index: any) => {
        setCurrentIndex(index);
      }}
      onRequestClose={() => {
        close(false);
      }}
      keyExtractor={item.uri}
      visible={showModal}
      animationType={'slide'}
      swipeToCloseEnabled={false}
      FooterComponent={renderFooter}
      HeaderComponent={renderHeader}
    />
  );
}

const styles = StyleSheet.create({
  headerBox: {
    width: '100%',
    alignItems: 'center',
    flexDirection: 'row',
    justifyContent: 'space-between',
    paddingTop: 5,
  },
  bottomOptions: {
    flexDirection: 'row',
    justifyContent: 'flex-end',
    width: Dimensions.get('window').width / 1,
    paddingLeft: 15,
    paddingRight: 15,
    paddingBottom: 20,
    marginBottom: 30,
  },
  btnBox: {
    backgroundColor: 'rgba(255,255,255,.2)',
    flexDirection: 'row',
    justifyContent: 'center',
    alignItems: 'center',
    paddingLeft: 20,
    paddingRight: 20,
    paddingTop: 4,
    paddingBottom: 4,
    borderRadius: 20,
    height: 30,
    overflow: 'hidden',
  },
  btnText: {
    fontSize: 14,
    color: '#fff',
    marginLeft: 5,
  },
});
  • 保存图片 ——savePicture.tsx——第三方库:@react-native-camera-roll/camera-roll
import {PermissionsAndroid, Platform} from 'react-native';
import RNFetchBlob from 'rn-fetch-blob';
import {CameraRoll} from '@react-native-camera-roll/camera-roll';

async function hasAndroidPermission() {
  const getCheckPermissionPromise = async () => {
    if (Number(Platform.Version) >= 33) {
      return Promise.all([
        PermissionsAndroid.check(
          PermissionsAndroid.PERMISSIONS.READ_MEDIA_IMAGES,
        ),
        PermissionsAndroid.check(
          PermissionsAndroid.PERMISSIONS.READ_MEDIA_VIDEO,
        ),
      ]).then(
        ([hasReadMediaImagesPermission, hasReadMediaVideoPermission]) =>
          hasReadMediaImagesPermission && hasReadMediaVideoPermission,
      );
    } else {
      return PermissionsAndroid.check(
        PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE,
      );
    }
  };

  const hasPermission = await getCheckPermissionPromise();
  if (hasPermission) {
    return true;
  }
  const getRequestPermissionPromise = async () => {
    if (Number(Platform.Version) >= 33) {
      return PermissionsAndroid.requestMultiple([
        PermissionsAndroid.PERMISSIONS.READ_MEDIA_IMAGES,
        PermissionsAndroid.PERMISSIONS.READ_MEDIA_VIDEO,
      ]).then(
        statuses =>
          statuses[PermissionsAndroid.PERMISSIONS.READ_MEDIA_IMAGES] ===
            PermissionsAndroid.RESULTS.GRANTED &&
          statuses[PermissionsAndroid.PERMISSIONS.READ_MEDIA_VIDEO] ===
            PermissionsAndroid.RESULTS.GRANTED,
      );
    } else {
      return PermissionsAndroid.request(
        PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE,
      ).then(status => status === PermissionsAndroid.RESULTS.GRANTED);
    }
  };

  return await getRequestPermissionPromise();
}

async function savePicture(tag: string, fileName: string) {
  if (Platform.OS === 'android' && !(await hasAndroidPermission())) {
    return;
  }
  const imageUrl = tag;
  const {dirs} = RNFetchBlob.fs;
  const downloadPath = `${dirs.DocumentDir}/${fileName}`;
  return new Promise((resolve, reject) => {
    RNFetchBlob.config({
      fileCache: true,
      path: downloadPath,
    })
      .fetch('GET', imageUrl)
      .then(res => {
        // CameraRoll.save(res.path())  该方法已弃用
        CameraRoll.saveToCameraRoll(res.path())
          .then(() => {
            resolve(res);
          })
          .catch(err => {
            console.log(err);
          });
      })
      .catch(error => {
        reject(error);
      });
  });
}

export default savePicture;

十、基础API

本小节我们先来看一下一些比较基础的 API,包含以下内容:

  • Alert

  • StyleSheet

  • Transforms

  • Keyboard

  • AppState

10.1.Alert

Alert 主要用于显示一个带有指定标题和消息的警报对话框。Alert.alert 方法接收 3 个参数,一个参数是警报对话框的标题,第二个参数是警报内容,最后一个参数是一个数组,数组的每一项是按钮对象。

import { Alert } from "react-native";
  Alert.alert(
      "Alert Title",
      "My Alert Msg",
      [
        {
          text: "Cancel",
          onPress: () => console.log("Cancel Pressed"),
          style: "cancel"
        },
        { text: "OK", onPress: () => console.log("OK Pressed") }
      ]
    );

10.2.StyleSheet

这个 API 我们在前面已经用过很多次了,StyleSheet 是一种类似于 CSS StyleSheets 的抽象。
需要注意以下几个点:
并不是所有的 CSS 属性在 StyleSheet 中都支持
书写样式时要使用驼峰命名法,例如 backgroundColor

hairlineWidth: 自适应不同设备生成一条线
var styles = StyleSheet.create({
  separator: {
    borderBottomColor: '#bbb',
    borderBottomWidth: StyleSheet.hairlineWidth,
  },
});
adsoluteFill: (position: 'absolute', left: 0, right: 0, top: 0, bottom: 0) 的缩写形式
const styles = StyleSheet.create({
  wrapper: {
    ...StyleSheet.absoluteFill,
    top: 10,
    backgroundColor: 'transparent',
  },
});

10.3.Transforms

Transforms 类似于 CSS 中的变形。可以帮助我们使用 2D 或者 3D 变换来修改组件的外观和位置。
但是需要注意的是,一旦应用了变换,变换后的组件周围的布局将保持不变,因此它可能会与附近的组件重叠。

 <View style={[styles.box, {
        transform: [{ scale: 2 }]
      }]}>
 <View style={[styles.box, {
        transform: [{ scaleX: 2 }]
      }]}>
 <View style={[styles.box, {
        transform: [{ rotate: "45deg" }]
      }]}>
 <View style={[styles.box, {
        transform: [
          { rotateX: "45deg" },
          { rotateZ: "45deg" }
        ]
      }]}>
 <View style={[styles.box, {
        transform: [
          { skewX: "30deg" },
          { skewY: "30deg" }
        ]
      }]}>     
 <View style={[styles.box, {
        transform: [{ translateX: -50 }]
      }]}>    

10.4.Keyboard

Keyboard 模块用来控制键盘相关的事件。
利用 Keyboard 模块,可以监听原生键盘事件以做出相应回应,比如收回键盘。

import React, {useState, useEffect} from 'react';
import {Keyboard, Text, TextInput, StyleSheet, View} from 'react-native';

const Example = () => {
  const [keyboardStatus, setKeyboardStatus] = useState('');

  useEffect(() => {
    const showSubscription = Keyboard.addListener('keyboardDidShow', () => {
      setKeyboardStatus('Keyboard Shown');
    });
    const hideSubscription = Keyboard.addListener('keyboardDidHide', () => {
      setKeyboardStatus('Keyboard Hidden');
    });

    return () => {
      showSubscription.remove();
      hideSubscription.remove();
    };
  }, []);

  return (
    <View style={style.container}>
      <TextInput
        style={style.input}
        placeholder="Click here…"
        onSubmitEditing={Keyboard.dismiss}
      />
      <Text style={style.status}>{keyboardStatus}</Text>
    </View>
  );
};

const style = StyleSheet.create({
  container: {
    flex: 1,
    padding: 36,
  },
  input: {
    padding: 10,
    borderWidth: 0.5,
    borderRadius: 4,
  },
  status: {
    padding: 10,
    textAlign: 'center',
  },
});

export default Example;

Keyboard.dismiss:把弹出的键盘收回去,同时使当前的文本框失去焦点。

十一、屏幕API

这一小节我们来看一下 RN 中和屏幕信息相关的 API,主要包括:

  • Dimensions
  • PixelRatio

11.1.Dimensions

该 API 主要用于获取设备屏幕的宽高,Dimensions 的使用比较简单,只需要使用 get 方法即可获取宽高信息,如下所示:

import { Dimensions } from 'react-native';
const windowWidth = Dimensions.get('window').width;
const windowHeight = Dimensions.get('window').height;
const {scale} = Dimensions.get('window');

11.2.PixelRatio

PixelRatio 可以获取到设备的物理像素和 CSS 像素的比例,也就是 DPR。

如果 CSS 像素和设备像素 1:1 关系,那么 DPR 值就为 1。如果 1 个 CSS 像素对应 2 个设备像素,那么 DPR 值就为 2。

说简单点,就是一个 CSS 像素要用多少个设备像素来显示。如果 DPR 值为 1,表示用一个设备像素就够了,如果 DPR 值为 2,则表示一个 CSS 像素要用 2 个设备像素来表示。

以 iPhone4 为例,设备的物理像素为 640,为 CSS 像素为 320,因此 PixelRatio 值为 2。

在 RN 中,通过 PixelRatio.get( ) 方法即可获取 DPR 值。

import { PixelRatio } from "react-native";
const dpr = PixelRatio.get();

常见的屏幕像素密度表如下:

设备像素密度设备
1iPhone2G/3G/3GS 以及 mdpi Android 设备
1.5hdpi Android 设备
2iPhone4/5s/5/5c/5s/6/7/8 以及 xhdpi Android 设备
3iPhone6Plus/6sPlus/7Plus/X/XS/Max 以及 xxhdpi Android 设备
3.5Nexus6/PixelXL/2XL Android 设备

我们通过前面的学习已经知道,在 RN 中所有尺寸都是没有单位的,例如:width: 100,这是因为 RN 中尺寸只有一个单位 dp,这是一种基于屏幕密度的抽象单位,默认省略。

在 RN 中,我们可以通过 PixelRatio 来将真实像素大小和 dp 单位进行一个转换

  • static getPixelSizeForLayoutSize(layoutSize: number):
    number:获取一个布局元素的真实像素大小,返回值是一个四舍五入的整型
  • static roundToNearestPixel(px: number): number:将真实像素大小转为 RN 的 dp 单位
import { PixelRatio } from 'react-native';
const dp2px = dp => PixelRatio.getPixelSizeForLayoutSize(dp);
const px2dp = px => PixelRatio.roundToNearestPixel(px);

//按照下面的方式可实现px与dp之间的转换(比如100px*200px的View)
<View style={{width:px2dp(100),height:px2dp(200),backgroundColor:"red"}}/>

十二、设备API

设备 API 主要用于获取当前用户的设备相关信息,从而根据不同的设备信息来做出可能不同的操作。主要包括:

  • Platform
  • PlatformColor
  • Appearance

12.1.Platform

Platform 主要用于获取设备的相关信息。下面是官方提供的一个示例:

import React from 'react';
import { Platform, StyleSheet, Text, ScrollView } from 'react-native';

const App = () => {
  return (
    <ScrollView contentContainerStyle={styles.container}>
      <Text>OS</Text>
      <Text style={styles.value}>{Platform.OS}</Text>
      <Text>OS Version</Text>
      <Text style={styles.value}>{Platform.Version}</Text>
      <Text>isTV</Text>
      <Text style={styles.value}>{Platform.isTV.toString()}</Text>
      {Platform.OS === 'ios' && <>
        <Text>isPad</Text>
        <Text style={styles.value}>{Platform.isPad.toString()}</Text>
      </>}
      <Text>Constants</Text>
      <Text style={styles.value}>
        {JSON.stringify(Platform.constants, null, 2)}
      </Text>
    </ScrollView>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  value: {
    fontWeight: '600',
    padding: 4,
    marginBottom: 8
  }
});
export default App;

12.2.PlatformColor

每个平台都有系统定义的颜色,尽管可以通过 AppearanceAPI 或 AccessibilityInfo 检测并设置其中的某些样式,但是这样的操作不仅开发成本高昂,而且还局限。
RN 从 0.63 版本开始提供了一个开箱即用的解决方案来使用这些系统颜色。PlatformColor 是一个新的 API,可以像 RN 中的其它任何颜色一样使用。
例如,在 iOS 上,系统提供一种颜色 labelColor,可以在 RN 中这样使用 PlatformColor:

import { Text, PlatformColor } from 'react-native';
<Text style={{ color: PlatformColor('labelColor') }}>
  This is a label
</Text>;

另一方面,Android 提供像 colorButtonNormal 这样的颜色,可以在 RN 中这样使用 PlatformColor:

import { View, Text, PlatformColor } from 'react-native';
<View
  style={{
    backgroundColor: PlatformColor('?attr/colorButtonNormal')
  }}>
  <Text>This is colored like a button!</Text>
</View>;

同时 DynamicColorIOS 是仅限于 iOS 的 API,可以定义在浅色和深色模式下使用的颜色。与 PlatformColor 相似,可以在任何可以使用颜色的地方使用:

import { Text, DynamicColorIOS } from 'react-native';

const customDynamicTextColor = DynamicColorIOS({
  dark: 'lightskyblue',
  light: 'midnightblue'
});

<Text style={{ color: customDynamicTextColor }}>
  This color changes automatically based on the system theme!
</Text>;

下面是来自官方的一个示例:

import React from 'react';
import {
  Platform,
  PlatformColor,
  StyleSheet,
  Text,
  View
} from 'react-native';

const App = () => (
  <View style={styles.container}>
    <Text style={styles.label}>
      I am a special label color!
    </Text>
  </View>
);

const styles = StyleSheet.create({
  label: {
    padding: 16,
    ...Platform.select({
      ios: {
        color: PlatformColor('label'),
        backgroundColor:
          PlatformColor('systemTealColor'),
      },
      android: {
        color: PlatformColor('?android:attr/textColor'),
        backgroundColor:
          PlatformColor('@android:color/holo_blue_bright'),
      },
      default: { color: 'black' }
    })
  },
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  }
});

export default App;

12.3.Appearance(系统深浅主题模式获取)

Appearance 模块主要用于获取用户当前的外观偏好。目前的手机系统一般都可以选择浅色模式和深色模式,通过 Appearance 模块,开发者就可以获取此信息。

Appearance 模块提供了一个 getColorScheme 的静态方法,该方法可以获取当前用户首选的配色方案,对应的值有 3 个:

light: 浅色主题
dark: 深色主题
null: 没有选择外观偏好
例如:

import React from "react";
import {
  StyleSheet,
  Text,
  View,
  Appearance,
} from "react-native";

const App = () => {
  return (
    <View style={styles.container}>
      <Text>外观偏好</Text>
      <Text style={styles.value}>{Appearance.getColorScheme()}</Text>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
  },
  value: {
    fontWeight: "600",
    padding: 4,
    marginBottom: 8,
  },
});

export default App;

十三、动画API

这一小节我们来看一下 RN 中和动画相关的 API,主要包括:

  • LayoutAnimation
  • Animated

13.1.LayoutAnimation

LayoutAnimation 是 RN 提供的一套全局布局动画 API,只需要配置好动画的相关属性(例如大小、位置、透明度),然后调用组件的状态更新方法引起重绘,这些布局变化就会在下一次渲染时以动画的形式呈现。

在 Andriod 设备上使用 LayoutAnimation,需要通过 UIManager 手动启用,并且需要放在任何动画代码之前,比如可以放在入口文件 App.js 中。

if (Platform.OS === 'android') {
  if (UIManager.setLayoutAnimationEnabledExperimental) {
    UIManager.setLayoutAnimationEnabledExperimental(true);
  }
}

下面我们来看一个示例:

const customAnim = {
  customSpring: {
    duration: 400,
    create: {
      type: LayoutAnimation.Types.spring,
      property: LayoutAnimation.Properties.scaleXY,
      springDamping: 0.6,
    },
    update: {
      type: LayoutAnimation.Types.spring,
      springDamping: 0.6,
    },
  },
  customLinear: {
    duration: 200,
    create: {
      type: LayoutAnimation.Types.linear,
      property: LayoutAnimation.Properties.opacity,
    },
    update: {
      type: LayoutAnimation.Types.easeInEaseOut,
    },
  },
};

在上面的代码中,我们定义了 customAnim 是一个对象,该对象包含了两种动画方式,一种是 customSpring,另一种是 customLinear。

每一种动画都用对象来描述,包含 4 个可选值:

  • duration:动画的时长
  • create:组件创建时的动画
  • update:组件更新时的动画
  • delete:组件销毁时的动画

以 customSpring 为例,对应的 duration 为 400 毫秒,而 create 和 update 包括 delete 对应的又是一个对象,其类型定义如下:

type Anim = {
    duration? : number, // 动画时常
    delay? : number, // 动画延迟
    springDamping? : number, // 弹跳动画阻尼系数
    initialV elocity? : number, // 初始速度
    type? : $Enum<typeof TypesEnum> // 动画类型
    property? : $Enum<typeof PropertiesEnum> // 动画属性
}

其中 type 定义在 LayoutAnimation.Types 中,常见的动画类型有:

  • spring:弹跳动画
  • linear:线性动画
  • easeInEaseOut:缓入缓出动画
  • easeIn:缓入动画
  • easeOut:缓出动画

动画属性 property 定义在 LayoutAnimation.Properties 中,支持的动画属性有:

  • opacity:透明度
  • scaleXY:缩放

因此,上面我们所定义的 customSpring 动画的不同属性值也就非常清晰了。

customSpring: {
    duration: 400,
    create: {
      type: LayoutAnimation.Types.spring,
      property: LayoutAnimation.Properties.scaleXY,
      springDamping: 0.6,
    },
    update: {
      type: LayoutAnimation.Types.spring,
      springDamping: 0.6,
    },
},

下面附上该示例的完整代码:

import React, { useState } from "react";
import {
  View,
  StyleSheet,
  Text,
  LayoutAnimation,
  TouchableOpacity,
  UIManager,
} from "react-native";

if (
  Platform.OS === "android" &&
  UIManager.setLayoutAnimationEnabledExperimental
) {
  UIManager.setLayoutAnimationEnabledExperimental(true);
}

const customAnim = {
  customSpring: {
    duration: 400,
    create: {
      type: LayoutAnimation.Types.spring,
      property: LayoutAnimation.Properties.scaleXY,
      springDamping: 0.6,
    },
    update: {
      type: LayoutAnimation.Types.spring,
      springDamping: 0.6,
    },
  },
  customLinear: {
    duration: 200,
    create: {
      type: LayoutAnimation.Types.linear,
      property: LayoutAnimation.Properties.opacity,
    },
    update: {
      type: LayoutAnimation.Types.easeInEaseOut,
    },
  },
};

const App = () => {
  const [width, setWidth] = useState(200);
  const [height, setHeight] = useState(200);
  const [whichAni,setWhichAni] = useState(true);

  function largePress() {
    whichAni ? 
    LayoutAnimation.configureNext(customAnim.customSpring) :
    LayoutAnimation.configureNext(customAnim.customLinear);
    setWhichAni(!whichAni);
    setWidth(width + 20);
    setHeight(height + 20);
  }

  return (
    <View style={styles.container}>
      <View style={[styles.content, { width, height }]} />
      <TouchableOpacity style={styles.btnContainer} onPress={largePress}>
        <Text style={styles.textStyle}>点击增大</Text>
      </TouchableOpacity>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: "center",
    justifyContent: "center",
    backgroundColor: "#F5FCFF",
  },
  content: {
    backgroundColor: "#FF0000",
    marginBottom: 10,
  },
  btnContainer: {
    marginTop: 30,
    marginLeft: 10,
    marginRight: 10,
    backgroundColor: "#EE7942",
    height: 38,
    width: 320,
    borderRadius: 5,
    justifyContent: "center",
    alignItems: "center",
  },
  textStyle: {
    fontSize: 18,
    color: "#ffffff",
  },
});

export default App;

当然,如果不想那么麻烦的进行配置,LayoutAnimation 也提供了一些 linear、spring 的替代方法,这些替代方法会直接使用默认值。

例如:

function largePress() {
    whichAni ? 
    LayoutAnimation.spring() :
    LayoutAnimation.linear();
    setWhichAni(!whichAni);
    setWidth(width + 20);
    setHeight(height + 20);
}

13.2.Animated

前面所学习的 LayoutAnimation 称为布局动画,这种方法使用起来非常便捷,它会在如透明度渐变、缩放这类变化时触发动画效果,动画会在下一次渲染或布局周期运行。布局动画还有个优点就是无需使用动画化组件,如 Animated.View。

Animated 是 RN 提供的另一种动画方式,相较于 LayoutAnimation,它更为精细,可以只作为单个组件的单个属性,也可以更加手势的响应来设定动画(例如通过手势放大图片等行为),甚至可以将多个动画变化组合到一起,并可以根据条件中断或者修改。

下面我们先来看一个快速入门示例:

import React, { useState } from "react";
import { Animated, Text, View, StyleSheet, Button, Easing } from "react-native";

const App = () => {
  // fadeAnim will be used as the value for opacity. Initial Value: 0
  const [fadeInValue, setFadeInValue] = useState(new Animated.Value(0));

  const fadeIn = () => {
    // Will change fadeAnim value to 1 in 5 seconds
    Animated.timing(fadeInValue, {
      toValue: 1,
      duration: 2000,
      easing: Easing.linear,
      useNativeDriver: true,
    }).start();
  };

  const fadeOut = () => {
    // Will change fadeAnim value to 0 in 3 seconds
    Animated.timing(fadeInValue, {
      toValue: 0,
      duration: 3000,
      useNativeDriver: true,
    }).start();
  };

  return (
    <View style={styles.container}>
      <Animated.View
        style={[
          styles.fadingContainer,
          {
            // Bind opacity to animated value
            opacity: fadeInValue,
          },
        ]}
      >
        <Text style={styles.fadingText}>Fading View!</Text>
      </Animated.View>
      <View style={styles.buttonRow}>
        <Button title="Fade In View" onPress={fadeIn} />
        <Button title="Fade Out View" onPress={fadeOut} />
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: "center",
    justifyContent: "center",
  },
  fadingContainer: {
    padding: 20,
    backgroundColor: "powderblue",
  },
  fadingText: {
    fontSize: 28,
  },
  buttonRow: {
    flexBasis: 100,
    justifyContent: "space-evenly",
    marginVertical: 16,
  },
});

export default App;

在上面的代码中,我们书写了一个淡入淡出的效果。下面我们来分析其中关键的代码。

const [fadeInValue, setFadeInValue] = useState(new Animated.Value(0));

在 App 组件中,我们定义了一个状态 fadeInValue,该状态的初始值为 new Animated.Value(0),这就是设置动画的初始值。

<Animated.View
    style={[
      styles.fadingContainer,
      {
        // Bind opacity to animated value
        opacity: fadeInValue,
      },
    ]}
>
    <Text style={styles.fadingText}>Fading View!</Text>
</Animated.View>

接下来,我们将要应用动画的组件包裹在 Animated.View 组件中,然后将 Animated.Value 绑定到组件的 style 属性上。
之后点击按钮的时候,我们要控制 Text 的显隐效果,按钮各自绑定事件,对应的代码:

const fadeIn = () => {
    // Will change fadeAnim value to 1 in 5 seconds
    Animated.timing(fadeInValue, {
      toValue: 1,
      duration: 2000,
      easing: Easing.linear,
      useNativeDriver: true,
    }).start();
};

const fadeOut = () => {
    // Will change fadeAnim value to 0 in 3 seconds
    Animated.timing(fadeInValue, {
      toValue: 0,
      duration: 3000,
      useNativeDriver: true,
    }).start();
};

在事件处理函数中,使用 Animated.timing 方法并设置动画参数,最后调用 start 方法启动动画。

timing 对应的参数属性如下:

  • duration: 动画的持续时间,默认为 500
  • easing: 缓动动画,默认为 Easing.inOut
  • delay: 开始动画前的延迟时间,默认为 0
  • isInteraction: 指定本动画是否在 InteractionManager 的队列中注册以影响任务调度,默认值为 true
  • useNativeDriver: 是否启用原生动画驱动,默认为 false

除了 timing 动画,Animated 还支持 decay 和 spring。每种动画类型都提供了特定的函数曲线,用于控制动画值从初始值到最终值的变化过程。

  • decay:衰减动画,以一个初始速度开始并且逐渐减慢停止
  • spring:弹跳动画,基于阻尼谐振动的弹性动画
  • timing:渐变动画,按照线性函数执行的动画

在 Animated 动画 API 中,decay、spring 和 timing 是动画的核心,其他复杂动画都可以使用这三种动画类型来实现。

除了上面介绍的动画 API 之外,Animated 还支持复杂的组合动画,如常见的串行动画和并行动画。Animated 可以通过以下的方法将多个动画组合起来。

  • parallel:并行执行
  • sequence:顺序执行
  • stagger:错峰执行,其实就是插入 delay 的 parallel 动画

来看一个示例:

import React, { Component } from "react";
import {
  View,
  StyleSheet,
  Text,
  Animated,
  Easing,
  TouchableOpacity,
} from "react-native";

/**
 * 串行动画
 */
export default class AnimatedTiming extends Component {
  constructor(props) {
    super(props);
    this.state = {
      bounceValue: new Animated.Value(0),
      rotateValue: new Animated.Value(0),
    };
  }

  onPress() {
    Animated.sequence([
      //串行动画函数
      Animated.spring(this.state.bounceValue, { toValue: 1,useNativeDriver: true }), //弹性动画
      Animated.delay(500),
      Animated.timing(this.state.rotateValue, {
        //渐变动画
        toValue: 1,
        duration: 800,
        easing: Easing.out(Easing.quad),
        useNativeDriver: true
      }),
    ]).start(() => this.onPress()); //开始执行动画
  }

  render() {
    return (
      <View style={styles.container}>
        <Animated.View
          style={[
            styles.content,
            {
              transform: [
                {
                  rotate: this.state.rotateValue.interpolate({
                    inputRange: [0, 1],
                    outputRange: ["0deg", "360deg"],
                  }),
                },
                {
                  scale: this.state.bounceValue,
                },
              ],
            },
          ]}
        >
          <Text style={styles.content}>Hello World!</Text>
        </Animated.View>
        <TouchableOpacity
          style={styles.btnContainer}
          onPress={this.onPress.bind(this)}
        >
          <Text style={styles.textStyle}>串行动画</Text>
        </TouchableOpacity>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: "center",
    justifyContent: "center",
    backgroundColor: "#F5FCFF",
  },
  content: {
    backgroundColor: "#FF0000",
    marginBottom: 10,
    padding: 10,
  },
  btnContainer: {
    marginTop: 30,
    marginLeft: 10,
    marginRight: 10,
    backgroundColor: "#EE7942",
    height: 38,
    width: 320,
    borderRadius: 5,
    justifyContent: "center",
    alignItems: "center",
  },
  textStyle: {
    fontSize: 18,
    color: "#ffffff",
  },
});

在上面我们就是使用的 Animated.sequence 顺序执行,如果想要并行执行,可以将上面 Animated.sequence 部分代码修改为:

Animated.parallel([
  //串行动画函数
  Animated.spring(this.state.bounceValue, { toValue: 1,useNativeDriver: true }), //弹性动画
  Animated.timing(this.state.rotateValue, {
    //渐变动画
    toValue: 1,
    duration: 800,
    easing: Easing.out(Easing.quad),
    useNativeDriver: true
  }),
]).start(() => this.onPress()); //开始执行动画

关于动画化组件,前面我们使用的是 Animated.View,目前官方提供的动画化组件有 6 种:

  • Animated.Image
  • Animated.ScrollView
  • Animated.Text
  • Animated.View
  • Animated.FlatList
  • Animated.SectionList

它们非常强大,基本可以满足大部分动画需求,在实际应用场景中,可以应用于透明度渐变、位移、缩放、颜色的变化等。

除了上面介绍的一些常见的动画场景,Animated 还支持手势控制动画。手势控制动画使用的是 Animated.event,它支持将手势或其他事件直接绑定到动态值上。

来看一个示例,下面是使用 Animated.event 实现图片水平滚动时的图片背景渐变效果。

import React, { useState } from "react";
import {
  ScrollView,
  Animated,
  Image,
  View,
  StyleSheet,
  Dimensions,
} from "react-native";

const { width } = Dimensions.get("window");

const App = () => {
  const [xOffset, setXOffset] = useState(new Animated.Value(1.0));

  return (
    <View style={styles.container}>
      <ScrollView
        horizontal={true}
        showsHorizontalScrollIndicator={false}
        style={styles.imageStyle}
        onScroll={Animated.event(
          [{ nativeEvent: { contentOffset: { x: xOffset } } }],
          { useNativeDriver: false }
        )}
        scrollEventThrottle={100}
      >
        <Animated.Image
          source={{ uri: "http://doc.zwwill.com/yanxuan/imgs/banner-1.jpg" }}
          style={[
            styles.imageStyle,
            {
              opacity: xOffset.interpolate({
                inputRange: [0, 375],
                outputRange: [1.0, 0.0],
              }),
            },
          ]}
          resizeMode="cover"
        />
        <Image
          source={{ uri: "http://doc.zwwill.com/yanxuan/imgs/banner-2.jpg" }}
          style={styles.imageStyle}
          resizeMode="cover"
        />
      </ScrollView>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: "center",
    marginTop: 44,
    justifyContent: "center",
    backgroundColor: "#F5FCFF",
  },
  imageStyle: {
    height: 200,
    width: width,
  },
});

export default App;

当 ScrollView 逐渐向左滑动时,左边的图片的透明度会逐渐降为 0。

作为提升用户体验的重要手段,动画对于移动应用程序来说是非常重要的,因此合理地使用动画是必须掌握的一项技能。

十四、手势API

这一小节我们来看一下 RN 中和手势相关的 API
文档地址:https://reactnative.cn/docs/panresponder
我们先来看一个简单的例子:

import React from "react";
import { PanResponder, StyleSheet, View } from "react-native";

export default function App() {
  const panResponder = PanResponder.create({
    onStartShouldSetPanResponder: function () {
      console.log("shouldstart");
      return true;
    },
    onPanResponderMove: function () {
      console.log("moving");
    },
    onPanResponderRelease: function () {
      console.log("release");
    },
  });

  console.log(panResponder.panHandlers);

  return (
    <View style={styles.container}>
      <View style={styles.box} {...panResponder.panHandlers}></View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
  },
  box: {
    backgroundColor: "#61dafb",
    width: 80,
    height: 80,
    borderRadius: 4,
  },
});

在上面的示例中,我们通过 PanResponder 这个 API 的 create 方法来创建一个手势方法的集合对象。该方法接收一个配置对象,配置对象中能够传递的参数如下:
在这里插入图片描述
可以看到,配置对象对应的每一个配置值都是一个回调函数,每个回调函数都接收两个参数,一个是原生事件对象,另一个是 gestureState 对象。

nativeEvent 原生事件对象有如下字段:

  • changedTouches - 在上一次事件之后,所有发生变化的触摸事件的数组集合(即上一次事件后,所有移动过的触摸点)
  • identifier - 触摸点的 ID
  • locationX - 触摸点相对于父元素的横坐标
  • locationY - 触摸点相对于父元素的纵坐标
  • pageX - 触摸点相对于根元素的横坐标
  • pageY - 触摸点相对于根元素的纵坐标
  • target - 触摸点所在的元素 ID
  • timestamp - 触摸事件的时间戳,可用于移动速度的计算
  • touches - 当前屏幕上的所有触摸点的集合

一个 gestureState 对象有如下的字段:

  • stateID - 触摸状态的 ID。在屏幕上有至少一个触摸点的情况下,这个 ID 会一直有效。
  • moveX - 最近一次移动时的屏幕横坐标
  • moveY - 最近一次移动时的屏幕纵坐标
  • x0 - 当响应器产生时的屏幕坐标
  • y0 - 当响应器产生时的屏幕坐标
  • dx - 从触摸操作开始时的累计横向路程
  • dy - 从触摸操作开始时的累计纵向路程
  • vx - 当前的横向移动速度
  • vy - 当前的纵向移动速度
  • numberActiveTouches - 当前在屏幕上的有效触摸点的数量

例如我们通过 gestureState 对象来判断用户手指的移动方向:

const panResponder = PanResponder.create({
    onStartShouldSetPanResponder: function () {
      console.log("shouldstart");
      return true;
    },
    onPanResponderMove: function (e, gs) {
      console.log(`正在移动: X轴: ${gs.dx}, Y轴: ${gs.dy}`);
    },
    onPanResponderRelease: function (e, gs) {
      console.log(`结束移动: X轴移动了: ${gs.dx}, Y轴移动了: ${gs.dy}`);
      if (gs.dx > 50) {
        console.log("由左向右");
      } else if (gs.dx < -50) {
        console.log("由右向左");
      } else if (gs.dy > 50) {
        console.log("由上向下");
      } else if (gs.dy < -50) {
        console.log("由下向上");
      }
    },
});

最后,我们把上一节课介绍的 Animated 结合起来,书写一个拖动小方块的示例:

import { useState } from "react";
import { Animated, PanResponder, StyleSheet, View } from "react-native";

export default function App() {
  const [transXY] = useState(new Animated.ValueXY());

  const panResponder = PanResponder.create({
    onStartShouldSetPanResponder: function () {
      console.log("shouldstart");
      return true;
    },
    onPanResponderMove: Animated.event(
      [
        null,
        {
          dx: transXY.x,
          dy: transXY.y,
        },
      ],
      { useNativeDriver: false }
    ),

    onPanResponderRelease: function () {
      Animated.spring(transXY, {
        toValue: { x: 0, y: 0 },
        useNativeDriver: false,
      }).start();
    },
  });

  return (
    <View style={styles.container}>
      <Animated.View
        style={[
          styles.box,
          {
            transform: [{ translateX: transXY.x }, { translateY: transXY.y }],
          },
        ]}
        {...panResponder.panHandlers}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
  },
  box: {
    backgroundColor: "#61dafb",
    width: 80,
    height: 80,
    borderRadius: 4,
  },
});

十五、React Navigation

15.1.React Navigation 简介

从本章起我们开始学习 RN 社区相关的生态库。RN 的社区生态相当丰富,很多东西官方没有提供,但是在社区已经有了很好的解决方案。
这里首当其冲要介绍的就是 React Navigation,这是一个诞生于社区的 RN 导航库。
本小节将介绍如下的内容:

  • 什么是 React Navigation
  • React Navigation 安装

15.2.什么是 React Navigation

React Navigation 的诞生,源于 RN 社区对基于 Javascript 的可扩展且使用简单的导航解决方案的需求。
React Navigation 是 Facebook、Expo 和 React 社区的开发者们合作的结果:它取代并改进了 RN 生态系统中的多个导航库,其中包括 Ex-Navigation、RN 官方的 Navigator 和 NavigationExperimental 组件。
学习 React Navigation,可以阅读官方的文档:https://reactnavigation.org/

React Navigation 特点
在 React Navigation 中,内置了 3 种导航器,可以帮助我们实现页面之间的跳转。
主要包含以下 3 种导航器:

  • StackNavigator:一次只渲染一个页面,并提供页面之间跳转的方法。当打开一个新的页面时,它被放置在堆栈的顶部。简单来讲,就是普通页面跳,可传递参数。
  • TabNavigator:渲染一个选项卡,类似底部导航栏,让用户可以在同一屏中进行几个页面之间切换。
  • DrawerNavigator:提供一个从屏幕左侧滑入的抽屉。

15.3.React Navigation 安装

接下来,要使用 React Navigation 首先第一步肯定是需要安装这个库。
关于安装,请参阅:https://reactnavigation.org/docs/getting-started
首先第一步,在项目中输入如下的命令:

npm install @react-navigation/native

安装完成后,根据官方文档的描述,还需要安装 react-native-screens 以及 react-native-safe-area-context 这两个依赖库,因为我们是使用 expo 搭建的项目,所以可以输入如下的命令:

expo install react-native-screens react-native-safe-area-context

15.3.1.快速体验 React Navigation

安装完成后,我们就可以来书写一个简单的 demo 来体验下 React Navigation。
由于新版本的 React Navigation 已经将导航器独立成了一个单独的包,因此我们首先需要安装要用到的导航器。

npm install @react-navigation/native-stack

接下来书写如下的测试代码:

// In App.js in a new project

import * as React from "react";
import { View, Text, Button } from "react-native";
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";

function HomeScreen({ navigation }) {
  return (
    <View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
      <Text>Home Screen</Text>
      <Button
        title="Go to Details"
        onPress={() => navigation.navigate("Details")}
      />
    </View>
  );
}

function DetailsScreen({ navigation }) {
  return (
    <View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
      <Text>Details Screen</Text>
      <Button
        title="Go to Details1"
        onPress={() => navigation.push("Details")}
      />
      <Button
        title="Go to Details2"
        onPress={() => navigation.navigate("Details")}
      />
      <Button title="Go to Home" onPress={() => navigation.navigate("Home")} />
      <Button title="Go back" onPress={() => navigation.goBack()} />
    </View>
  );
}

const Stack = createNativeStackNavigator();

function App() {
  return (
    <NavigationContainer>   
      <Stack.Navigator
	       initialRouteName="BottomTabs"  // 初始路由
	     	 screenOptions={{
	       		 headerShown: false,
	     	 }}
	      headerBackTitleStyle={{fontWeight: '600'}}   
      >
        <Stack.Screen
          name="Home"
          component={HomeScreen}
          options={{ title: "Overview" }}
        />
        <Stack.Screen name="Details" component={DetailsScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

export default App;

在上面的代码中,我们首先创建了 HomeScreen 和 DetailsScreen 这两个组件,也就是我们的两个屏幕。
接下来调用 createNativeStackNavigator 方法创建了一个 Stack 导航的实例对象,之后通过如下的结构嵌套多个屏幕:

<NavigationContainer>
  <Stack.Navigator>
    <Stack.Screen
      name="Home"
      component={HomeScreen}
      options={{ title: "Overview" }}
    />
    <Stack.Screen name="Details" component={DetailsScreen} />
  </Stack.Navigator>
</NavigationContainer>

可以看到,Stack.Screen 就代表一屏,因为我们现在有两屏,所以一共有两个 Stack.Screen。
在屏幕组件中,会自动传入当前的导航器实例,通过解构拿到这个导航器实例,上面常用的方法有:

  • navigate:导航方法,要导航到哪一屏,如果本身已经处于该屏,则不进行操作
  • push:以栈的形式往路由栈里面压入新的一屏,即使当前已处于该屏,也会重复压入新的一屏
  • goBack:返回上一屏,简单来讲就是栈顶那一屏出栈,回到栈顶的倒数第二屏

15.4.React Navigation开发使用(BottomTab)

import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";

function BottomTabs({ navigation, dispatch }: any) {
 
  //BottomTabs页面默认返回事件变为离开APP
  useEffect(() => {
    const backHandler = BackHandler.addEventListener(
      "hardwareBackPress",
      onBackButtonPress
    );
    return () => backHandler.remove();
  }, []);
  const onBackButtonPress = () => {
    if (navigations.isFocused()) {
      BackHandler.exitApp();
      return true;
    } else {
      return false;
    }
  };
  
  return (
    <Tab.Navigator
      initialRouteName="Message"
      screenOptions={({ route }) => ({
        tabBarIcon: ({ focused }) => {
          let iconPath: any;
          if (route.name === "Message") {
            iconPath = focused
              ? require("../static/icon/activeMessage.png")
              : require("../static/icon/message.png");
          }
          if (route.name === "Contact") {
            iconPath = focused
              ? require("../static/icon/activeContact.png")
              : require("../static/icon/contact.png");
          }
          if (route.name === "Staying") {
            iconPath = focused
              ? require("../static/icon/activeStaying.png")
              : require("../static/icon/staying.png");
          }
          if (route.name === "User") {
            iconPath = focused
              ? require("../static/icon/activeUser.png")
              : require("../static/icon/user.png");
          }
          return <Image source={iconPath} style={{ width: 20, height: 20 }} />;
        },
      })}
    >
      <Tab.Screen
        options={{
          title: "消息",
          headerStyle: {
            ...sharedStyles.TabScreen,
          },
          headerTitleStyle: {
            fontWeight: "bold",
          },
          tabBarLabelStyle: {
            ...sharedStyles.tabBarLabel,
          },
          tabBarBadge: getUnread(),
        }}
        name="Message"
        component={MessageView}
        listeners={() => ({
          tabPress: () => {
            dispatch({
              type: "RE_ROOM",
            });
          },
        })}
      />
      <Tab.Screen
        options={{
          title: "联系人",
          headerStyle: {
            ...sharedStyles.TabScreen,
          },
          headerTitleStyle: {
            fontWeight: "bold",
          },
          tabBarLabelStyle: {
            ...sharedStyles.tabBarLabel,
          },
          headerRight: () => RItem,
        }}
        name="Contact"
        component={ContactsView}
      />
      <Tab.Screen
        options={{
          title: "工作台",
          headerStyle: {
            ...sharedStyles.TabScreen,
          },
          headerTitleStyle: {
            fontWeight: "bold",
          },
          tabBarLabelStyle: {
            ...sharedStyles.tabBarLabel,
          },
        }}
        name="Staying"
        component={StayingView}
      />
      <Tab.Screen
        options={{
          title: "我的",
          headerStyle: {
            ...sharedStyles.TabScreen,
          },
          headerTitleStyle: {
            fontWeight: "bold",
          },
          tabBarLabelStyle: {
            ...sharedStyles.tabBarLabel,
          },
        }}
        name="User"
        component={UserView}
      />
    </Tab.Navigator>
  );
}

const mapStateToProps = ({ Room }: any) => {
  return {
    rooms: Room.rooms,
  };
};

export default connect(mapStateToProps)(BottomTabs);

Styles.ts

import {StyleSheet} from 'react-native';

export default StyleSheet.create({
  TabScreen: {
    backgroundColor: '#f7f7f7',
    elevation: 0,
    shadowOpacity: 0,
  },
  tabBarLabel: {
    fontSize: 10,
    lineHeight: 16,
    fontWeight: '600',
  }
});

十六、参数传递和标题栏信息配置

本小节我们来看两个内容:

  • 参数传递
  • 标题栏信息配置

16.1.参数传递

参数传递在导航中是一个非常重要的内容,例如点击电影进入到这一部电影的电影详情,那么我们就需要传递一个 id 过去。
参数传递整体分为两步:

  • 传递参数:通过将参数放在对象中作为 navigation.navigate 函数的第二个参数,将参数传递给路由
navigation.navigate('RouteName', { /* params go here */ })
  • 接受参数:获取上一屏组件传递过来的参数
route.params

来看一个官方提供的示例:

function HomeScreen({ navigation }) {
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Home Screen</Text>
      <Button
        title="Go to Details"
        onPress={() => {
          /* 1. Navigate to the Details route with params */
          navigation.navigate('Details', {
            itemId: 86,
            otherParam: 'anything you want here',
          });
        }}
      />
    </View>
  );
}

function DetailsScreen({ route, navigation }) {
  /* 2. Get the param */
  const { itemId, otherParam } = route.params;
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Details Screen</Text>
      <Text>itemId: {JSON.stringify(itemId)}</Text>
      <Text>otherParam: {JSON.stringify(otherParam)}</Text>
      <Button
        title="Go to Details... again"
        onPress={() =>
          navigation.push('Details', {
            itemId: Math.floor(Math.random() * 100),
          })
        }
      />
      <Button title="Go to Home" onPress={() => navigation.navigate('Home')} />
      <Button title="Go back" onPress={() => navigation.goBack()} />
    </View>
  );
}

第二屏不仅可以接收第一屏传递过来的参数,还可以手动修改这个参数,例如:

<Button title="修改参数" onPress={() => {
    navigation.setParams({
      otherParam: 'someText',
    });
}}/>

在上面的代码中,我们在 DetailsScreen 这一屏所返回的 JSX 中,添加了一个 Button 组件,点击之后通过 navigation.setParams 重新设置接收到的 otherParam 参数。
可以在 Stack.Screen 中通过 initialParams 属性设置参数的默认值,例如:

<Stack.Screen 
  name="Details" 
  component={DetailsScreen} 
  initialParams={{ itemId: 42 }}
/>

设置之后,我们在 Home 这一屏中,跳转到 DetailsScreen 时,即使不传递 itemId 参数,DetailsScreen 这一屏也能接收到一个名为 itemId 的参数。
有些时候,我们并不是只会将第一屏的数据传递给第二屏,可能刚好相反要将第二屏的数据反向传递给第一屏,使用 navigate 方法的时候,也可以很轻松的向上一屏传递数据。例如:

import * as React from 'react';
import { Text, TextInput, View, Button } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';

function HomeScreen({ navigation, route }) {
  React.useEffect(() => {
    if (route.params?.post) {
      // Post updated, do something with `route.params.post`
      // For example, send the post to the server
    }
  }, [route.params?.post]);

  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Button
        title="Create post"
        onPress={() => navigation.navigate('CreatePost')}
      />
      <Text style={{ margin: 10 }}>Post: {route.params?.post}</Text>
    </View>
  );
}

function CreatePostScreen({ navigation, route }) {
  const [postText, setPostText] = React.useState('');

  return (
    <>
      <TextInput
        multiline
        placeholder="What's on your mind?"
        style={{ height: 200, padding: 10, backgroundColor: 'white' }}
        value={postText}
        onChangeText={setPostText}
      />
      <Button
        title="Done"
        onPress={() => {
          // Pass and merge params back to home screen
          navigation.navigate({
            name: 'Home',
            params: { post: postText },
            merge: true,
          });
        }}
      />
    </>
  );
}

const Stack = createNativeStackNavigator();

export default function App() {
  return (
    <NavigationContainer>
      <Stack.Navigator mode="modal">
        <Stack.Screen name="Home" component={HomeScreen} />
        <Stack.Screen name="CreatePost" component={CreatePostScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

16.1.1参数传递——跨深层级传递(重点、常用)

传递给上一个页面数据

 navigation.dispatch((state: any) => {
        const prevRoute = state.routes[state.routes.length - 2];
        return navigation.navigate({
          name: prevRoute.name,
          params: {
            id,
          },
          merge: true,
        });
      });

16.2.标题栏信息配置

Screen 组件接受 options 属性,它可以是对象,也可以是返回对象的函数,其中包含各种配置选项。 例如上面我们所写的:

<Stack.Screen
  name="Home"
  component={HomeScreen}
  options={{ title: "Overview" }}
/>

有些时候标题栏并不是一开始就固定的,而是通过上一屏跳转过来时传递的参数过来而决定的。例如:

import * as React from 'react';
import { View, Text, Button } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';

function HomeScreen({ navigation }) {
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Home Screen</Text>
      <Button
        title="Go to Profile"
        onPress={() =>
          navigation.navigate('Profile', { name: 'Custom profile header' })
        }
      />
    </View>
  );
}

function ProfileScreen({ navigation }) {
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Profile screen</Text>
      <Button title="Go back" onPress={() => navigation.goBack()} />
    </View>
  );
}

const Stack = createNativeStackNavigator();

function App() {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen
          name="Home"
          component={HomeScreen}
          options={{ title: 'My home' }}
        />
        <Stack.Screen
          name="Profile"
          component={ProfileScreen}
          options={({ route }) => ({ title: route.params.name })}
        />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

export default App;

在标题栏内容已经确定的当前屏幕下,想要修改当前屏的标题,可以使用 navigation.setOptions,来看下面的例子:

import * as React from 'react';
import { View, Text, Button } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';

function HomeScreen({ navigation }) {
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Home Screen</Text>
      <Button
        title="Update the title"
        onPress={() => navigation.setOptions({ title: 'Updated!' })}
      />
    </View>
  );
}

const Stack = createNativeStackNavigator();

function App() {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen
          name="Home"
          component={HomeScreen}
          options={{ title: 'My home' }}
        />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

export default App;

我们可以自定义标题的样式。自定义标题样式时要使用三个关键属性:headerStyle、headerTintColor 和 headerTitleStyle。

  • headerStyle:一个样式对象,将应用于包装标题的 View。 如果你在它上面设置了
  • backgroundColor,那将是你的标题的颜色。
  • headerTintColor:后退按钮和标题都使用这个属性作为它们的颜色。 在下面的示例中,我们将色调颜色设置为白色
    (#fff),因此后退按钮和标题标题将为白色。
  • headerTitleStyle:如果我们想自定义标题的 fontFamily、fontWeight 等 Text
    样式属性,可以用这个来做。

例如:

import * as React from 'react';
import { View, Text } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';

function HomeScreen() {
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Home Screen</Text>
    </View>
  );
}

const Stack = createNativeStackNavigator();

function App() {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen
          name="Home"
          component={HomeScreen}
          options={{
            title: 'My home',
            headerStyle: {
              backgroundColor: '#f4511e',
            },
            headerTintColor: '#fff',
          }}
        />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

export default App;

一个很常见的需求,就是将整个应用的标题栏样式统一成一个样式。可以通过配置 screenOptions 来实现这个功能。如下:

// In App.js in a new project

import * as React from "react";
import { View, Text, Button } from "react-native";
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";

function HomeScreen({ navigation }) {
  return (
    <View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
      <Text>Home Screen</Text>
      <Button
        title="Go to Details"
        onPress={() => navigation.navigate("Details")}
      />
    </View>
  );
}

function DetailsScreen({ navigation }) {
  return (
    <View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
      <Text>Details Screen</Text>
    </View>
  );
}

const Stack = createNativeStackNavigator();

function App() {
  return (
    <NavigationContainer>
      <Stack.Navigator
        screenOptions={{
          headerStyle: {
            backgroundColor: "#f4511e",
          },
          headerTintColor: "#fff",
        }}
      >
        <Stack.Screen
          name="Home"
          component={HomeScreen}
          options={{ title: "Overview" }}
        />
        <Stack.Screen name="Details" component={DetailsScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

export default App;

有时,我们需要的不仅仅是更改标题的文本和样式,还需要更多的控制。

例如,我们可能想要渲染图像来代替标题,或者将标题变成一个按钮。在这些情况下,可以完全覆盖用于标题的组件并提供我们自己的组件。例如:

import * as React from 'react';
import { View, Text, Image } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';

function HomeScreen() {
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Home Screen</Text>
    </View>
  );
}

function LogoTitle() {
  return (
    <Image
      style={{ width: 30, height: 30 }}
      source={require('./images/logo.jpg')}
    />
  );
}

const Stack = createNativeStackNavigator();

function App() {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen
          name="Home"
          component={HomeScreen}
          options={{ headerTitle: (props) => <LogoTitle {...props} /> }}
        />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

export default App;

十七、嵌套路由与生命周期

本小节主要介绍 React Navigation 中的嵌套路由和路由的生命周期。

17.1.嵌套路由

嵌套路由意味着在一个屏幕内渲染另一个路由,例如:

function Home() {
  return (
    <Tab.Navigator>
      <Tab.Screen name="Feed" component={Feed} />
      <Tab.Screen name="Messages" component={Messages} />
    </Tab.Navigator>
  );
}

function App() {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen
          name="Home"
          component={Home}
          options={{ headerShown: false }}
        />
        <Stack.Screen name="Profile" component={Profile} />
        <Stack.Screen name="Settings" component={Settings} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

在上面的示例中,Home 组件是一个选项卡路由,但同时 Home 组件还用于 App 组件内 Stack 导航的主屏幕。所以在这里,选项卡路由嵌套在一个堆栈导航器中,类似于如下的结构:

Stack.Navigator
Home (Tab.Navigator)
Feed (Screen)
Messages (Screen)
Profile (Screen)
Settings (Screen)
下面是一个比较常见的嵌套路由示例:

import * as React from 'react';
import { Button, View, Text } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';

function SettingsScreen({ navigation }) {
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Settings Screen</Text>
      <Button
        title="Go to Profile"
        onPress={() => navigation.navigate('Profile')}
      />
    </View>
  );
}

function ProfileScreen({ navigation }) {
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Profile Screen</Text>
      <Button
        title="Go to Settings"
        onPress={() => navigation.navigate('Settings')}
      />
    </View>
  );
}

function HomeScreen({ navigation }) {
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Home Screen</Text>
      <Button
        title="Go to Details"
        onPress={() => navigation.navigate('Details')}
      />
    </View>
  );
}

function DetailsScreen({ navigation }) {
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Details Screen</Text>
      <Button
        title="Go to Details... again"
        onPress={() => navigation.push('Details')}
      />
    </View>
  );
}
const Tab = createBottomTabNavigator();
const SettingsStack = createNativeStackNavigator();
const HomeStack = createNativeStackNavigator();

export default function App() {
  return (
    <NavigationContainer>
      <Tab.Navigator screenOptions={{ headerShown: false }}>
        <Tab.Screen name="First">
          {() => (
            <SettingsStack.Navigator>
              <SettingsStack.Screen
                name="Settings"
                component={SettingsScreen}
              />
              <SettingsStack.Screen name="Profile" component={ProfileScreen} />
            </SettingsStack.Navigator>
          )}
        </Tab.Screen>
        <Tab.Screen name="Second">
          {() => (
            <HomeStack.Navigator>
              <HomeStack.Screen name="Home" component={HomeScreen} />
              <HomeStack.Screen name="Details" component={DetailsScreen} />
            </HomeStack.Navigator>
          )}
        </Tab.Screen>
      </Tab.Navigator>
    </NavigationContainer>
  );
}

当使用嵌套路由时,有一些注意细节,这里可以参阅官方文档:
https://reactnavigation.org/docs/nesting-navigators/#how-nesting-navigators-affects-the-behaviour
嵌套路由的最佳实践
一般来讲,我们应该尽可能的减少嵌套的层数,因为过多的嵌套层数可能会导致如下的问题:

  • 过深的嵌套层数可能导致低端设备出现内存和性能问题
  • 嵌套相同类型的导航器(例如选项卡内的选项卡,抽屉内的抽屉等)可能会导致混乱的用户体验
  • 由于嵌套过多,在导航到嵌套屏幕、配置深层链接等时,代码变得难以调试和阅读

下面是一个关于登录注册的嵌套路由的最佳实践示例:

<Stack.Navigator>
  {isLoggedIn ? (
    // Screens for logged in users
    <Stack.Group>
      <Stack.Screen name="Home" component={Home} />
      <Stack.Screen name="Profile" component={Profile} />
    </Stack.Group>
  ) : (
    // Auth screens
    <Stack.Group screenOptions={{ headerShown: false }}>
      <Stack.Screen name="SignIn" component={SignIn} />
      <Stack.Screen name="SignUp" component={SignUp} />
    </Stack.Group>
  )}
  {/* Common modal screens */}
  <Stack.Group screenOptions={{ presentation: 'modal' }}>
    <Stack.Screen name="Help" component={Help} />
    <Stack.Screen name="Invite" component={Invite} />
  </Stack.Group>
</Stack.Navigator>

17.2.监听物理返回按钮的按下事件——BackHandler(常用)

import {useNavigation} from '@react-navigation/native';

  const navigations = useNavigation();
  useEffect(() => {
    const backHandler = BackHandler.addEventListener(
      'hardwareBackPress',
      onBackButtonPress,
    );
    return () => {
      backHandler.remove();
    };
  }, []);
  const onBackButtonPress = () => {
    if (navigations.isFocused()) {
      navigation.goBack();
      return true;
    } else {
      return false;
    }
  };

17.3.生命周期

在 React 的类组件中,存在生命周期这一特性。
考虑具有屏幕 A 和 B 的 Stack 类型路由。导航到 A 后,调用其 componentDidMount。在压入 B 时,它的 componentDidMount 也会被调用,但 A 仍然挂载在堆栈上,因此不会调用它的 componentWillUnmount。
从 B 回到 A 时,调用了 B 的 componentWillUnmount,但 A 的 componentDidMount 没有被调用,因为 A 一直处于挂载状态。
这就是在 React 中类组件的生命周期钩子函数特性。这些 React 生命周期方法在 React Navigation 中仍然有效。
不过自从 React 推出了 Hook 后,更多的使用函数式组件,类组件中的生命周期钩子函数自然也被一些 Hook 替代。
我们可以通过监听 focus 和 blur 事件来分别了解屏幕何时聚焦或失焦。

function Profile({ navigation }) {
  React.useEffect(() => {
    const unsubscribe = navigation.addListener('focus', () => {
      // Screen was focused
      // Do something
    });

    return unsubscribe;
  }, [navigation]);

  return <ProfileContent />;
}

另外,我们还可以使用 useFocusEffect 挂钩来执行副作用来替代上面手动添加事件侦听器的方式。它类似于 React 的 useEffect 钩子,但它与导航生命周期相关联。
下面是一个使用示例:

import * as React from 'react';
import { Button, View, Text } from 'react-native';
import { NavigationContainer, useFocusEffect } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';

function SettingsScreen({ navigation }) {
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Settings Screen</Text>
      <Button
        title="Go to Profile"
        onPress={() => navigation.navigate('Profile')}
      />
    </View>
  );
}

function ProfileScreen({ navigation }) {
  useFocusEffect(
    React.useCallback(() => {
      alert('Screen was focused');
      // Do something when the screen is focused
      return () => {
        alert('Screen was unfocused');
        // Do something when the screen is unfocused
        // Useful for cleanup functions
      };
    }, [])
  );

  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Profile Screen</Text>
      <Button
        title="Go to Settings"
        onPress={() => navigation.navigate('Settings')}
      />
    </View>
  );
}

function HomeScreen({ navigation }) {
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Home Screen</Text>
      <Button
        title="Go to Details"
        onPress={() => navigation.navigate('Details')}
      />
    </View>
  );
}

function DetailsScreen({ navigation }) {
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Details Screen</Text>
      <Button
        title="Go to Details... again"
        onPress={() => navigation.push('Details')}
      />
    </View>
  );
}
const Tab = createBottomTabNavigator();
const SettingsStack = createNativeStackNavigator();
const HomeStack = createNativeStackNavigator();

export default function App() {
  return (
    <NavigationContainer>
      <Tab.Navigator screenOptions={{ headerShown: false }}>
        <Tab.Screen name="First">
          {() => (
            <SettingsStack.Navigator>
              <SettingsStack.Screen
                name="Settings"
                component={SettingsScreen}
              />
              <SettingsStack.Screen name="Profile" component={ProfileScreen} />
            </SettingsStack.Navigator>
          )}
        </Tab.Screen>
        <Tab.Screen name="Second">
          {() => (
            <HomeStack.Navigator>
              <HomeStack.Screen name="Home" component={HomeScreen} />
              <HomeStack.Screen name="Details" component={DetailsScreen} />
            </HomeStack.Navigator>
          )}
        </Tab.Screen>
      </Tab.Navigator>
    </NavigationContainer>
  );
}

十八、其他类型的导航

除了上面我们所介绍的 Stack 类型导航以外,React Navigation 中提供了常用的其他类型的导航。本小节我们就一起来看一下这些常用导航类型,主要包括:

  • Tab navigation
  • Drawer navigation
  • Material Top Tabs Navigator

18.1.Tab navigation(重点)

移动应用程序中最常见的导航样式可能是基于选项卡的导航。这可以是屏幕底部的选项卡,也可以是标题下方顶部的选项卡(甚至可以代替标题)。
首先安装 @react-navigation/bottom-tabs:

npm install @react-navigation/bottom-tabs

下面是一个 Tab navigation 最基本的示例:

import * as React from 'react';
import { Text, View } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';

function HomeScreen() {
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>Home!</Text>
    </View>
  );
}

function SettingsScreen() {
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>Settings!</Text>
    </View>
  );
}

const Tab = createBottomTabNavigator();

function MyTabs() {
  return (
    <Tab.Navigator>
      <Tab.Screen name="Home" component={HomeScreen} />
      <Tab.Screen name="Settings" component={SettingsScreen} />
    </Tab.Navigator>
  );
}

export default function App() {
  return (
    <NavigationContainer>
      <MyTabs />
    </NavigationContainer>
  );
}

我们同样可以自定义外观,这类似于之前所介绍的 Stack 路由的方式,在初始化选项卡导航器时会设置一些属性,而其他属性可以在选项中按屏幕自定义。

// You can import Ionicons from @expo/vector-icons if you use Expo or
// react-native-vector-icons/Ionicons otherwise.
import * as React from "react";
import { Text, View } from "react-native";
import { Ionicons } from "@expo/vector-icons";
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import { NavigationContainer } from "@react-navigation/native";

function HomeScreen() {
  return (
    <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
      <Text>Home!</Text>
    </View>
  );
}

function MailScreen() {
  return (
    <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
      <Text>Settings!</Text>
    </View>
  );
}

const Tab = createBottomTabNavigator();

export default function App() {
  return (
    <NavigationContainer>
      <Tab.Navigator
        screenOptions={({ route }) => ({
          tabBarIcon: ({ focused, color, size }) => {
            let iconName;

            if (route.name === "Home") {
              iconName = focused
                ? "ios-information-circle"
                : "ios-information-circle-outline";
            } else if (route.name === "Mail") {
              iconName = focused ? "ios-mail" : "ios-mail-unread";
            }

            // You can return any component that you like here!
            return <Ionicons name={iconName} size={size} color={color} />;
          },
          tabBarActiveTintColor: "tomato",
          tabBarInactiveTintColor: "gray",
        })}
      >
        <Tab.Screen name="Home" component={HomeScreen} />
        <Tab.Screen name="Mail" component={MailScreen} />
      </Tab.Navigator>
    </NavigationContainer>
  );
}

在上面的示例中,我们用到了 tabBarIcon 属性,该属性是底部选项卡导航器中支持的选项,需要将它放在 Tab.Navigator 的 screenOptions 属性中是为了方便集中图标配置。tabBarIcon 是一个函数,它被赋予了焦点状态、颜色和大小参数。另外,tabBarActiveTintColor 和 tabBarInactiveTintColor 表示活动以及非活动的颜色值。
有时我们想给一些图标添加徽章。可以使用 tabBarBadge 选项来执行此操作:

<Tab.Screen name="Mail" component={MailScreen} options={{ tabBarBadge: 3 }} />

当我们处于某一屏中,想要通过屏幕中的按钮进行 Tab 跳转可以使用 navigation.navigate

import * as React from 'react';
import { Button, Text, View } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';

function HomeScreen({ navigation }) {
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>Home!</Text>
      <Button
        title="Go to Settings"
        onPress={() => navigation.navigate('Settings')}
      />
    </View>
  );
}

function SettingsScreen({ navigation }) {
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>Settings!</Text>
      <Button title="Go to Home" onPress={() => navigation.navigate('Home')} />
    </View>
  );
}

const Tab = createBottomTabNavigator();

export default function App() {
  return (
    <NavigationContainer>
      <Tab.Navigator>
        <Tab.Screen name="Home" component={HomeScreen} />
        <Tab.Screen name="Settings" component={SettingsScreen} />
      </Tab.Navigator>
    </NavigationContainer>
  );
}

很多时候,我们的某一屏并不是只属于某一个 Tab 栏,而是有多个 Tab 栏都可以跳转到这一屏,下面是一个这种情况的示例:

import * as React from 'react';
import { Button, Text, View } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';

function DetailsScreen() {
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>Details!</Text>
    </View>
  );
}

function HomeScreen({ navigation }) {
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>Home screen</Text>
      <Button
        title="Go to Details"
        onPress={() => navigation.navigate('Details')}
      />
    </View>
  );
}

function SettingsScreen({ navigation }) {
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>Settings screen</Text>
      <Button
        title="Go to Details"
        onPress={() => navigation.navigate('Details')}
      />
    </View>
  );
}

const HomeStack = createNativeStackNavigator();

function HomeStackScreen() {
  return (
    <HomeStack.Navigator>
      <HomeStack.Screen name="Home" component={HomeScreen} />
      <HomeStack.Screen name="Details" component={DetailsScreen} />
    </HomeStack.Navigator>
  );
}

const SettingsStack = createNativeStackNavigator();

function SettingsStackScreen() {
  return (
    <SettingsStack.Navigator>
      <SettingsStack.Screen name="Settings" component={SettingsScreen} />
      <SettingsStack.Screen name="Details" component={DetailsScreen} />
    </SettingsStack.Navigator>
  );
}

const Tab = createBottomTabNavigator();

export default function App() {
  return (
    <NavigationContainer>
      <Tab.Navigator screenOptions={{ headerShown: false }}>
        <Tab.Screen name="Home" component={HomeStackScreen} />
        <Tab.Screen name="Settings" component={SettingsStackScreen} />
      </Tab.Navigator>
    </NavigationContainer>
  );
}

更多关于 Tab navigation 标签页导航的 API,可以参阅 https://reactnavigation.org/docs/bottom-tab-navigator#api-definition

18.2.Drawer navigation

Drawer navigation 翻译成中文叫做抽屉导航,其实就是导航中常见的使用左侧(有时是右侧)的抽屉在屏幕之间导航。
首先第一步还是需要安装该类型的导航:

npm i @react-navigation/drawer

除了安装 @react-navigation/drawer 依赖库以外,这种类型的导航还需要额外安装 react-native-gesture-handler 以及 react-native-reanimated 依赖库,然后将下面的:

import 'react-native-gesture-handler';

放在入口文件的最上面,并且在 babel.config.js 中添加 plugins,具体操作如下图:
在这里插入图片描述
具体安装步骤请参阅:https://reactnavigation.org/docs/drawer-navigator#installation

下面是一个关于抽屉导航的简单示例:

import 'react-native-gesture-handler';
import * as React from 'react';
import { Button, View } from 'react-native';
import { createDrawerNavigator } from '@react-navigation/drawer';
import { NavigationContainer } from '@react-navigation/native';

function HomeScreen({ navigation }) {
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Button
        onPress={() => navigation.navigate('Notifications')}
        title="Go to notifications"
      />
    </View>
  );
}

function NotificationsScreen({ navigation }) {
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Button onPress={() => navigation.goBack()} title="Go back home" />
    </View>
  );
}

const Drawer = createDrawerNavigator();

export default function App() {
  return (
    <NavigationContainer>
      <Drawer.Navigator useLegacyImplementation initialRouteName="Home">
        <Drawer.Screen name="Home" component={HomeScreen} />
        <Drawer.Screen name="Notifications" component={NotificationsScreen} />
      </Drawer.Navigator>
    </NavigationContainer>
  );
}

注:添加 Babel 插件后,重新启动开发服务器并清除捆绑程序缓存:expo start --clear。

可以通过 open 和 close 方法来打开或者关闭抽屉,通过 toggleDrawer 来切换抽屉。
示例如下:

import * as React from 'react';
import { View, Text, Button } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import {
  createDrawerNavigator,
  DrawerContentScrollView,
  DrawerItemList,
  DrawerItem,
} from '@react-navigation/drawer';

function Feed({ navigation }) {
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>Feed Screen</Text>
      <Button title="Open drawer" onPress={() => navigation.openDrawer()} />
      <Button title="Toggle drawer" onPress={() => navigation.toggleDrawer()} />
    </View>
  );
}

function Notifications() {
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>Notifications Screen</Text>
    </View>
  );
}

function CustomDrawerContent(props) {
  return (
    <DrawerContentScrollView {...props}>
      <DrawerItemList {...props} />
      <DrawerItem
        label="Close drawer"
        onPress={() => props.navigation.closeDrawer()}
      />
      <DrawerItem
        label="Toggle drawer"
        onPress={() => props.navigation.toggleDrawer()}
      />
    </DrawerContentScrollView>
  );
}

const Drawer = createDrawerNavigator();

function MyDrawer() {
  return (
    <Drawer.Navigator
      useLegacyImplementation
      drawerContent={(props) => <CustomDrawerContent {...props} />}
    >
      <Drawer.Screen name="Feed" component={Feed} />
      <Drawer.Screen name="Notifications" component={Notifications} />
    </Drawer.Navigator>
  );
}

export default function App() {
  return (
    <NavigationContainer>
      <MyDrawer />
    </NavigationContainer>
  );
}

更多关于 Drawer navigation 抽屉导航的 API,可以参阅 https://reactnavigation.org/docs/drawer-navigator#api-definition

18.3.Material Top Tabs Navigator(重点)

Material Top Tabs Navigator 翻译成中文叫做“顶部滑动选项卡导航”。要使用这种导航,首先还是需要先安装依赖,命令如下:

npm install @react-navigation/material-top-tabs react-native-tab-view

除了上面的依赖以外,还需要安装 react-native-pager-view。因为我们是使用 expo 搭建的项目,因此安装指令如下:

expo install react-native-pager-view

到这里,依赖安装就完毕了,下面是一个这种导航的简单示例:

import * as React from 'react';
import { Text, View } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';

function HomeScreen() {
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>Home!</Text>
    </View>
  );
}

function SettingsScreen() {
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>Settings!</Text>
    </View>
  );
}

const Tab = createMaterialTopTabNavigator();

export default function App() {
  return (
    <NavigationContainer>
      <Tab.Navigator>
        <Tab.Screen name="Home" component={HomeScreen} />
        <Tab.Screen name="Settings" component={SettingsScreen} />
      </Tab.Navigator>
    </NavigationContainer>
  );
}

18.3.1Material Top Tabs Navigator(开发使用)

import {createMaterialTopTabNavigator} from '@react-navigation/material-top-tabs';

const Tab = createMaterialTopTabNavigator();

  // tabsHeaderOptions
  const containerOptions: Object = {
    tabBarLabelStyle: {fontSize: 16, fontWeight: '500'},
    tabBarActiveTintColor: '#4E83FD',
    tabBarInactiveTintColor: '#333',
    tabBarStyle: {
      backgroundColor: '#fff',
      elevation: 0,
    },
    tabBarBounces: true,
    tabBarIndicatorStyle: {
      width: 20,
      backgroundColor: '#4E83FD',
      marginLeft: Dimensions.get('window').width / 4.5,
    },
  };

     <Tab.Navigator screenOptions={containerOptions}>
        <Tab.Screen name="Tab1">
          {() => (
              <ReceiveGoods
	              	dataList={dataList}
	                loading={loading}
	                navigation={navigation}
              />
          )}
        </Tab.Screen>
        <Tab.Screen name="Tab2">
          {() => (
              <ReceiveGoods
	                dataList={dataList}
	                loading={loading}
	                navigation={navigation}
              />
          )}
        </Tab.Screen>
      </Tab.Navigator>

更多关于 Material Top Tabs Navigator 顶部滑动选项卡导航的 API,可以参阅 https://reactnavigation.org/docs/material-top-tab-navigator#api-definition

十九、状态管理介绍

二十、Redux

二十一、其他第三方库


总结

以上就是今天要讲的内容,本文详细介绍了React Native详细知识和使用。
愿:平安顺遂!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值