ReactNative-实战-全-
ReactNative 实战(全)
原文:React Native in Action
译者:飞龙
第一部分
开始使用 React Native
第一章将通过介绍 React Native 是什么,它是如何工作的,它与 React 的关系,以及你何时可能想要使用 React Native(以及何时可能不使用)来帮助你入门。本章提供了 React Native 组件的概述,这些组件是 React Native 的核心。它以创建一个小的 React Native 项目结束。
第二章涵盖了状态和属性:它们是什么,如何工作,以及为什么在 React Native 应用程序开发中它们很重要。它还涵盖了 React 组件规范和 React 生命周期方法。
在第三章中,你将从零开始构建你的第一个 React Native 应用——待办事项应用。你还将了解如何在 iOS 和 Android 中使用开发者菜单进行调试,以及其他操作。
1
开始使用 React Native
本章涵盖
-
介绍 React Native
-
React Native 的优势
-
创建组件
-
创建一个入门项目
原生移动应用开发可能很复杂。开发者面临复杂的开发环境、冗长的框架和漫长的编译时间,开发高质量的原生移动应用并非易事。市场看到其份额的解决方案出现在场景中,试图解决原生移动应用开发的问题,并试图使其更容易。
这种复杂性的核心是跨平台开发的障碍。各种平台在本质上都是不同的,它们在开发环境、API 或代码方面共享的很少。正因为如此,我们必须为每个平台拥有独立的团队进行工作,这不仅成本高昂,而且效率低下。
但这是移动应用开发的一个激动人心的时期。我们正在见证移动开发领域的一个新范式,React Native 正处于这一转变的前沿,它改变了我们构建和工程化移动应用的方式。现在,我们可以使用单一的语言和单一团队来构建原生性能的跨平台应用以及网络应用。随着移动设备的兴起以及随之而来的人才需求增加,推动开发者薪资不断提高,React Native 带来了在所有平台上以极小的时间和成本交付高质量应用的能力,同时仍然提供高质量的用户体验和令人愉悦的开发者体验。
1.1 介绍 React 和 React Native
React Native 是一个使用 React JavaScript 库在 JavaScript 中构建原生移动应用的框架;React Native 代码编译成真正的原生组件。如果你不确定 React 是什么,它是一个由 Facebook 开源并在 Facebook 内部使用的 JavaScript 库。最初,它被用来构建网络应用的用户界面。它已经发展起来,现在也可以用来构建服务器端和移动应用(使用 React Native)。
React Native 有很多优点。除了得到 Facebook 的支持和开源之外,它还拥有一个庞大的、充满激情的社区。Facebook 群组,拥有数百万用户,也由 React Native 和 Facebook Ads Manager 提供支持。Airbnb、Bloomberg、Tesla、Instagram、Ticketmaster、SoundCloud、Uber、Walmart、Amazon 和 Microsoft 等公司都在投资或在生产中使用 React Native。
使用 React Native,开发者可以使用 JavaScript 构建原生视图并访问特定平台的原生组件。这使得 React Native 与其他混合应用框架(如 Cordova 和 Ionic)区分开来,后者将使用 HTML 和 CSS 构建的 web 视图打包成原生应用。相反,React Native 将 JavaScript 编译成真正的原生应用,可以访问特定平台的 API 和组件。类似的选择如 Xamarin 也采用相同的方法,但 Xamarin 应用是用 C#而不是 JavaScript 构建的。许多网页开发者都有 JavaScript 经验,这有助于从网页开发过渡到移动应用开发。
选择 React Native 作为移动应用框架有很多好处。因为应用直接渲染原生组件和 API,所以速度和性能比 Cordova 和 Ionic 等混合框架要好得多。使用 React Native,我们可以使用单一编程语言:JavaScript 来编写整个应用。我们可以重用大量代码,从而减少发布跨平台应用所需的时间。而且,招聘和寻找高质量的 JavaScript 开发者比招聘 Java、Objective C 或 Swift 开发者要容易得多,成本也更低,从而使得整个过程更加经济。
我们将在第二章深入探讨 React。在此之前,让我们先简要介绍几个核心概念。
1.1.1 一个基本的 React 类
组件是 React 或 React Native 应用的基本构建块。应用的入口点是一个需要并由其他组件构成组件。这些组件也可能需要其他组件,依此类推。
React Native 主要有两种组件类型:有状态和无状态。以下是一个使用 ES6 类的有状态组件示例:
class HelloWorld extends React.Component {
constructor() {
super()
this.state = { name: 'Chris' }
}
render () {
return (
<SomeComponent />
)
}
}
以下是一个无状态组件的示例:
const HelloWorld = () => (
<SomeComponent />
)
主要区别在于无状态组件不会连接到任何生命周期方法,也不持有自己的状态,因此任何要渲染的数据都必须作为属性(props)接收。我们将在第二章深入讲解生命周期方法,但在此我们先初步了解它们,并看看一个类。
列表 1.1 创建基本的 React Native 类
import React from 'react'
import { View, Text, StyleSheet } from 'react-native'
class HelloWorld extends React.Component {
constructor () { ①
super()
this.state = {
name: 'React Native in Action'
}
}
componentDidMount () { ②
console.log('mounted..')
}
render () { ③
return (
<View style={styles.container}>
<Text>{this.state.name}</Text>
</View>
)
}
}
const styles = StyleSheet.create({
container: {
marginTop: 100,
flex: 1
}
})
在文件顶部,你需要从'react'中引入React,以及从'react-native'中引入View、Text和StyleSheet。View是创建 React Native 组件和 UI 的基本构建块,可以将其视为 HTML 中的div元素。Text允许你创建文本元素,类似于 HTML 中的span标签。StyleSheet允许你创建用于应用程序中的样式对象。这两个包(react和react-native)作为 npm 模块提供。
当组件首次加载时,你在构造函数中设置一个带有name属性的state对象。为了使 React Native 应用程序中的数据动态化,它需要要么在状态中设置,要么作为 props 传递。在这里,你在构造函数中设置状态,因此可以通过调用
this.setState({
name: 'Some Other Name'
})
这将重新渲染组件。在状态中设置变量允许你在组件的其他地方更新值。
然后调用render:它检查 props 和 state,然后必须返回一个 React Native 元素、null或false。如果你有多个子元素,它们必须被包装在一个父元素中。在这里,组件、样式和数据被组合起来,创建出将被渲染到 UI 中的内容。
生命周期中的最后一个方法是componentDidMount。如果你需要进行 API 调用或 AJAX 请求来重置状态,这通常是最佳位置。最后,UI 被渲染到设备上,你可以看到结果。
1.1.2 React 生命周期
当创建 React Native 类时,会实例化你可以挂钩的方法。这些方法被称为生命周期方法,我们将在第二章中深入探讨。列表 1.1 中的方法是constructor、componentDidMount和render,但还有一些其他方法,它们都有自己的用途。
生命周期方法同步发生,有助于管理组件的状态,并在每个步骤中执行代码,如果你愿意的话。唯一必需的生命周期方法是render;所有其他都是可选的。当你使用 React Native 时,你基本上使用的是与 React 相同的生命周期方法和规范。
1.2 你将学到什么
在这本书中,我们将涵盖你使用 React Native 框架构建健壮的 iOS 和 Android 移动应用所需了解的所有内容。因为 React Native 是用 React 库构建的,所以我们将从第二章开始,详细解释 React 的工作原理。
然后我们将介绍样式,涉及框架中大多数可用的样式属性。因为 React Native 使用 flexbox 进行 UI 布局,我们将深入探讨 flexbox 的工作原理,并讨论所有 flexbox 属性。如果你在 CSS 中使用了 flexbox 进行网页布局,所有这些对你来说都会很熟悉,但请注意,React Native 使用的 flexbox 实现并不完全相同。
然后,我们将详细介绍框架自带的大量原生组件,并说明每个组件是如何工作的。在 React Native 中,组件基本上是一段提供特定功能或 UI 元素的代码块,并且可以轻松地用于应用程序中。组件在本书中被广泛涵盖,因为它们是 React Native 应用程序的基本构建块。
实现导航的方式有很多,每种方式都有其独特的细微差别、优点和缺点。我们将深入讨论导航,并介绍如何使用最重要的导航 API 构建健壮的导航。我们将涵盖不仅包括 React Native 自带的本地导航 API,还包括通过 npm 可用的几个社区项目。
接下来,我们将深入探讨 React Native 中可用的跨平台和平台特定 API,以及它们是如何工作的。然后,你将开始使用网络请求、AsyncStorage(一种本地存储形式)、Firebase 和 WebSocket 等方式处理数据。然后,我们将深入研究不同的数据架构以及它们如何处理应用程序的状态。最后,我们将探讨测试以及在 React Native 中测试的几种不同方法。
1.3 你应该了解
为了充分利用这本书,你应该具备从初学者到中级水平的 JavaScript 知识。你的大部分工作将在命令行中完成,因此需要了解如何使用命令行的基本知识。你还应该了解 npm 是什么以及它在基本层面上是如何工作的。如果你将构建 iOS 应用,对 Xcode 的基本理解将有益于加快进度,但不是必需的。同样,如果你为 Android 构建应用,对 Android Studio 的基本理解将有益于加快进度,但也不是必需的。
对 JavaScript 编程语言 ES2015 版本中实现的新 JavaScript 特性的基本知识有益,但不是必需的。对 MVC 框架和单页架构的一些概念性知识也很好,但不是必需的。
1.4 理解 React Native 的工作原理
让我们通过讨论 JSX、线程模型、React、单向数据流等内容来了解 React Native 的工作原理。
1.4.1 JSX
React 和 React Native 都鼓励使用 JSX。JSX 实际上是对 JavaScript 的一种语法扩展,看起来类似于 XML。你可以不使用 JSX 来构建 React Native 组件,但 JSX 使得 React 和 React Native 更易于阅读和维护。JSX 可能一开始看起来有些奇怪,但它非常强大,大多数人最终都会喜欢上它。
1.4.2 线程
所有与原生平台交互的 JavaScript 操作都在一个单独的线程中完成,允许用户界面以及任何动画平滑地执行。这个线程是 React 应用程序所在的地方,也是所有 API 调用、触摸事件和交互处理的地方。当原生支持的组件发生变化时,更新会被批量发送到原生端。这发生在事件循环的每次迭代结束时。对于大多数 React Native 应用程序,业务逻辑在 JavaScript 线程上运行。
1.4.3 React
React Native 的一个显著特点是它使用 React。React 是一个由 Facebook 支持的开放源代码 JavaScript 库。它最初是为了构建应用程序和解决网页上的问题而设计的。自从发布以来,这个框架已经变得极其流行,许多知名公司都利用了它的快速渲染、可维护性和声明式 UI 等特性。
传统的 DOM 操作在性能上既慢又昂贵,应该尽量减少。React 通过所谓的虚拟 DOM绕过了传统的 DOM:基本上,这是实际 DOM 在内存中的副本,只有当比较虚拟 DOM 的新版本和旧版本时才会发生变化。这最小化了实现新状态所需的 DOM 操作数量。
1.4.4 单向数据流
React 和 React Native 强调单向,或单程,的数据流。由于 React Native 应用程序的构建方式,这种单向数据流很容易实现。
1.4.5 Diffing
React 将 diffing 的概念应用到原生组件上。它将你的 UI 发送到主线程,用原生组件渲染,所需的数据量最小。UI 基于状态声明式渲染,React 使用 diffing 通过桥梁发送必要的更改。
1.4.6 组件化思维
在 React Native 中构建 UI 时,将你的应用程序视为由一组组件组成是有用的。思考页面是如何设置的,你已经在概念上这样做,但使用像header、footer、body、sidebar等概念、名称或类名。在 React Native 中,你可以为这些组件命名,使其对你和其他可能使用你代码的开发者有意义,这使得将新人引入项目或转交给他人变得容易。
假设一位设计师给了你图 1.1 中显示的示例原型。让我们思考如何将这个概念转化为组件。

图 1.1 示例应用程序设计
首先要做的是在心理上将 UI 元素拆分成它们所代表的内容。示例原型中有一个标题栏,标题栏内包含标题和菜单按钮。标题栏下方是标签栏,标签栏内包含三个单独的标签。浏览原型其余部分,思考其他项目是什么。你正在识别的项目将被转换为组件。这是你在使用 React Native 构建 UI 时应该考虑的方法:将 UI 中的常见元素拆分成可重用组件,并相应地定义它们的接口。当你未来需要某个元素时,它将可供重用。
将 UI 元素拆分成可重用的组件有利于代码复用,同时也使得代码更具声明性和可理解性。例如,用 12 行代码实现页脚,元素可以命名为 footer。查看这样构建的代码,推理起来更容易,可以确切地知道正在发生什么。
图 1.2 展示了如何将 图 1.1 中的设计拆分成我刚才描述的样子。名称可以是任何对你有意义的名称。一些项目被分组在一起——我逻辑上单独分离了项目,并在概念上将组件分组。

图 1.2 将应用程序结构拆分成单独的组件
接下来,让我们看看使用实际的 React Native 代码会是怎样的。首先,让我们看看主要 UI 元素在页面上的显示方式:
<Header />
<TabBar />
<ProjectList />
<Footer />
接下来,让我们看看子元素的外观:
TabBar:
<TabBarItem />
<TabBarItem />
<TabBarItem />
ProjectList:
// Add a Project component for each project in the list:
<Project />
我使用了 图 1.2 中声明的名称,但它们可以是任何对你有意义的名称。
1.5 认可 React Native 的优势
如前所述,React Native 的主要优势之一是它使用 React。React,就像 React Native 一样,是一个由 Facebook 支持的开源项目。截至本文写作时,React 在 GitHub 上有超过 100,000 个星标和 1,100 多个贡献者——这表明项目受到了很多关注和社区参与,作为开发者或项目经理,更容易对其下注。由于 React 由 Facebook 开发、维护和使用,它拥有世界上最优秀的工程师之一来监督它,推动它向前发展,并添加新功能,因此它可能不会很快消失。
1.5.1 开发者可用性
随着原生移动开发者的成本上升和可用性下降,React Native 带着与原生开发相比的关键优势进入市场:它利用了现有的丰富才华横溢的 Web 和 JavaScript 开发者,并为他们提供了一个新的平台,让他们可以在不学习新语言的情况下构建应用程序。
1.5.2 开发者生产力
传统上,要构建跨平台移动应用程序,你需要一个 Android 团队和一个 iOS 团队。React Native 允许你使用单一编程语言 JavaScript(以及可能是一个单一团队)来构建 Android、iOS 和(很快)Windows 应用程序,这极大地减少了开发时间和开发成本,同时提高了生产力。作为一名原生开发者,来到这样一个平台的好处是你不再仅仅局限于 Android 或 iOS 开发者,这为许多机会打开了大门。这对 JavaScript 开发者来说也是个好消息,因为它允许他们在切换网页和移动项目时,能够保持一个统一的心态。这对那些传统上在 Android 和 iOS 之间分裂的团队来说也是一个胜利,因为他们现在可以基于单一代码库共同工作。为了强调这些观点,如果你使用 Redux(在第十二章中讨论)这样的工具,你可以分享你的数据架构不仅跨平台,还可以在网络上进行。
1.5.3 性能
如果你遵循其他跨平台解决方案,你可能已经熟悉了像 PhoneGap、Cordova 和 Ionic 这样的解决方案。尽管这些也是可行的解决方案,但普遍认为性能还没有达到原生应用程序的水平。这正是 React Native 也表现出色的地方,因为其性能通常与使用 Objective-C/Swift 或 Java 构建的原生移动应用程序没有明显区别。
1.5.4 单向数据流
单向数据流将 React 和 React Native 与其他大多数 JavaScript 框架以及任何 MVC 框架区分开来。React 从顶级组件开始,实现了一路到底的单向数据流(参见图 1.3)。这使得应用程序更容易推理,因为数据层有一个单一的真实来源,而不是散布在应用程序中。我们将在本书的后面更详细地探讨这一点。

图 1.3 单向数据流的工作原理
1.5.5 开发者体验
开发者体验是 React Native 的一个重大优势。如果你曾经为网页开发过,你会知道浏览器快速的重载时间。网页开发没有编译步骤:只需刷新屏幕,你的更改就会出现。这与原生开发的漫长编译时间大相径庭。Facebook 决定开发 React Native 的一个原因是为了克服使用原生 iOS 和 Android 构建工具时 Facebook 应用程序漫长的编译时间。为了进行小的 UI 更改或其他更改,Facebook 开发者不得不等待很长时间,直到程序编译完成才能看到结果。漫长的编译时间会导致生产力下降和开发者成本增加。React Native 通过提供与网页、Chrome 和 Safari 调试工具相同的快速重载时间来解决这一问题,使得调试体验感觉就像在网页上一样。
React Native 还内置了一个名为 热重载 的功能。这意味着什么?好吧,在开发应用程序时,想象一下你需要点击几次才能到达你正在开发的地方。在使用热重载时,当你进行代码更改时,你不需要重新加载并通过应用程序点击回到当前状态。使用这个功能,你保存文件,应用程序只会重新加载你更改的组件,立即给你反馈并更新 UI 的当前状态。
1.5.6 转译
转译 通常是指一个名为 转译器 的工具将用一种编程语言编写的源代码转换为另一种语言的等效代码。随着新的 ECMAScript 特性和标准的兴起,转译已经扩展到包括将某些语言(在这种情况下是 JavaScript)的新版本和尚未实现的功能转换为转译后的标准 JavaScript,使得代码可以在只能处理语言较旧版本的平台上使用。
React Native 使用 Babel 来执行这个转译步骤,并且默认内置。Babel 是一个开源工具,可以将最前沿的 JavaScript 语言特性转译成今天可以使用的代码。你不需要等待语言特性被提出、批准然后实施的过程,你可以在它们进入 Babel 的时候就开始使用它们,这通常非常快。JavaScript 类、箭头函数和对象解构都是 ES2015 强大特性的例子,这些特性尚未在所有浏览器和运行时中实现;但使用 Babel 和 React Native,你可以今天就开始使用它们,无需担心它们是否能够工作。如果你喜欢使用最新的语言特性,你可以使用相同的转译过程来开发 Web 应用程序。
1.5.7 效率和生产力
原生移动开发变得越来越昂贵,因此能够跨平台和堆栈交付应用程序的工程师将变得越来越有价值且需求增加。一旦 React Native 或者类似的技术变得主流,使用单个框架开发桌面、Web 以及移动应用程序,那么工程团队的重组和重新思考将会发生。不再是开发者专门化于某个平台,如 iOS 或 Web,他们将负责跨平台的功能。在这个跨平台和跨堆栈工程团队的新时代,交付原生移动、Web 和桌面应用程序的开发者将更加高效和高效,因此他们可以要求比只能交付 Web 应用程序的传统 Web 开发者更高的工资。
雇佣开发者进行移动开发的公司,使用 React Native 可以获得最大的好处。所有内容都使用一种语言编写,这使得招聘变得更加容易和便宜。当团队在同一个页面上,使用单一技术工作时,生产力也会大幅提升,这简化了协作和知识共享。
1.5.8 社区
React 社区,以及由此扩展的 React Native 社区,是我曾经互动过的最开放和最有帮助的群体之一。当我遇到我在网上或 Stack Overflow 上搜索无法解决的问题时,我直接联系了团队成员或社区成员,并且得到了积极的反馈和帮助。
1.5.9 开源
React Native 是开源的。这带来了许多好处。首先,除了 Facebook 团队外,还有数百名开发者为 React Native 做出贡献。在开源软件中,比在专有软件中更快地指出错误,因为专有软件只有特定团队的工作人员负责错误修复和改进。开源通常更接近用户的需求,因为用户可以参与使软件成为他们想要的样子。考虑到购买专有软件、许可费用和支持成本,在衡量价格时,开源也更具优势。
立即更新
传统上,当发布应用的新版本时,您必须依赖应用商店的审批流程和日程安排。这个过程漫长而繁琐,可能需要长达两周的时间。即使是一个极小的更改,也是痛苦的,并且需要发布应用的新版本。
React Native 以及混合应用框架允许您直接将移动应用更新部署到用户的设备上,无需经过应用商店的审批流程。如果您习惯了网络和它提供的快速发布周期,现在您可以使用 React Native 和其他混合应用框架做到同样的事情。
其他构建跨平台移动应用的解决方案
React Native 并不是构建跨平台移动应用的唯一选择。还有多种其他选项可用,其中主要的是 Cordova、Xamarin 和 Flutter:
-
Cordova 实际上是一个围绕网络应用的本地壳,允许开发者访问应用内的本地 API。与传统的网络应用不同,Cordova 应用可以部署到 App Store 和 Google Play Store。使用类似 Cordova 的好处是,如果您已经是网络开发者,那么您不需要学习更多:您可以使用 HTML、JavaScript、CSS 以及您选择的 JavaScript 框架。Cordova 的主要缺点是,您将很难匹配 React Native 提供的性能和流畅的用户界面:您依赖于 DOM,因为您主要使用的是网络技术。
-
Xamarin 是一个框架,允许开发者使用 C# 编写的单一代码库来构建 iOS、Android、Windows 和 macOS 应用程序。Xamarin 根据目标平台的不同,以不同的方式编译成原生应用程序。Xamarin 提供了免费层,让开发者可以构建和部署移动应用程序,以及针对更大或企业公司的付费层。由于它不像 React Native 和 Cordova 那样与 Web 技术相似,Xamarin 可能会更吸引原生开发者。
-
Flutter 是由 Google 开源的一个框架,它使用 Dart 编程语言来构建在 iOS 和 Android 平台上运行的应用程序。
1.6 React Native 的缺点
既然我们已经讨论了使用 React Native 的好处,让我们看看一些可能不希望选择该框架的原因和情况。首先,与原生 iOS、Android 和 Cordova 等其他平台相比,React Native 仍然不够成熟。与原生 iOS 或 Cordova 的功能对等性尚未实现。大多数功能现在都已内置,但有时你可能需要尚未提供的功能,这意味着你必须深入研究原生代码来构建它,雇佣某人来做这件事,或者不实现该功能。
另一个需要考虑的事实是,如果你不熟悉 React,你和/或你的团队必须学习一项全新的技术。大多数人认为 React 很容易上手;但如果你已经熟练掌握 Angular 和 Ionic,例如,并且你有一个即将到来的应用程序截止日期,那么选择你已经熟悉的技术而不是花时间去学习和培训团队使用新技术可能是明智的。除了学习 React 和 React Native 之外,你还必须熟悉 Xcode 和 Android 开发环境,这可能需要一些时间来适应。
最后,React Native 是建立在现有平台 API 之上的一个抽象层。当 iOS、Android 和其他未来平台发布新版本时,可能会有一个时期 React Native 在新功能上落后,迫使你必须构建自定义实现来与这些新 API 交互,或者等待 React Native 恢复与新发布的功能对等性。
1.7 创建和使用基本组件
组件是 React Native 的基本构建块,它们在功能和类型上可能有所不同。常用案例中的组件包括按钮、标题、页脚和导航组件。它们的类型可以从包含自身状态和功能的完整视图,到仅从其父组件接收所有属性的单个无状态组件不等。
1.7.1 组件概述
正如我所说的,React Native 的核心是组件的概念。组件是数据和 UI 元素的集合,它们构成了视图,最终构成了应用程序。React Native 提供了内置组件,本书中描述为 原生组件,但您也可以使用框架构建自定义组件。我们将深入探讨如何构建、创建和使用组件。
如前所述,React Native 组件是使用 JSX 构建的。表 1.1 展示了 React Native 中 JSX 与 HTML 的几个基本示例。如您所见,JSX 看起来与 HTML 或 XML 类似。
表 1.1 JSX 组件与 HTML 元素对比
| 组件类型 | HTML | React Native JSX |
|---|---|---|
| 文本 |
`<span>Hello World</span>`
|
`<Text>Hello World</Text>`
|
| 查看 |
|---|
`<div>`
`<span>Hello World 2</span>`
`</div>`
|
`<View>`
`<Text>Hello World 2</Text>`
`</View>`
|
| 可触摸高亮 |
|---|
`<button>`
`<span>Hello World 2</span>`
`</button >`
|
`<TouchableHighlight>`
`<Text>Hello World 2</Text>`
`</TouchableHighlight>`
|
1.7.2 原生组件
框架提供了一些原生组件,例如 View、Text 和 Image 等。您可以使用这些原生组件作为构建块来创建组件。例如,您可以使用以下标记使用 React Native 的 TouchableHighlight 和 Text 组件创建一个 Button 组件。
列表 1.4 创建 Button 组件
import { Text, TouchableHighlight } from 'react-native'
const Button = () => (
<TouchableHighlight>
<Text>Hello World</Text>
</TouchableHighlight>
)
export default Button
然后,您可以导入并使用新的按钮。
列表 1.5 导入和使用 Button 组件
import React from 'react'
import { Text, View } from 'react-native'
import Button from './components/Button'
const Home = () => (
<View>
<Text>Welcome to the Hello World Button!</Text>
<Button />
</View>
)
接下来,我们将介绍组件的基本概念,组件如何适应工作流程,以及构建组件的常见用例和设计模式。
1.7.3 组件组合
组件通常使用 JSX 组成,但也可以使用 JavaScript 组成。在本节中,您将以几种不同的方式创建组件,以查看所有选项。您将创建以下组件:
<MyComponent />
此组件将“Hello World”输出到屏幕上。现在,让我们看看如何构建这个基本组件。您将用于构建此自定义组件的唯一开箱即用的组件是前面讨论过的 View 和 Text 元素。记住,View 组件类似于 HTML <div>,而 Text 组件类似于 HTML <span>。
让我们看看创建组件的几种方法。整个应用程序的组件定义不必完全一致,但通常建议您保持一致,并在整个应用程序中遵循相同的模式来定义类。
createClass 语法(ES5,JSX)
这是使用 ES5 语法创建 React Native 组件的方法。您可能仍然会在一些较旧的文档和示例中看到这种语法,但它现在不再被使用,并且已被弃用。本书的其余部分将专注于 ES2015 类语法,但在此处将回顾 createClass 语法,以防您在较旧的代码中遇到它:
const React = require('react')
const ReactNative = require('react-native')
const { View, Text } = ReactNative
const MyComponent = React.createClass({
render() {
return (
<View>
<Text>Hello World</Text>
</View>)
}
})
类语法(ES2015,JSX)
创建有状态的 React Native 组件的主要方法是使用 ES2015 类。这是您将在本书的其余部分创建有状态组件的方式,并且现在是社区和 React Native 创建者推荐的方法:
import React from 'react'
import { View, Text } from ‘react-native’
class MyComponent extends React.Component {
render() {
return (
<View>
<Text>Hello World</Text>
</View>)
}
}
无状态(可重用)组件(JSX)
自从 React 0.14 版本发布以来,我们就有能力创建无状态组件。我们还没有深入研究状态,但请记住,无状态组件基本上是纯函数,不能修改自己的数据,也不包含自己的状态。这种语法比class或createClass语法更简洁:
import React from 'react'
import { View, Text } from 'react-native'
const MyComponent = () => (
<View>
<Text>Hello World</Text>
</View>
)
or
import React from 'react'
import { View, Text } from 'react-native'
function MyComponent () {
return <View><Text>HELLO FROM STATELESS</Text></View>
}
createElement (JavaScript)
React.createElement很少使用,你可能永远不需要使用这种语法来创建 React Native 元素。但如果你需要更多控制创建组件的方式,或者你在阅读别人的代码时,它可能会很有用。它还会让你了解 JavaScript 如何编译 JSX。React.createElement接受几个参数:
React.createElement(type, props, children) {}
让我们逐一了解它们:
-
type—你想要渲染的元素 -
props—你希望组件拥有的任何属性 -
children—子组件或文本
在以下示例中,你将视图作为第一个参数传递给React.createElement的第一个实例,将一个空对象作为第二个参数,并将另一个元素作为最后一个参数。在第二个实例中,你将文本作为第一个参数,将一个空对象作为第二个参数,并将“Hello”作为最后一个参数:
class MyComponent extends React.Component {
render() {
return (
React.createElement(View, {},
React.createElement(Text, {}, "Hello")
)
)
}
}
这与以下声明组件的方式相同:
class MyComponent extends React.Component {
render () {
return (
<View>
<Text>Hello</Text>
</View>
)
}
}
1.7.4 可导出组件
接下来,让我们看看另一个更深入的 React Native 组件实现。你将创建一个可以导出并在另一个文件中使用的完整组件:
import React, { Component } from 'react'
import {
Text,
View
} from 'react-native'
class Home extends Component {
render() {
return (
<View>
<Text>Hello from Home</Text>
</View>)
}
}
export default Home
让我们逐一了解组成这个组件的各个部分,并讨论正在发生的事情。
导入
以下代码导入了 React Native 变量声明:
import React, { Component } from 'react'
import {
Text,
View
} from 'react-native'
这里,你使用默认导入直接从 React 库中导入 React,并使用命名导入从 React 库中导入Component。你还使用命名导入将Text和View拉入你的文件。
使用 ES5 的import语句看起来像这样:
var React = require('react')
如果不使用命名导入,这个语句看起来像这样:
import React = from 'react'
const Component = React.Component
import ReactNative from 'react-native'
const Text = ReactNative.Text
const View = ReactNative.View
import语句用于导入已从另一个模块、文件或脚本中导出的函数、对象或变量。
组件声明
以下代码声明了组件:
class Home extends Component { }
这里你通过扩展它并命名为Home来创建一个 React Native Component类的新实例。之前,你声明了React.Component;现在你只是声明了Component,因为你已经在对象解构语句中导入了Component元素,这样你就可以访问Component,而不是必须调用React.Component。
渲染方法
接下来,看看render方法:
render() {
return (
<View>
<Text>Hello from Home</Text>
</View>)
}
组件的代码在render方法中执行,return语句之后的内容返回的是屏幕上渲染的内容。当调用render方法时,它应该返回一个单一子元素。任何在render函数外部声明的变量或函数都可以在这里执行。如果你需要进行任何计算,可以使用状态或属性声明任何变量,或者运行不操作组件状态的任何函数,你可以在render方法和return语句之间这样做。
导出
现在,将组件导出以在应用程序的其他地方使用:
export default Home
如果你想在同一文件中使用组件,你不需要导出它。在声明之后,你可以在文件中使用它,或者将其导出到其他文件中使用。你也可以使用module.exports = 'Home',这是 ES5 语法。
1.7.5 组合组件
让我们看看如何组合组件。首先,在单个文件中创建Home、Header和Footer组件。首先创建Home组件:
import React, { Component } from 'react'
import {
Text,
View
} from 'react-native'
class Home extends Component {
render() {
return (
<View>
</View>)
}
}
在同一文件中,在Home类声明下方,构建一个Header组件:
class Header extends Component {
render() {
return <View>
<Text>HEADER</Text>
</View>
}
}
这看起来不错,但让我们看看如何将Header重写为无状态组件。我们将在本书的后面深入讨论何时以及为什么使用无状态组件比常规 React Native 类更好。正如你将开始看到的那样,当你使用无状态组件时,语法和代码要干净得多:
const Header = () => (
<View>
<Text>HEADER</Text>
</View>
)
现在,将Header插入到Home组件中:
class Home extends Component {
render() {
return (
<View>
<Header />
</View>
)
}
}
创建一个Footer和一个Main视图:
const Footer = () => (
<View>
<Text>Footer</Text>
</View>
)
const Main = () => (
<View>
<Text> Main </Text>
</View>
)
现在,将这些组件添加到你的应用程序中:
class Home extends Component {
render() {
return (
<View>
<Header />
<Main />
<Footer />
</View>
)
}
}
你刚才编写的代码非常声明式,这意味着它是这样编写的,它描述了你想要做什么,并且单独理解起来很容易。这是你对如何在 React Native 中创建组件和视图的高级概述,但应该能给你一个关于基础知识如何工作的良好概念。
1.8 创建起始项目
现在我们已经详细介绍了许多关于 React Native 的内容,让我们深入一些代码。我们将专注于使用 React Native CLI 构建应用程序,但你也可以使用 Create React Native App CLI 创建新项目。
1.8.1 创建 React Native App CLI
你可以使用 Create React Native App CLI 创建 React Native 项目,这是一个由 React 社区 GitHub 仓库维护的项目生成器,主要由 Expo 团队维护。Expo 创建了 React Native App 项目,作为一种让开发者能够无需担心安装所有与使用 CLI 运行 React Native 项目相关的原生 SDK 的方式,快速开始使用 React Native。
要使用 Create React Native App 创建新项目,首先安装 CLI:
npm install -g create-react-native-app
这是使用命令行中的create-react-native-app创建新项目的方法:
create-react-native-app myProject
1.8.2 React Native CLI
在我们继续之前,请检查这本书的附录以验证您是否已在您的机器上安装了必要的工具。如果您没有安装所需的 SDK,您将无法继续使用 React Native CLI 构建您的第一个项目。
要开始使用 React Native 启动项目和 React Native CLI,请打开命令行,然后创建并导航到一个空目录。一旦到达那里,通过输入以下内容全局安装 react-native CLI:
npm install -g react-native-cli
在 React Native 安装到您的机器上后,您可以通过输入react-native init后跟项目名称来初始化一个新的项目:
react-native init myProject
myProject可以是您选择的任何名称。CLI 将在您所在的任何目录中启动一个新的项目。在文本编辑器中打开项目。
首先,让我们看看这个过程为您生成的主文件和文件夹:
-
android —此文件夹包含所有 Android 平台特定的代码和依赖项。除非您正在实现自定义桥接至 Android 或安装需要某种深度配置的插件,否则您不需要进入此文件夹。
-
ios —此文件夹包含所有 iOS 平台特定的代码和依赖项。除非您正在实现自定义桥接至 iOS 或安装需要某种深度配置的插件,否则您不需要进入此文件夹。
-
node_modules —React Native 使用npm(node 包管理器)来管理依赖项。这些依赖项在.package.json 文件中标识和版本化,并存储在 node_modules 文件夹中。当您从 npm/node 生态系统安装任何新包时,它们将在这里。这些可以使用 npm 或 yarn 安装。
-
.flowconfig —Flow(也被 Facebook 开源)为 JavaScript 提供类型检查。如果您熟悉 TypeScript,Flow 就像 TypeScript 一样。此文件是 flow 的配置文件,如果您选择使用它。
-
.gitignore —这是存储您不想在版本控制中包含的任何文件路径的地方。
-
.watchmanconfig —Watchman 是 React Native 用来监视文件并记录它们何时更改的文件监视器。这是 Watchman 的配置文件。除非在罕见的使用情况下,否则不需要对此文件进行更改。
-
index.js —这是应用程序的入口点。在此文件中,导入 App.js 并调用
AppRegistry.registerComponent,初始化应用程序。 -
App.js —这是在 index.js 中使用的默认主导入,包含基本项目。您可以通过删除此文件并在 index.js 中替换主导入来更改它。
-
package.json —此文件包含您的 npm 配置。当您使用 npm 安装文件时,您可以将其保存为依赖项。您还可以设置脚本来运行不同的任务。
以下列表显示了 App.js。
列表 1.6 App.js
/**
* Sample React Native App
* https://github.com/facebook/react-native
* @flow
*/
import React, { Component } from 'react';
import {
Platform,
StyleSheet,
Text,
View
} from 'react-native';
const instructions = Platform.select({
ios: 'Press Cmd+R to reload,\n' +
'Cmd+D or shake for dev menu',
android: 'Double tap R on your keyboard to reload,\n' +
'Shake or press menu button for dev menu',
});
export default class App extends Component<{}> {
render() {
return (
<View style={styles.container}>
<Text style={styles.welcome}>
Welcome to React Native!
</Text>
<Text style={styles.instructions}>
To get started, edit App.js
</Text>
<Text style={styles.instructions}>
{instructions}
</Text>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
welcome: {
fontSize: 20,
textAlign: 'center',
margin: 10,
},
instructions: {
textAlign: 'center',
color: '#333333',
marginBottom: 5,
},
});
这段代码看起来与我们在上一节中讨论的内容非常相似。有几个您尚未见过的新的项目:
StyleSheet
Platform
Platform 是一个 API,它允许你检测你正在运行的当前操作系统类型:Web、iOS 或 Android。
StyleSheet 是类似于 CSS 样式表的抽象。在 React Native 中,你可以声明样式,要么是内联的,要么使用样式表。正如你在第一个视图中看到的,容器样式被声明:
<View style={styles.container}>
这直接对应于
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
}
在 index.js 文件的底部,你可以看到
AppRegistry.registerComponent('myProject', () => App);
这是运行所有 React Native 应用的 JavaScript 入口点。在 index 文件中,你将唯一调用此函数。应用的主组件应该通过 AppRegistry.registerComponent 进行注册。原生系统可以加载应用的包并在准备就绪时运行应用。

图 1.4 React Native 入门项目:在模拟器上运行入门项目后你应该看到的内容
现在我们已经了解了文件中的内容,请在你的 iOS 模拟器或 Android 模拟器上运行项目(参见 图 1.4)。在包含“欢迎使用 React Native”文本的文本元素中,输入“欢迎使用 Hello World!”或其他你选择的文本。刷新屏幕,你应该能看到你的更改。
摘要
-
React Native 是一个使用 React JavaScript 库在 JavaScript 中构建原生移动应用的框架。
-
React Native 的一些优势包括其性能、开发者体验、使用单一语言构建跨平台应用的能力、单向数据流和社区。你可能会考虑使用 React Native 而不是混合应用主要是因为其性能,以及使用 Native 而不是 React Native 主要是因为开发者体验和单一语言的跨平台能力。
-
JSX 是一个预处理步骤,它为 JavaScript 添加了类似 XML 的语法。你可以在 React Native 中使用 JSX 创建 UI。
-
组件是 React Native 的基本构建块。它们的功能和类型可能不同。你可以创建自定义组件以实现常见的设计元素。
-
需要使用 JavaScript 类通过扩展
React.Component类来创建需要状态或生命周期方法的组件。 -
无状态组件可以通过为不需要维护自身状态的组件创建更少的样板代码来创建。
-
可以通过组合较小的子组件来创建较大的组件。
2
理解 React
本章 涵盖
-
状态是如何工作的以及为什么它很重要
-
属性是如何工作的以及为什么它们很重要
-
理解 React 组件规范
-
实现 React 生命周期方法
现在我们已经了解了基础知识,是时候深入探讨构成 React 和 React Native 的其他一些基本组成部分了。我们将讨论如何管理状态和数据,以及数据是如何在应用中传递的。我们还将通过演示如何在组件之间传递属性(props)以及如何从上到下操作这些属性来进一步深入。
在你掌握了关于状态和属性的知识后,我们将更深入地探讨如何使用内置的 React 生命周期方法。这些方法允许你在组件创建或销毁时执行某些操作。理解它们是理解 React 和 React Native 的工作原理以及如何充分利用框架的关键。生命周期方法也是 React 和 React Native 中概念上最大的部分。
2.1 使用状态管理组件数据
在 React 或 React Native 组件中创建和管理数据的一种方式是使用状态。组件创建时声明状态,其结构是一个普通的 JavaScript 对象。状态可以在组件内部使用名为 setState 的函数进行更新,我们将在稍后深入了解。
处理数据的另一种方式是使用属性。属性在组件创建时作为参数传递;与状态不同,它们不能在组件内部更新。
2.1.1 正确操作组件状态
状态 是组件管理的一组值。React 将 UI 视为简单的状态机。当组件使用 setState 函数改变状态时,React 会重新渲染该组件。如果任何子组件作为属性继承了此状态,那么所有子组件也会被重新渲染。
当使用 React Native 构建应用程序时,理解状态的工作原理是基础,因为状态决定了有状态组件的渲染和行为。组件状态允许你创建动态和交互式的组件。在区分状态和属性时,需要理解的主要点是状态是可变的,而属性是不可变的。
设置 初始状态
状态在组件创建时通过构造函数或属性初始化器初始化。一旦状态初始化,它就作为 this.state 在组件中可用。以下列表展示了示例。
列表 2.1 使用属性初始化器设置状态
import React from 'react'
class MyComponent extends React.Component {
state = {
year: 2016,
name: 'Nader Dabit',
colors: ['blue']
}
render() {
return (
<View>
<Text>My name is: { this.state.name }</Text>
<Text>The year is: { this.state.year }</Text>
<Text>My colors are { this.state.colors[0] }</Text>
</View>
)
}
`}`
constructor 函数在 JavaScript 类实例化时被调用,如以下列表所示。这不是一个 React 生命周期方法,而是一个常规的 JavaScript 类方法。
列表 2.2 使用构造函数设置状态
import React {Component} from 'react'
class MyComponent extends Component {
constructor(){
super()
this.state = {
year: 2016,
name: 'Nader Dabit',
colors: ['blue']
}
}
render() {
return (
<View>
<Text>My name is: { this.state.name }</Text>
<Text>The year is: { this.state.year }</Text>
<Text>My colors are { this.state.colors[0] }</Text>
</View>
)
}
}
构造函数和属性初始化器的工作方式完全相同,你使用哪种方法取决于个人喜好。
更新状态
可以通过调用 this.setState(object) 来更新状态,传入一个包含你想要使用的新状态的对象。setState 会将前一个状态与当前状态合并,所以如果你只传入一个单项(键值对),其余的状态将保持不变,而状态中的新项将被覆盖。
让我们看看如何使用 setState(参见列表 2.3)。为此,我们将引入一个新的方法,一个名为 onPress 的触摸处理程序。onPress 可以在几种“可触摸”的 React Native 组件上调用,但在这里你将把它附加到一个 Text 组件上,以便从这个基本示例开始。当文本被按下时,你将调用一个名为 updateYear 的函数,以使用 setState 更新状态。这个函数将在 render 函数之前定义,因为通常最好在 render 方法之前定义任何自定义方法,但请注意,函数定义的顺序不会影响实际的功能。
列表 2.3 更新状态
import React {Component} from 'react'
class MyComponent extends Component {
constructor(){
super()
this.state = {
year: 2016,
}
}
updateYear() {
this.setState({
year: 2017
})
}
render() {
return (
<View>
<Text
onPress={() => this.updateYear()}>
The year is: { this.state.year }
</Text>
</View>
)
}
}

图 2.1 setState 的流程,箭头指示文本元素被按下时。在构造函数中,状态 year 属性被初始化为 2016。每次按下文本时,状态 year 属性被设置为 2017。
图 2.1 展示了每次按下列表 2.3 中的文本元素时状态是如何更新的。每次调用 setState,React 都会重新渲染组件(再次调用 render 方法)以及任何子组件。调用 this.setState 是改变状态变量并再次触发 render 方法的途径,因为直接改变状态变量不会触发组件的重新渲染,因此 UI 中不会看到任何变化。初学者常见的错误是直接更新状态变量。例如,以下代码在尝试更新状态时不起作用——state 对象被更新了,但由于没有调用 setState 且组件没有重新渲染,UI 不会更新:
class MyComponent extends Component {
constructor(){
super()
this.state = {
year: 2016,
}
}
updateYear() {
this.state.year = 2017
}
render() {
return (
<View>
<Text
onPress={() => this.updateYear()}>
The year is: { this.state.year }
</Text>
</View>
)
}
}
但在 React 中有一个方法可以在状态变量改变后强制更新,就像前面的代码片段中那样。这个方法叫做 forceUpdate;参见列表 2.4。调用 forceUpdate 会导致组件上的 render 被调用,从而触发 UI 的重新渲染。使用 forceUpdate 通常不是必需的或推荐的,但了解它在示例或文档中可能会遇到是有好处的。大多数情况下,这种重新渲染可以通过其他方法来处理,例如调用 setState 或传入新的 props。
列表 2.4 使用 forceUpdate 强制重新渲染
class MyComponent extends Component {
constructor(){
super()
this.state = {
year: 2016
}
}
updateYear() {
this.state.year = 2017
}
update() {
this.forceUpdate()
}
render() {
return (
<View>
<Text onPress={ () => this.updateYear() }>
The year is: { this.state.year }
</Text>
<Text
onPress={ () => this. update () }>Force Update
</Text>
</View>
)
}
}
现在我们已经了解了如何使用基本字符串来处理状态,让我们看看其他几种数据类型。你将把布尔值、数组和对象附加到状态中,并在组件中使用它。你还将根据状态中的布尔值有条件地显示组件。
列表 2.5 使用其他数据类型的状态
class MyComponent extends Component {
constructor(){
super()
this.state = {
year: 2016,
leapYear: true,
topics: ['React', 'React Native', 'JavaScript'],
info: {
paperback: true,
length: '335 pages',
type: 'programming'
}
}
}
render() {
let leapyear = <Text>This is not a leapyear!</Text>
if (this.state.leapYear) {
leapyear = <Text>This is a leapyear!</Text>
}
return (
<View>
<Text>{ this.state.year }</Text>
<Text>Length: { this.state.info.length }</Text>
<Text>Type: { this.state.info.type }</Text>
{ leapyear }
</View>
)
}
}
2.2 使用 props 管理组件数据
属性(简称 属性)是组件继承的值或属性,它们是从父组件传递下来的。属性在声明时可以是静态或动态值,但在继承时是不可变的;它们只能通过更改它们声明和传递的顶层初始值来更改。React 的 “React 思维” 文档说,属性最好解释为“从父组件到子组件传递数据的一种方式。” 表 2.1 突出了属性和状态之间的一些差异和相似之处。
表 2.1 属性与状态
| 属性 | 状态 |
|---|---|
| 外部数据 | 内部数据 |
| 不可变 | 可变 |
| 从父组件继承 | 在组件内部创建 |
| 可以由父组件更改 | 只能在组件内部更新 |
| 可以作为属性向下传递 | 可以作为属性向下传递 |
| 在组件内部无法更改 | 在组件内部可以更改 |
解释属性工作原理的一个好方法是展示一个示例。以下列表声明了一个 book 值并将其作为静态属性传递给子组件。
列表 2.6 静态属性
class MyComponent extends Component {
render() {
return (
<BookDisplay book="React Native in Action" />
)
}
}
class BookDisplay extends Component {
render() {
return (
<View>
<Text>{ this.props.book }</Text>
</View>
)
}
}
此代码创建了两个组件:<MyComponent /> 和 <BookDisplay />。当你创建 <BookDisplay /> 时,你传递一个名为 book 的属性并将其设置为字符串 “React Native in Action”。以这种方式传递的任何属性都可在子组件上作为 this.props 使用。
你也可以使用大括号和字符串值,就像下面的示例那样,以变量的方式传递字面量。
列表 2.7 显示静态属性
class MyComponent extends Component {
render() {
return (
<BookDisplay book={"React Native in Action"} />
)
}
}
class BookDisplay extends Component {
render() {
return (
<View>
<Text>{ this.props.book }</Text>
</View>
)
}
}
动态属性
接下来,将一个动态属性传递给组件。在 render 方法中,在 return 语句之前,声明一个变量 book 并将其作为属性传递。
列表 2.8 动态属性
class MyComponent extends Component {
render() {
let book = 'React Native in Action'
return (
<BookDisplay book={ book } />
)
}
}
class BookDisplay extends Component {
render() {
return (
<View>
<Text>{ this.props.book }</Text>
</View>
)
}
}
现在,使用状态将一个动态属性传递给组件。
列表 2.9 使用状态动态属性
class MyComponent extends Component {
constructor() {
super()
this.state = {
book: 'React Native in Action'
}
}
render() {
return (
<BookDisplay book={this.state.book} />
)
}
}
class BookDisplay extends Component {
render() {
return (
<View>
<Text>{ this.props.book }</Text>
</View>
)
}
}
接下来,让我们看看如何更新状态以及随之而来的将作为属性传递给 BookDisplay 的值。记住,属性是不可变的,所以你需要更改父组件(MyComponent)的状态,这将向 BookDisplay 的 book 属性提供一个新值并触发组件及其子组件的重新渲染。将这个想法分解成单独的部分,以下是需要执行的操作:
- 声明状态变量:
this.state = {
book: 'React Native in Action'
}
- 编写一个函数来更新状态变量:
updateBook() {
this.setState({
book: 'Express in Action'
})
}
- 将函数和状态作为属性传递给子组件:
<BookDisplay
updateBook={ () => this.updateBook() }
book={ this.state.book } />
- 将函数附加到子组件中的触摸处理程序:
<Text onPress={ this.props.updateBook }>
现在你已经知道了所需的组件,你可以编写代码来实现这一功能。你将使用前一个示例中的组件并添加新的功能。
列表 2.10 更新动态属性
class MyComponent extends Component {
constructor(){
super()
this.state = {
book: 'React Native in Action'
}
}
updateBook() {
this.setState({
book: 'Express in Action'
})
}
render() {
return (
<BookDisplay
updateBook={ () => this.updateBook() }
book={ this.state.book } />
)
}
}
class BookDisplay extends Component {
render() {
return (
<View>
<Text
onPress={ this.props.updateBook }>
{ this.props.book }
</Text>
</View>
)
}
}
属性和状态的解构
持续引用状态和 props 为this.state和this.props可能会变得重复,违反了我们许多人试图遵循的 DRY(不要重复自己)原则。为了解决这个问题,你可以尝试使用解构。解构是作为 ES2015 规范的一部分添加到 JavaScript 中的新特性,并在 React Native 应用程序中可用。基本思想是你可以从对象中提取属性并将它们用作应用程序中的变量:
const person = { name: 'Jeff', age: 22 }
const { age } = person
console.log(age) #22
按照下面的示例使用解构编写组件。
列表 2.11 解构状态和 props
class MyComponent extends Component {
constructor(){
super()
this.state = {
book: 'React Native in Action'
}
}
updateBook() {
this.setState({ book: 'Express in Action' })
}
render() {
const { book } = this.state
return (
<BookDisplay
updateBook={ () => this.updateBook() }
book={ book } />
)
}
}
class BookDisplay extends Component {
render() {
const { book, updateBook } = this.props
return (
<View>
<Text
onPress={ updateBook }>
{ book }
</Text>
</View>
)
}
}
当引用书籍时,你不再需要在组件中引用this.state或this.props;相反,你已经从状态和 props 中提取了book变量,可以直接引用该变量。这开始变得更有意义,并且随着状态和 props 变得更大更复杂,你的代码也会变得更加清晰。
使用无状态组件的 props
由于无状态组件只需要关注 props 而没有自己的状态,因此在创建可重用组件时它们可以非常有用。让我们看看 props 如何在无状态组件中使用。
要使用无状态组件访问 props,将props作为函数的第一个参数传递。
列表 2.12 使用无状态组件的 props
const BookDisplay = (props) => {
const { book, updateBook } = props
return (
<View>
<Text
onPress={ updateBook }>
{ book }
</Text>
</View>
)
}
你也可以在函数参数中解构 props。
列表 2.13 在无状态组件中解构 props
const BookDisplay = ({ updateBook, book }) => {
return (
<View>
<Text
onPress={ updateBook }>
{ book }
</Text>
</View>
)
}
这样看起来更美观,并且清理了很多不必要的代码!你应该尽可能使用无状态组件,简化你的代码库和逻辑。
将数组和对象作为 props 传递
其他数据类型的工作方式与你预期的一样。例如,要传递一个数组,你将数组作为 prop 传递。要传递一个对象,你将对象作为 prop 传递。让我们看看一个基本示例。
列表 2.14 将其他数据类型作为 props 传递
class MyComponent extends Component {
constructor(){
super()
this.state = {
leapYear: true,
info: {
type: 'programming'
}
}
}
render() {
return (
<BookDisplay
leapYear={ this.state.leapYear }
info={ this.state.info }
topics={['React', 'React Native', 'JavaScript']} />
)
}
}
const BookDisplay = (props) => {
let leapyear
let { topics } = props
const { info } = props
topics = topics.map((topic, i) => {
return <Text>{ topic }</Text>
})
if (props.leapYear) {
leapyear = <Text>This is a leapyear!</Text>
}
return (
<View>
{ leapyear }
<Text>Book type: { info.type }</Text>
{ topics }
</View>
)
}
2.3 React 组件规范
当创建 React 和 React Native 组件时,你可以挂钩到几个规范和生命周期方法来控制组件中的行为。在本节中,我们将讨论它们,并给你一个很好的理解,了解每个方法做什么以及何时应该使用它们。
首先,我们将介绍组件规范的基础知识。组件规范基本上概述了组件应该如何对组件生命周期中发生的不同事件做出反应。规范如下:
-
render方法 -
constructor方法 -
statics对象,用于定义类可用的静态方法
2.3.1 使用 render 方法创建 UI
render方法是组件规范中创建组件时唯一必需的方法。它必须返回单个子元素、null或false。这个子元素可以是你在组件中声明的组件(如View或Text组件),或者另一个你定义的组件(比如你创建并导入到文件中的Button组件):
render() {
return (
<View>
<Text>Hello</Text>
</View>
)
}
你可以使用带或不带括号的render方法。如果你不使用括号,那么返回的元素当然必须与return语句在同一行:
render() {
return <View><Text>Hello</Text></View>
}
render方法也可以返回在别处定义的另一个组件:
render() {
return <SomeComponent />
}
#or
render() {
return (
<SomeComponent />
)
}
你也可以在render方法中检查条件,执行逻辑,并根据它们的值返回组件:
render() {
if(something === true) {
return <SomeComponent />
} else return <SomeOtherComponent />
}
2.3.2 使用属性初始化器和构造函数
状态可以在构造函数中创建或使用属性初始化器。属性初始化器是 JavaScript 语言的 ES7 规范,但它们与 React Native 无缝配合工作。它们提供了一种简洁的方法来在 React 类中声明状态:
class MyComponent extends React.Component {
state = {
someNumber: 1,
someBoolean: false
}
你也可以使用constructor方法在类中使用时设置初始状态。类以及constructor函数的概念并不特定于 React 或 React Native;它是一个 ES2015 规范,并且只是 JavaScript 现有基于原型的继承之上创建和初始化对象的语法糖。你还可以在构造函数中通过使用this.property语法(property是属性的名称)为组件类设置其他属性。关键字this指的是你当前所在的类实例:
constructor(){
super()
this.state = {
someOtherNumber: 19,
someOtherBoolean: true
}
this.name = 'Hello World'
this.type = 'class'
this.loaded = false
}
当使用构造函数创建 React 类时,你必须在使用this关键字之前使用super关键字,因为你正在扩展另一个类。此外,如果你需要在构造函数中访问任何 props,它们必须作为参数传递给构造函数,并在super调用中。
根据 props 设置状态通常不是好的做法,除非你故意为组件的内部功能设置某种类型的种子数据,因为如果数据被更改,状态将不再在组件之间保持一致性。状态仅在组件首次挂载或创建时创建。如果你使用不同的 prop 值重新渲染相同的组件,那么已经挂载的该组件实例将不会使用新的 prop 值来更新状态。
以下示例显示了在构造函数中使用 props 设置状态值。假设你最初将“Nader Dabit”作为 props 传递给组件:状态中的fullName属性将是“Nader Dabit”。如果组件随后被重新渲染为“Another Name”,则构造函数不会再次被调用,因此fullName的状态值将保持为“Nader Dabit”:
constructor(props){
super(props)
this.state = {
fullName: props.first + ' ' + props.last,
}
}
2.4 React 生命周期方法
在组件的生命周期中,各种方法会在特定的点被调用:这些被称为 生命周期方法。理解它们是如何工作的很重要,因为它们允许你在组件的创建和销毁的不同点执行特定的操作。例如,假设你想进行一个返回一些数据的 API 调用。你可能想确保组件已经准备好渲染这些数据,所以你会在组件挂载后,在名为 componentDidMount 的方法中进行 API 调用。在本节中,我们将介绍生命周期方法,并解释它们是如何工作的。
一个 React 组件的生命周期分为三个阶段:创建(挂载)、更新,以及删除(卸载)。在这三个阶段中,你可以钩入三组生命周期方法:
-
挂载(创建) —当组件被创建时,一系列生命周期方法会被触发,你有选择性地钩入任何一个或所有这些方法:
constructor、getDerivedStateFromProps、render和componentDidMount。你迄今为止使用的一个这样的方法是render,它渲染并返回一个 UI。 -
更新 —当组件更新时,更新生命周期方法会被触发:
getDerivedStateFromProps(当属性改变时)、shouldComponentUpdate、render、getSnapshotBeforeUpdate和componentDidUpdate。更新可以通过两种方式之一发生: -
当在组件内部调用
setState或forceUpdate时 -
当将新的属性传递到组件中
-
卸载 —当组件被卸载(销毁)时,会触发一个最终的生命周期方法:
componentWillUnmount。
2.4.1 getDerivedStateFromProps 静态方法
getDerivedStateFromProps 是一个静态类方法,在组件创建时以及接收到新属性时都会被调用。这个方法接收新的属性和最新的状态作为参数,并返回一个对象。对象中的数据会被更新到状态中。以下列表展示了一个示例。
列表 2.15 static getDerivedStateFromProps
export default class App extends Component {
state = {
userLoggedIn: false
}
static getDerivedStateFromProps(nextProps, nextState) {
if (nextProps.user.authenticated) {
return {
userLoggedIn: true
}
}
return null
}
render() {
return (
<View style={styles.container}>
{
this.state.userLoggedIn && (
<AuthenticatedComponent />>
)
}
</View>
);
}
}
2.4.2 componentDidMount 生命周期方法
componentDidMount 只会被调用一次,在组件加载完毕之后。这个方法是一个很好的地方来使用 AJAX 调用获取数据,执行 setTimeout 函数,以及与其他 JavaScript 框架集成。
列表 2.16 componentDidMount
class MainComponent extends Component {
constructor() {
super()
this.state = { loading: true, data: {} }
}
componentDidMount() {
#simulate ajax call
setTimeout(() => {
this.setState({
loading: false,
data: {name: 'Nader Dabit', age: 35}
})
}, 2000)
}
render() {
if(this.state.loading) {
return <Text>Loading</Text>
}
const { name, age } = this.state.data
return (
<View>
<Text>Name: {name}</Text>
<Text>Age: {age}</Text>
</View>
)
}
}
2.4.3 shouldComponentUpdate 生命周期方法
shouldComponentUpdate 返回一个布尔值,让你决定组件何时渲染。如果你知道新的状态或属性不会要求组件或其子组件进行渲染,你可以返回 false。如果你想使组件重新渲染,返回 true。
列表 2.17 shouldComponentUpdate
class MainComponent extends Component {
shouldComponentUpdate(nextProps, nextState) {
if(nextProps.name !== this.props.name) {
return true
}
return false
}
render() {
return <SomeComponent />
}
}
2.4.4 componentDidUpdate 生命周期方法
componentDidUpdate 在组件更新和重新渲染后被立即调用。你将获得前一个状态和前一个属性作为参数。
列表 2.18 componentDidUpdate
class MainComponent extends Component {
componentDidUpdate(prevProps, prevState) {
if(prevState.showToggled === this.state.showToggled) {
this.setState({
showToggled: !showToggled
})
}
}
render() {
return <SomeComponent />
}
}
2.4.5 componentWillUnmount 生命周期方法
componentWillUnmount在组件从应用程序中移除之前被调用。在这里,你可以执行任何必要的清理工作,移除监听器,并移除在componentDidMount中设置的计时器。
列表 2.19 componentWillUnmount
class MainComponent extends Component {
handleClick() {
this._timeout = setTimeout(() => {
this.openWidget();
}, 2000);
}
componentWillUnmount() {
clearTimeout(this._timeout);
}
render() {
return <SomeComponent
handleClick={() => this.handleClick()} />
}
}
概述
-
状态是处理 React 组件中数据的一种方式。更新状态会重新渲染组件的 UI 以及任何依赖于这些数据的子组件。
-
属性(props)是通过 React Native 应用程序向下传递数据到子组件的方式。更新 props 会自动更新接收相同 props 的任何组件。
-
React 组件规范是一组在 React 组件中的方法和属性,它指定了组件的声明。在创建 React 组件时,
render是唯一必需的方法;所有其他方法和属性都是可选的。 -
React 组件的生命周期有三个主要阶段:创建(挂载)、更新和删除(卸载)。每个阶段都有自己的生命周期方法集。
-
React 生命周期方法在 React 组件中可用,并在组件生命周期的特定点执行。它们控制组件的功能和更新。
3
构建你的第一个 React Native 应用
本章主要涵盖***
-
从头开始构建待办事项应用
-
轻量级调试
当学习一个新的框架、技术、语言或概念时,通过构建一个真实的应用程序来直接进入过程是一种很好的快速启动学习过程的方法。现在你已经了解了 React 和 React Native 的基本工作原理,让我们将这些部分组合起来,制作你的第一个应用:一个待办事项应用。通过构建一个小应用并使用我们迄今为止所讨论的信息,将是一个很好的巩固你对如何使用 React Native 理解的方法。
你将使用一些在应用中尚未深入探讨的功能,以及一些我们尚未讨论的样式细节,但不用担心。现在不是逐一介绍这些新想法的时候,你将构建基本的应用程序,然后在后面的章节中详细学习这些概念。抓住这个机会,在构建应用的过程中尽可能多地学习:你可以自由地破坏和修复样式和组件,看看会发生什么。
3.1 设计待办事项应用
让我们开始构建待办事项应用。它将类似于 TodoMVC 网站上的应用(todomvc.com)。图 3.1 显示了完成后的应用外观,这样你可以概念化你需要哪些组件以及如何组织它们。就像第一章中一样,图 3.2 将应用分解为组件和容器组件。让我们看看在应用中使用 React Native 组件的基本实现,这将如何呈现。

图 3.1 待办事项应用设计

图 3.2 带描述的待办事项应用
列表 3.1 基本待办事项应用程序实现
<View>
<Heading />
<Input />
<TodoList />
<Button />
<TabBar />
</View>
应用程序将显示一个标题、一个文本输入框、一个按钮和一个标签栏。当你添加一个待办事项时,应用程序会将它添加到待办事项数组中,并在输入框下方显示新的待办事项。每个待办事项将有两个按钮:完成和删除。完成按钮将标记为完成,删除按钮将从待办事项数组中移除它。屏幕底部,标签栏将根据待办事项是否完成或仍然活跃来过滤待办事项。
3.2 编写待办事项应用程序的代码
让我们开始编写应用程序的代码。在终端中输入 react-native init TodoApp 来创建一个新的 React Native 项目(见图 3.3)。现在,进入你的索引文件:如果你正在为 iOS 开发,打开 index.iOS.js;如果你正在为 Android 开发,打开 index.Android.js。两个平台的代码将是相同的。

图 3.3 初始化新的 React Native 应用程序
在索引文件中,导入一个 App 组件(你很快就会创建它),并删除任何不再使用的样式和额外组件。
列表 3.2 index.js
import React from 'react'
import { AppRegistry } from 'react-native'
import App from './app/App'
const TodoApp = () => <App />
AppRegistry.registerComponent('TodoApp', () => TodoApp)
在这里,你从 react-native 中引入了 AppRegistry。同时,你也引入了主要的 App 组件,你将在下一部分创建它。
在 AppRegistry 方法中,你初始化了应用程序。AppRegistry 是运行所有 React Native 应用程序的 JS 入口点。它接受两个参数:appKey,或者你在初始化应用程序时定义的应用程序名称;以及一个返回你想要用作应用程序入口点的 React Native 组件的函数。在这种情况下,你返回了在列表 3.2 中声明的 TodoApp 组件。
现在,在应用程序的根目录下创建一个名为 app 的文件夹。在 app 文件夹中,创建一个名为 App.js 的文件,并添加下一列表中显示的基本代码。
列表 3.3 创建 App 组件
import React, { Component } from 'react'
import { View, ScrollView, StyleSheet } from 'react-native'
class App extends Component {
render() {
return (
<View style={styles.container}>
<ScrollView keyboardShouldPersistTaps='always'
style={styles.content}>
<View/>
</ScrollView>
</View>
)
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5'
},
content: {
flex: 1,
paddingTop: 60
}
})
export default App
你导入了一个名为 ScrollView 的新组件,它包装了平台 ScrollView,基本上是一个可滚动的 View 组件。添加了一个 keyboardShouldPersistTaps 属性,值为 always:这个属性会在键盘打开时关闭键盘,并允许 UI 处理任何 onPress 事件。你确保 ScrollView 和 ScrollView 的父 View 都有一个 flex:1 的值。flex:1 是一个样式值,它使组件填充其父容器整个空间。
现在,为一些你稍后需要的值设置一个初始状态。你需要一个数组来保存你的待办事项,你可以将其命名为 todos;一个用于保存添加待办事项的 TextInput 当前状态的值,命名为 inputValue;以及一个用于存储你当前查看的待办事项类型(全部、当前或活跃),命名为 type。
在 App.js 中,在 render 函数之前,给类添加一个构造函数和一个初始状态,并在状态中初始化这些值。
列表 3.4 设置初始状态
...
class App extends Component {
constructor() {
super()
this.state = {
inputValue: '',
todos: [],
type: 'All'
}
}
render() {
...
}
}
...
接下来,创建Heading组件并为其设置一些样式。在应用文件夹中创建一个名为 Heading.js 的文件。这将是一个无状态组件。
列表 3.5 创建Heading组件
import React from 'react'
import { View, Text, StyleSheet } from 'react-native'
const Heading = () => (
<View style={styles.header}>
<Text style={styles.headerText}>
todos
</Text>
</View>
)
const styles = StyleSheet.create({
header: {
marginTop: 80
},
headerText: {
textAlign: 'center',
fontSize: 72,
color: 'rgba(175, 47, 47, 0.25)',
fontWeight: '100'
}
})
export default Heading
注意,在headerText的样式设置中,你传递一个rgba值给color属性。如果你不熟悉 RGBA,前三个值组成 RGB 颜色值,最后一个值代表 alpha 或透明度(红、蓝、绿、alpha)。你传入一个 alpha 值为 0.25,即 25%。你还设置了字体粗细为100,这将使文本看起来更细。
返回 App.js,引入Heading组件,并将其放置在ScrollView中,替换你最初放置的空View。
运行应用以查看新的标题和布局:见图 3.4。要在 iOS 上运行应用,使用react-native run-ios。要在 Android 上运行,从你的 React Native 应用程序的根目录使用终端中的react-native run-android。

图 3.4 运行应用
列表 3.6 导入和使用Heading组件
import React, { Component } from 'react'
import {View, ScrollView, StyleSheet} from 'react-native'
import Heading from './Heading'
class App extends Component {
...
render() {
return (
<View style={styles.container}>
<ScrollView
keyboardShouldPersistTaps='always'
style={styles.content}>
<Heading />
</ScrollView>
</View>
)
}
}
...
接下来,创建TextInput组件并为其设置一些样式。在应用文件夹中创建一个名为 Input.js 的文件。
列表 3.7 创建TextInput组件
import React from 'react'
import { View, TextInput, StyleSheet } from 'react-native'
const Input = () => (
<View style={styles.inputContainer}>
<TextInput
style={styles.input}
placeholder='What needs to be done?'
placeholderTextColor='#CACACA'
selectionColor='#666666' />
</View>
)
const styles = StyleSheet.create({
inputContainer: {
marginLeft: 20,
marginRight: 20,
shadowOpacity: 0.2,
shadowRadius: 3,
shadowColor: '#000000',
shadowOffset: { width: 2, height: 2 }
},
input: {
height: 60,
backgroundColor: '#ffffff',
paddingLeft: 10,
paddingRight: 10
}
})
export default Input
你在这里使用了一个新的 React Native 组件TextInput。如果你熟悉 Web 开发,这类似于 HTML 中的input。你还为TextInput和外部View设置了它们自己的样式。
TextInput还有一些其他的属性。在这里,你指定了一个placeholder来在用户开始输入之前显示文本,一个placeholderTextColor来设置占位符文本的样式,以及一个selectionColor来设置TextInput的光标样式。
下一个步骤,在第 3.4 节中,将是一个将函数连接到获取TextInput的值并将其保存到App组件的状态中的步骤。你还将进入 App.js 并在constructor下方、render函数上方添加一个名为inputChange的新函数。这个函数将使用传入的值更新inputValue的状态值,并且目前还将使用console.log()输出inputValue的值,以便你可以通过查看console.log()语句来确保函数正常工作。但要在 React Native 中查看console.log()语句,你首先需要打开开发者菜单。让我们看看它是如何工作的。
3.3 打开开发者菜单
开发者菜单是 React Native 内置的菜单,作为 React Native 的一部分提供;它为你提供了使用的主要调试工具。你可以在 iOS 模拟器或 Android 模拟器中打开它。在本节中,我将向你展示如何在两个平台上打开和使用开发者菜单。
3.3.1 在 iOS 模拟器中打开开发者菜单
当项目在 iOS 模拟器中运行时,你可以通过以下三种方式之一打开开发者菜单:
-
在键盘上按 Cmd-D。
-
在键盘上按 Cmd-Ctrl-Z。
-
在模拟器选项中打开硬件 > 摇动手势菜单(见图 3.5)。

图 3.5 手动打开开发者菜单(iOS 模拟器)

图 3.6 React Native 开发者菜单(iOS 模拟器)
当你这样做时,你应该看到图 3.6 所示的开发者菜单。
3.3.2 在 Android 模拟器中打开开发者菜单
在 Android 模拟器中打开并运行项目时,开发者菜单可以通过以下三种方式之一打开:
-
按键盘上的 F2 键。
-
按键盘上的 Cmd-M 键。
-
按下硬件按钮(见图 3.7)。

图 3.7 手动打开硬件菜单(Android 模拟器)

图 3.8 React Native 开发者菜单(Android 模拟器)
当你这样做时,你应该看到图 3.8 所示的开发者菜单。
3.3.3 使用开发者菜单
当开发者菜单打开时,你应该看到以下选项:
-
重新加载(iOS 和 Android)—重新加载应用。这也可以通过按键盘上的 Cmd-R(iOS)或按两次 R(Android)键来完成。
-
远程调试 JS(iOS 和 Android)—打开 Chrome 开发者工具,并通过浏览器提供完整的调试支持(图 3.9)。在这里,你可以访问代码中的日志语句,以及你在调试 Web 应用时习惯使用的断点和其他功能(除了 DOM)。如果你需要在应用中记录任何信息或数据,这通常是这样做的地方。

图 3.9 在 Chrome 中进行调试
-
启用实时重新加载(iOS 和 Android)—启用实时重新加载。当你修改代码时,整个应用将在模拟器中重新加载并刷新。
-
启动 Systrace(仅限 iOS)—Systrace 是一个性能分析工具。这将在你的应用运行时,为你提供一个关于每个 16 毫秒帧期间时间消耗的好主意。被分析代码块被起始/结束标记所包围,然后以彩色图表格式进行可视化。Systrace 也可以在 Android 中通过命令行手动启用。如果你想了解更多信息,请查看文档以获取全面的概述。
-
启用热重新加载(iOS 和 Android)—React Native 版本.22 中添加的一个优秀功能。它提供了一个惊人的开发者体验,允许你在文件更改时立即看到更改,而不会丢失应用当前状态。这对于在不丢失状态的情况下对应用深层的 UI 进行更改特别有用。这与实时重新加载不同,因为它保留了应用当前状态,只更新已更改的组件和状态(实时重新加载会重新加载整个应用,因此会丢失当前状态)。
-
切换检查器(iOS 和 Android):弹出一个类似于你在 Chrome 开发工具中看到的属性检查器。你可以点击一个元素,并看到它在组件层次结构中的位置,以及应用到此元素上的任何样式(图 3.10)。

图 3.10 使用检查器(左:iOS,右:Android)
- 显示性能监控器(iOS 和 Android):在应用的左上角弹出一个小的框,显示一些关于应用性能的信息。在这里,你可以看到正在使用的 RAM 数量和应用当前运行的每秒帧数。如果你点击这个框,它将展开以显示更多信息(图 3.11)。

图 3.11 性能监控器(左:iOS,右:Android)

图 3.12 开发设置(Android 模拟器)
- 开发设置(仅限 Android 模拟器):弹出一个包含额外调试选项的窗口,包括一个轻松切换
__DEV__环境变量为true或false的方法(图 3.12)。
3.4 继续构建待办事项应用
现在你已经了解了开发者菜单的工作方式,打开它并按 Debug JS Remotely 以打开 Chrome 开发工具。你准备好开始将信息记录到 JavaScript 控制台了。
你需要将 Input 组件导入到 app/App.js 中,并将一个方法附加到 TextInput 上,然后将这个方法作为属性传递给 Input。你还将把存储在状态中的 inputValue 传递给 Input 作为属性。
列表 3.8 创建 inputChange 函数
...
import Heading from './Heading'
import Input from './Input'
class App extends Component {
constructor() {
…
}
inputChange(inputValue) { ①
console.log(' Input Value: ' , inputValue) ②
this.setState({ inputValue }) ③
}
render() {
const { inputValue } = this.state
return (
<View style={styles.container}>
<ScrollView
keyboardShouldPersistTaps='always'
style={styles.content}>
<Heading />
<Input
inputValue={inputValue} ④
inputChange={(text) => this.inputChange(text)} /> ⑤
</ScrollView>
</View>
)
}}
inputChange 接收一个参数,即 TextInput 的值,并使用 TextInput 返回的值更新状态中的 inputValue。
现在,你需要将函数与 Input 组件中的 TextInput 连接起来。打开 app/Input.js,并更新 TextInput 组件,使用新的 inputChange 函数和 inputValue 属性。
列表 3.9 将 inputChange 和 inputValue 添加到 TextInput
...
const Input = ({ inputValue, inputChange }) => ( ①
<View style={styles.inputContainer}>
<TextInput
value={inputValue}
style={styles.input}
placeholder='What needs to be done?'
placeholderTextColor='#CACACA'
selectionColor='#666666'
onChangeText={inputChange} /> ②
</View>
)
...
在创建无状态组件时,你将解构 inputValue 和 inputChange 属性。当 TextInput 的值发生变化时,将调用 inputChange 函数,并将值传递给父组件以设置 inputValue 的状态。你还将 TextInput 的值设置为 inputValue,这样你就可以稍后控制和重置 TextInput。onChangeText 是一个方法,每次 TextInput 组件的值发生变化时都会被调用,并将 TextInput 的值传递给它。
再次运行项目并查看其外观(图 3.13)。你正在记录输入值,所以当你输入时,你应该看到值被记录到控制台(图 3.14)。

图 3.13 添加 TextInput 后的更新视图

图 3.14 使用 inputChange 方法记录 TextInput 的值
现在 inputValue 的值被存储在状态中,你需要创建一个按钮来将项目添加到待办事项列表中。在这样做之前,创建一个函数,你将把它绑定到按钮上,以便将新的待办事项添加到构造函数中定义的待办事项数组中。将此函数命名为 submitTodo,并将其放置在 inputChange 函数之后和 render 函数之前。
列表 3.10 添加 submitTodo 函数
...
submitTodo () { ①
if (this.state.inputValue.match(/^\s*$/)) { ①
return ①
} ①
const todo = { ②
title: this.state.inputValue, ②
todoIndex, ②
complete: false ②
} ②
todoIndex++ ③
const todos = [...this.state.todos, todo] ④
this.setState({ todos, inputValue: '' }, () => { ⑤
console.log('State: ', this.state) ⑥
})
}
...
接下来,在 App.js 文件顶部创建 todoIndex,在最后一个 import 语句下方。
列表 3.11 创建 todoIndex 变量
...
import Input from './Input'
let todoIndex = 0
class App extends Component {
...
现在已经创建了 submitTodo 函数,创建一个名为 Button.js 的文件,并将该函数连接到按钮以工作。
列表 3.12 创建 Button 组件
import React from 'react'
import { View, Text, StyleSheet, TouchableHighlight } from 'react-native'
const Button = ({ submitTodo }) => ( ①
<View style={styles.buttonContainer}>
<TouchableHighlight
underlayColor='#efefef'
style={styles.button}
onPress={submitTodo}> ②
<Text style={styles.submit}>
Submit
</Text>
</TouchableHighlight>
</View>
)
const styles = StyleSheet.create({
buttonContainer: {
alignItems: 'flex-end'
},
button: {
height: 50,
paddingLeft: 20,
paddingRight: 20,
backgroundColor: '#ffffff',
width: 200,
marginRight: 20,
marginTop: 15,
borderWidth: 1,
borderColor: 'rgba(0,0,0,.1)',
justifyContent: 'center',
alignItems: 'center'
},
submit: {
color: '#666666',
fontWeight: '600'
}
})
export default Button
在此组件中,你首次使用 TouchableHighlight。TouchableHighlight 是创建 React Native 中按钮的一种方式,与 HTML 的 button 元素基本相当。
使用 TouchableHighlight,你可以包裹视图并使其正确响应触摸事件。在按下时,默认的 backgroundColor 被替换为你提供的指定 underlayColor 属性。在这里,你指定一个 underlayColor 为 '#efefef',这是一种浅灰色;背景颜色是白色。这将使用户对触摸事件是否已注册有一个良好的感觉。如果没有定义 underlayColor,则默认为黑色。
TouchableHighlight 只支持一个主要子组件。在这里,你传递一个 Text 组件。如果你想在 TouchableHighlight 中使用多个组件,将它们包裹在一个单独的 View 中,并将此 View 作为 TouchableHighlight 的子组件传递。
你已经创建了 Button 组件,并将其与 App.js 中定义的函数连接起来。现在将此组件带入应用(app/App.js)并查看它是否工作!
列表 3.13 导入 Button 组件
...
import Button from './Button' ①
let todoIndex = 0
...
constructor() {
super()
this.state = {
inputValue: '',
todos: [],
type: 'All'
}
this.submitTodo = this.submitTodo.bind(this) ②
}
...
render () {
let { inputValue } = this.state
return (
<View style={styles.container}>
<ScrollView
keyboardShouldPersistTaps='always'
style={styles.content}>
<Heading />
<Input
inputValue={inputValue}
inputChange={(text) => this.inputChange(text)} />
<Button submitTodo={this.submitTodo} /> ③
</ScrollView>
</View>
)
}
你导入 Button 组件,并在 render 函数中将它放置在 Input 组件下方。submitTodo 作为名为 this.submitTodo 的属性传递给 Button。
现在,刷新应用。它应该看起来像 图 3.15。当你添加待办事项时,TextInput 应该清除,并且应用状态应该记录到控制台,显示包含新待办事项的待办事项数组 (图 3.16)。

图 3.15 带有 Button 组件的更新应用

图 3.16 记录状态
现在你正在向待办事项数组中添加待办事项,你需要将它们渲染到屏幕上。要开始这个操作,你需要创建两个新的组件:TodoList 和 Todo。TodoList 将渲染待办事项列表,并使用 Todo 组件为每个单独的待办事项。首先,在应用文件夹中创建一个名为 Todo.js 的文件。
列表 3.14 创建 Todo 组件
import React from 'react'
import { View, Text, StyleSheet } from 'react-native'
const Todo = ({ todo }) => (
<View style={styles.todoContainer}>
<Text style={styles.todoText}>
{todo.title}
</Text>
</View>
)
const styles = StyleSheet.create({
todoContainer: {
marginLeft: 20,
marginRight: 20,
backgroundColor: '#ffffff',
borderTopWidth: 1,
borderRightWidth: 1,
borderLeftWidth: 1,
borderColor: '#ededed',
paddingLeft: 14,
paddingTop: 7,
paddingBottom: 7,
shadowOpacity: 0.2,
shadowRadius: 3,
shadowColor: '#000000',
shadowOffset: { width: 2, height: 2 },
flexDirection: 'row',
alignItems: 'center'
},
todoText: {
fontSize: 17
}
})
export default Todo
目前 Todo 组件只接受一个属性——一个待办事项,并在 Text 组件中渲染标题。你还在 View 和 Text 组件上添加了样式。
接下来,创建 TodoList 组件(app/TodoList.js)。
列表 3.15 创建 TodoList 组件
import React from 'react'
import { View } from 'react-native'
import Todo from './Todo'
const TodoList = ({ todos }) => {
todos = todos.map((todo, i) => {
return (
<Todo
key={todo.todoIndex}
todo={todo} />
)
})
return (
<View>
{todos}
</View>
)
}
export default TodoList
目前 TodoList 组件只接受一个属性:一个待办事项数组。然后你遍历这些待办事项,并为每个待办事项创建一个新的 Todo 组件(在文件顶部导入),将待办事项作为属性传递给 Todo 组件。你还指定了一个键,并将待办事项项的索引作为键传递给每个组件。key 属性帮助 React 在计算与虚拟 DOM 的差异时识别已更改的项目。如果你省略了这一点,React 将会给出警告。
你需要做的最后一件事是将 TodoList 组件导入到 App.js 文件中,并将待办事项作为属性传入。
列表 3.16 导入 TodoList 组件
...
import TodoList from './TodoList'
...
render () {
const { inputValue, todos } = this.state
return (
<View style={styles.container}>
<ScrollView
keyboardShouldPersistTaps='always'
style={styles.content}>
<Heading />
<Input inputValue={inputValue} inputChange={(text) => this.inputChange(text)} />
<TodoList todos={todos} />
<Button submitTodo={this.submitTodo} />
</ScrollView>
</View>
)
}
...
运行应用。当你添加一个待办事项时,你应该看到它在待办事项列表中弹出(图 3.17)。
下一步是标记待办事项为完成,以及删除待办事项。打开 App.js,并在 submitTodo 函数下方创建 toggleComplete 和 deleteTodo 函数。toggleComplete 将切换待办事项是否完成,而 deleteTodo 将删除待办事项。

图 3.17 更新后的应用,包含 TodoList 组件
列表 3.17 添加 toggleComplete 和 deleteTodo 函数
constructor () {
...
this.toggleComplete = this.toggleComplete.bind(this) ①
this.deleteTodo = this.deleteTodo.bind(this) ②
}
...
deleteTodo (todoIndex) { ③
let { todos } = this.state
todos = todos.filter((todo) => todo.todoIndex !== todoIndex)
this.setState({ todos })
}
toggleComplete (todoIndex) { ④
let todos = this.state.todos
todos.forEach((todo) => {
if (todo.todoIndex === todoIndex) {
todo.complete = !todo.complete
}
})
this.setState({ todos })
}
...
为了连接这些函数,你需要创建一个按钮组件并将其传递给待办事项。在 app 文件夹中,创建一个新的文件,命名为 TodoButton.js。
列表 3.18 创建 TodoButton.js
import React from 'react'
import { Text, TouchableHighlight, StyleSheet } from 'react-native'
const TodoButton = ({ onPress, complete, name }) => ( ①
<TouchableHighlight
onPress={onPress}
underlayColor='#efefef'
style={styles.button}>
<Text style={
styles.text,
complete ? styles.complete : null, [ ②
name === 'Delete' ? styles.deleteButton : null ]} ③
>
{name}
</Text>
</TouchableHighlight>
)
const styles = StyleSheet.create({
button: {
alignSelf: 'flex-end',
padding: 7,
borderColor: '#ededed',
borderWidth: 1,
borderRadius: 4,
marginRight: 5
},
text: {
color: '#666666'
},
complete: {
color: 'green',
fontWeight: 'bold'
},
deleteButton: {
color: 'rgba(175, 47, 47, 1)'
}
})
export default TodoButtton
现在,将新的函数作为 props 传递给 TodoList 组件。
列表 3.19 将 toggleComplete 和 deleteTodo 作为 props 传递给 TodoList
render () {
...
<TodoList
toggleComplete={this.toggleComplete}
deleteTodo={this.deleteTodo}
todos={todos} />
<Button submitTodo={this.submitTodo} />
...
}
接下来,将 toggleComplete 和 deleteTodo 作为 props 传递给 Todo 组件。
列表 3.20 将 toggleComplete 和 deleteTodo 作为 props 传递给 ToDo
...
const TodoList = ({ todos, deleteTodo, toggleComplete }) => {
todos = todos.map((todo, i) => {
return (
<Todo
deleteTodo={deleteTodo}
toggleComplete={toggleComplete}
key={i}
todo={todo} />
)
})
...
最后,打开 Todo.js 并更新 Todo 组件,以引入新的 TodoButton 组件和按钮容器的样式。
列表 3.21 更新 Todo.js 以引入 TodoButton 和功能
import TodoButton from './TodoButton'
...
const Todo = ({ todo, toggleComplete, deleteTodo }) => (
<View style={styles.todoContainer}>
<Text style={styles.todoText}>
{todo.title}
</Text>
<View style={styles.buttons}>
<TodoButton
name='Done'
complete={todo.complete}
onPress={() => toggleComplete(todo.todoIndex)} />
<TodoButton
name='Delete'
onPress={() => deleteTodo(todo.todoIndex)} />
</View>
</View>
)
const styles = StyleSheet.create({
...
buttons: {
flex: 1,
flexDirection: 'row',
justifyContent: 'flex-end',
alignItems: 'center'
},
...
)}
你添加了两个 TodoButton 按钮:一个命名为完成,另一个命名为删除。你还传递了 toggleComplete 和 deleteTodo 作为函数,以便在 TodoButton.js 中调用你定义的 onPress。如果你刷新应用并添加一个待办事项,你现在应该能看到新的按钮(图 3.18)。

图 3.18 显示 TodoButtons 的应用
如果你点击完成,按钮文本应该加粗并变为绿色。如果你点击删除,待办事项应该从待办事项列表中消失。
你现在几乎完成了应用。最后一步是构建一个标签栏过滤器,它将显示所有待办事项、仅显示完成的待办事项或仅显示未完成的待办事项。为了开始这个,你需要创建一个新的函数来设置要显示的待办事项类型。
在构造函数中,当你第一次创建应用时,你将状态变量 type 设置为 'All'。现在你将创建一个名为 setType 的函数,它将接受一个类型作为参数并更新状态中的类型。在 App.js 中 toggleComplete 函数下方放置此函数。
列表 3.22 添加 setType 函数
constructor () {
...
this.setType = this.setType.bind(this)
}
...
setType (type) {
this.setState({ type })
}
...
接下来,你需要创建 TabBar 和 TabBarItem 组件。首先,创建 TabBar 组件:在 app 文件夹中添加一个名为 TabBar.js 的文件。
列表 3.23 创建 TabBar 组件
import React from 'react'
import { View, StyleSheet } from 'react-native'
import TabBarItem from './TabBarItem'
const TabBar = ({ setType, type }) => (
<View style={styles.container}>
<TabBarItem type={type} title='All'
setType={() => setType('All')} />
<TabBarItem type={type} border title='Active'
setType={() => setType('Active')} />
<TabBarItem type={type} border title='Complete'
setType={() => setType('Complete')} />
</View>
)
const styles = StyleSheet.create({
container: {
height: 70,
flexDirection: 'row',
borderTopWidth: 1,
borderTopColor: '#dddddd'
}
})
export default TabBar
此组件接受两个属性:setType 和 type。这两个属性都是从主 App 组件传递下来的。
你正在导入尚未定义的 TabBarItem 组件。每个 TabBarItem 组件接受三个属性:title、type 和 setType。其中两个组件还接受一个 border 属性(布尔值),如果设置为 true,将添加左边界样式。
接下来,在 app 文件夹中创建一个名为 TabBarItem.js 的文件。
列表 3.24 创建 TabBarItem 组件
import React from 'react'
import { Text, TouchableHighlight, StyleSheet } from 'react-native'
const TabBarItem = ({ border, title, selected, setType, type }) => (
<TouchableHighlight
underlayColor='#efefef'
onPress={setType}
style={[
styles.item, selected ? styles.selected : null,
border ? styles.border : null,
type === title ? styles.selected : null ]}>
<Text style={[ styles.itemText, type === title ? styles.bold : null ]}>
{title}
</Text>
</TouchableHighlight>
)
const styles = StyleSheet.create({
item: {
flex: 1,
justifyContent: 'center',
alignItems: 'center'
},
border: {
borderLeftWidth: 1,
borderLeftColor: '#dddddd'
},
itemText: {
color: '#777777',
fontSize: 16
},
selected: {
backgroundColor: '#ffffff'
},
bold: {
fontWeight: 'bold'
}
})
export default TabBarItem
在 TouchableHighlight 组件中,你检查一些属性并根据属性设置样式。如果 selected 是 true,你给它 styles.selected 的样式。如果 border 是 true,你给它 styles.border 的样式。如果 type 等于 title,你给它 styles.selected 的样式。
在 Text 组件中,你也会检查 type 是否等于 title。如果是,给它添加粗体样式。
要实现 TabBar,打开 app/App.js,引入 TabBar 组件,并设置它。你还将 type 作为 render 函数的一部分,在解构 this.state 时引入。
列表 3.25 实现 TabBar 组件
...
import TabBar from './TabBar'
class App extends Component {
...
render () {
const { todos, inputValue, type } = this.state
return (
<View style={styles.container}>
<ScrollView
keyboardShouldPersistTaps='always'
style={styles.content}>
<Heading />
<Input inputValue={inputValue}
inputChange={(text) => this.inputChange(text)} />
<TodoList
type={type}
toggleComplete={this.toggleComplete}
deleteTodo={this.deleteTodo}
todos={todos} />
<Button submitTodo={this.submitTodo} />
</ScrollView>
<TabBar type={type} setType={this.setType} />
</View>
)
}
...
在这里,你引入了 TabBar 组件。然后从状态中解构 type 并不仅将其传递给新的 TabBar 组件,还传递给 TodoList 组件;你将在下一秒使用这个 type 变量来根据此类型过滤待办事项。你还把 setType 函数作为属性传递给 TabBar 组件。
你需要做的最后一件事是打开 TodoList 组件,并添加一个过滤器,只返回当前选定的标签页类型的待办事项。打开 TodoList.js,从属性中解构 type,在 return 语句之前添加以下 getVisibleTodos 函数。
列表 3.26 更新 TodoList 组件
...
const TodoList = ({ todos, deleteTodo, toggleComplete, type }) => {
const getVisibleTodos = (todos, type) => {
switch (type) {
case 'All':
return todos
case 'Complete':
return todos.filter((t) => t.complete)
case 'Active':
return todos.filter((t) => !t.complete)
}
}
todos = getVisibleTodos(todos, type)
todos = todos.map((todo, i) => {
...
你使用 switch 语句检查当前设置的是哪种类型。如果设置为 'All',则返回待办事项的整个列表。如果设置为 'Complete',则过滤待办事项并只返回完成的待办事项。如果设置为 'Active',则过滤待办事项并只返回未完成的待办事项。
然后,你将 todos 变量设置为 getVisibleTodos 返回的值。现在你应该能够运行应用并看到新的 TabBar (图 3.19)。TabBar 将根据选定的类型进行过滤。

图 3.19 最终待办事项应用
摘要
-
AppRegistry是运行所有 React Native 应用的 JavaScript 入口点。 -
React Native 组件
TextInput与 HTML 的input类似。你可以指定多个属性,包括一个placeholder属性,用于在用户开始输入之前显示文本,一个placeholderTextColor属性,用于设置占位符文本的样式,以及一个selectionColor属性,用于设置TextInput的光标样式。 -
TouchableHighlight是在 React Native 中创建按钮的一种方式;它与 HTML 的button元素相当。你可以使用TouchableHighlight来包裹视图,并使其正确响应触摸事件。 -
你已经学会了如何在 iOS 和 Android 模拟器中启用开发者工具。
-
使用 JavaScript 控制台(可通过开发者菜单访问)是调试你的应用并记录有用信息的好方法。
第二部分
在 React Native 中开发应用程序
在掌握基础知识之后,你就可以开始向你的 React Native 应用程序添加功能了。本部分中的章节涵盖了样式、导航、动画以及使用数据架构(重点关注 Redux)优雅地处理数据的方法。
第四章和第五章教授了如何将样式应用于组件内联或在其可以引用的样式表中。由于 React Native 组件是应用程序 UI 的主要构建块,第四章花了一些时间教授你可以使用 View 组件做的有用事情。第五章在第四章教授的技能基础上进行扩展。它涵盖了特定于平台的样式方面,以及一些高级技术,包括使用 flexbox 使布局更容易。
第六章展示了如何使用两个最推荐和最常用的导航库,React Navigation 和 React Native Navigation。我们介绍了创建三种主要类型的导航器——标签、堆栈和抽屉,以及如何控制导航状态。
第七章涵盖了创建动画需要做的四件事,与 Animated API 一起提供的四种可动画组件类型,如何创建自定义可动画组件,以及一些其他有用的技能。
在第八章中,我们探讨了使用数据架构处理数据。由于 Redux 是 React 生态系统中处理数据最广泛采用的方法,因此你使用它来构建应用程序,同时学习数据处理技能。我们展示了如何使用 Context API,以及如何通过使用 reducer 来保存 Redux 状态和从示例应用程序中删除项目来实现 Redux。我们还涵盖了如何使用 providers 将全局状态传递给应用程序的其余部分,如何使用 connect 函数从子组件访问示例应用程序,以及如何使用动作添加功能。
4
样式介绍
本章涵盖了
-
使用 JavaScript 进行样式化
-
应用和组织样式
-
将样式应用于
View组件 -
将样式应用于
Text组件
建立移动应用程序需要才能,但要使它们出色则需要 风格。如果你是一位图形设计师,你对此有直觉性的认识,深入骨髓。如果你是一位开发者,你可能正在抱怨并翻着白眼。无论哪种情况,理解 React Native 组件的基本样式原理对于制作一个吸引人且他人愿意使用的应用程序至关重要。
很可能你已经有一些 CSS 经验,即使只是看到过其语法。你很容易理解像 background-color: 'red' 这样的 CSS 规则想要做什么。当你开始阅读这一章时,可能会觉得在 React Native 中对组件进行样式化就像使用 CSS 规则的驼峰命名法一样简单。例如,设置 React Native 组件的背景颜色使用几乎相同的语法,backgroundColor: 'red'——但请提前警告,相似之处到此为止。
尽量不要执着于你在 CSS 中做事情的方式。拥抱 React Native 的方式,你会发现学习如何样式化组件是一个更加愉快的体验——即使是对于开发者来说也是如此。
本章的第一部分提供了样式组件的概述。我们将确保你理解将样式应用到组件的各种方法,并讨论如何在应用程序中组织样式。现在形成良好的组织习惯将使事情更容易管理,并有助于将来使用更高级的技术。
因为 React Native 使用 JavaScript 进行样式设计,我们将讨论如何开始将样式视为代码,以及如何利用 JavaScript 的特性,如变量和函数。最后两个部分将探讨 View 组件和 Text 组件的样式。在某些情况下,我们会用简短的例子来解释一个主题,但大部分时间,我们将通过实际样式的例子来讲解。你将所学应用到构建个人资料卡片的过程中。
本章中的所有示例代码,你可以从默认生成的应用程序开始,用单独列表中的代码替换 App.js 中的内容。完整的源代码可以在 www.manning.com/books/react-native-in-action 找到,以及本书的 Git 仓库 https://github.com/dabit3/react-native-in-action 下的第四章。
4.1 在 React Native 中应用和组织样式
React Native 内置了许多组件,社区也构建了许多你可以包含到你的项目中的组件。组件支持特定的样式集。这些样式可能或可能不适用于其他类型的组件。例如,Text 组件支持 fontWeight 属性(fontWeight 指的是字体的粗细),但 View 组件不支持。相反,View 组件支持 flex 属性(flex 指的是视图内组件的布局),但 Text 组件不支持。
不同的组件之间有些样式元素是相似的,但并不完全相同。例如,View 组件支持 shadowColor 属性,而 Text 组件支持 textShadowColor 属性。一些样式,如 ShadowPropTypesIOS,只适用于特定的平台(在这种情况下,是 iOS)。
学习各种样式以及如何操作它们需要时间。这就是为什么从如何应用和组织样式等基础知识开始很重要。本节将专注于教授这些样式基础知识,这样你将有一个良好的基础,从那里开始探索样式并构建示例个人资料卡片组件。
4.1.1 在应用程序中应用样式
为了在市场上竞争,移动应用程序必须有一种风格感。你可以开发一个功能齐全的应用程序,但如果它看起来很糟糕且不吸引人,人们是不会感兴趣的。你不必建造世界上看起来最酷的应用程序,但你确实需要致力于创建一个精致的产品。一个精致、外观锐利的应用程序会极大地影响人们对应用程序质量的看法。
你可以在 React Native 中以多种方式应用样式。在第一章和第三章中,我们介绍了内联样式(如下所示)以及使用 StyleSheet 的样式(清单 4.2)。
清单 4.1 使用内联样式
import React, { Component } from 'react'
import { Text, View } from 'react-native'
export default class App extends Component {
render () {
return (
<View style={{marginLeft: 20, marginTop: 20}}> ①
<Text style={{fontSize: 18,color: 'red'}}>Some Text</Text> ②
</View>
)
}
}
如你所见,你可以通过向 styles 属性提供一个对象来一次性指定多个样式。
清单 4.2 引用 StyleSheet 中定义的样式
import React, { Component } from 'react'
import { StyleSheet, Text, View } from 'react-native'
export default class App extends Component {
render () {
return (
<View style={styles.container}> ①
<Text style={[styles.message,styles.warning]}>Some Text</Text> ②
</View>
)
}
}
const styles = StyleSheet.create({
container: { ③
marginLeft: 20,
marginTop: 20
},
message: { ③
fontSize: 18
},
warning: { ③
color: 'red'
}
});
在功能上,使用内联样式与引用在 StyleSheet 中定义的样式没有区别。使用 StyleSheet,你可以创建一个 style 对象并单独引用每个样式。将样式与 render 方法分离使得代码更容易理解,并促进了样式在组件间的复用。
当使用像 warning 这样的样式名时,很容易识别消息的意图。但是内联样式 color: 'red' 并没有提供关于为什么消息是红色的任何见解。将样式指定在一个地方而不是在许多组件的内联中,使得在整个应用程序中应用更改变得更加容易。想象一下,如果你想将警告消息改为黄色,你只需要在样式表中更改一次样式定义,color: 'yellow'。
清单 4.2 还展示了如何通过提供一个包含样式属性的数组来指定多个样式。记住,当你这样做时,如果存在重复的属性,最后传入的样式将覆盖之前的样式。例如,如果提供了一个这样的样式数组,color 的最后一个值将覆盖所有之前的值:
style={[{color: 'black'},{color: 'yellow'},{color: 'red'}]}
在这个例子中,颜色将是红色。
你也可以通过使用内联样式和样式表的引用来组合这两种方法,指定一个包含样式属性的数组:
style={[{color: 'black'}, styles.message]}
React Native 在这方面非常灵活,这既有好的一面也有不好的一面。当你快速尝试原型设计时,指定内联样式非常容易,但从长远来看,你需要小心地组织你的样式;否则,你的应用程序可能会迅速变得混乱且难以管理。通过组织你的样式,你会使以下操作变得更加容易:
-
维护你的应用程序代码库
-
在组件间复用样式
-
在开发过程中尝试样式变化
4.1.2 组织样式
如前文所述,你可能已经猜到,使用内联样式并不是推荐的做法:样式表是管理样式的更有效方式。但在实际操作中这意味着什么呢?
在设计网站时,我们经常使用样式表。我们经常使用 Sass、Less 和 PostCSS 等工具来创建整个应用程序的单一代码样式表。在网页的世界里,样式本质上是全球的,但这不是 React Native 的方式。
React Native 专注于组件。目标是使组件尽可能可重用和独立。使组件依赖于应用程序的样式表是模块化的对立面。在 React Native 中,样式是针对组件的——而不是针对应用程序。
如何实现这种封装完全取决于你团队的偏好。没有对错之分,但在 React Native 社区中,你会发现两种常见的方法:
-
在组件相同的文件中声明样式表
-
在组件外部单独声明样式表
在组件相同的文件中声明样式表
正如你在本书中已经做的那样,声明样式的常见方法是在将使用它们的组件内部进行。这种方法的优点是组件及其样式完全封装在一个单独的文件中。然后,这个组件可以被移动或在任何地方使用。这是组件设计的一种常见方法,你会在 React Native 社区中经常看到。

图 4.1 一个示例文件结构,其中样式与组件分开保存在单个文件夹中而不是单个文件中
当将样式定义与组件一起包含时,通常的约定是在组件之后指定样式。本书中迄今为止的所有列表都遵循了这一约定。
在单独的文件中声明样式表
如果你习惯于编写 CSS,将样式放入单独的文件可能看起来是一个更好的方法,并且感觉更熟悉。样式表定义是在一个单独的文件中创建的。你可以给它取任何你想要的名称(styles.js 是典型的名称),但请确保扩展名是 .js;毕竟,它是 JavaScript。样式表文件和组件文件保存在同一个文件夹中。
如 图 4.1 所示的文件结构保留了组件和样式之间的紧密关系,并通过不将样式定义与组件的功能方面混合而提供了一些清晰度。列表 4.3 对应于一个用于样式化图中的 ComponentA 和 ComponentB 的 styles.js 文件。在定义样式表时使用有意义的名称,以便清楚地了解正在样式化组件的哪个部分。
列表 4.3 将组件的样式表外部化
import { StyleSheet } from 'react-native'
const styles = StyleSheet.create({ ①
container: { ②
marginTop: 150,
backgroundColor: '#ededed',
flexWrap: 'wrap'
}
})
const buttons = StyleSheet.create({ ③
primary: { ④
flex: 1,
height: 70,
backgroundColor: 'red',
justifyContent: 'center',
alignItems: 'center',
marginLeft: 20,
marginRight: 20
}
})
export { styles, buttons } ⑤
组件导入外部样式表并可以引用其中定义的任何样式。
列表 4.4 导入外部样式表
import { styles, buttons } from './component/styles' ①
<View style={styles.container}> ②
<TouchableHighlight style={buttons.primary} /> ③
...
</TouchableHighlight>
</View>
4.1.3 样式是代码
你已经看到了如何在 React Native 中使用 JavaScript 定义样式。尽管拥有完整的脚本语言,包括变量和函数,但你的样式相当静态,但它们当然不必如此!
网页开发者多年来一直在与 CSS 作斗争。为了克服层叠样式表的许多限制,创造了新的技术,如 Sass、Less 和 PostCSS。即使像定义一个变量来存储网站的主色这样的简单事情,没有 CSS 预处理器也是不可能的。2015 年 12 月 CSS 变量级联模块 1 的候选推荐引入了自定义属性的概念,这些属性类似于变量;但在撰写本文时,使用中的不到 80% 的浏览器支持此功能。
让我们利用我们正在使用 JavaScript 的这一事实,开始将样式视为代码。你将构建一个简单的应用程序,用户可以通过它从浅色主题切换到深色主题。但在你开始编码之前,让我们回顾一下你试图构建的内容。
应用程序在屏幕上有一个单独的按钮。该按钮被一个小正方形框包围。当按钮被按下时,主题将切换。当选择浅色主题时,按钮标签将显示为白色,背景将是白色,按钮周围的框将是黑色。当选择深色主题时,按钮标签将显示为黑色,背景将是黑色,按钮周围的框将是白色。图 4.2 展示了选择主题时屏幕应该看起来像什么。

图 4.2 一个支持两种主题(白色和黑色)的简单应用程序。用户可以按按钮在白色背景和黑色背景之间切换。
对于这个例子,将样式组织在一个单独的文件 styles.js 中。然后,创建一些常量来保存颜色值,并为浅色和深色主题创建两个样式表。
列表 4.5 从主组件文件中提取的动态样式表
import {StyleSheet} from 'react-native';
export const Colors = { ①
dark: 'black',
light: 'white'
};
const baseContainerStyles = { ②
flex: 1,
justifyContent: 'center',
alignItems: 'center'
};
const baseBoxStyles = { ③
justifyContent: 'center',
alignItems: 'center',
borderWidth: 2,
height: 150,
width: 150
};
const lightStyleSheet = StyleSheet.create({ ④
container: {
...baseContainerStyles,
backgroundColor: Colors.light
},
box: {
...baseBoxStyles,
borderColor: Colors.dark
}
});
const darkStyleSheet = StyleSheet.create({ ⑤
container: {
...baseContainerStyles,
backgroundColor: Colors.dark
},
box: {
...baseBoxStyles,
borderColor: Colors.light
}
});
export default function getStyleSheet(useDarkTheme){ ⑥
return useDarkTheme ? darkStyleSheet : lightStyleSheet; ⑦
}
一旦配置了样式,你就可以开始在 App.js 中构建组件应用程序了。因为你只有浅色和深色主题,创建一个实用函数 getStyleSheet,它接受一个布尔值。如果提供 true,则返回深色主题;否则返回浅色主题。
列表 4.6 在浅色和深色主题之间切换的应用程序
import React, { Component } from 'react';
import { Button, StyleSheet, View } from 'react-native';
import getStyleSheet from './styles'; ①
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
darkTheme: false ②
};
this.toggleTheme = this.toggleTheme.bind(this); ③
}
toggleTheme() {
this.setState({darkTheme: !this.state.darkTheme}) ④
};
render() {
const styles = getStyleSheet(this.state.darkTheme); ⑤
const backgroundColor =
StyleSheet.flatten(styles.container).backgroundColor; ⑥
return (
<View style={styles.container}> ⑦
<View style={styles.box}> ⑧
<Button title={backgroundColor} ⑨
onPress={this.toggleTheme}/> ⑩
</View>
</View>
);
}
}
应用程序切换主题:请随意尝试并进一步探索。尝试将浅色主题改为不同的颜色。注意这有多么容易,因为颜色被定义为一个地方的可变常量。尝试将深色主题中的按钮标签改为与背景相同的颜色,而不是总是白色。尝试创建一个全新的主题,或者修改代码以支持许多不同的主题,而不是仅仅两个——享受乐趣!
4.2 样式化视图组件
现在你已经对 React Native 中的样式有了全面的了解,让我们更多地讨论个别样式。本章涵盖了你会经常使用的许多基本属性。在第五章中,我们将更深入地探讨并介绍你不会每天看到的样式以及特定平台的样式。但就目前而言,让我们专注于基础知识:在本节中,那就是 View 组件。View 组件是 UI 的主要构建块,也是理解以正确方式设置样式的重要组件之一。记住,View 元素在某种程度上类似于 HTML 的 div 标签,因为你可以用它来包裹其他元素,并在其中构建 UI 代码块。
随着你通过本章的学习,你将使用所学知识来构建一个真实组件:配置文件组件。构建配置文件组件将展示如何将一切组合在一起。图 4.3 展示了本节结束时组件的外观。在创建此组件的过程中,你将学习以下内容:
-
使用
borderWidth在配置文件容器周围创建边框 -
使用
borderRadius来圆滑那个边框的角落 -
通过使用组件宽度一半大小的
borderRadius创建看起来像圆的边框 -
使用边距和填充属性来定位一切
接下来的几节将教授你创建配置文件组件所需了解的样式技术。我们将从讨论如何设置组件的背景颜色开始。你将能够使用相同的技巧来设置配置文件组件的背景颜色。

图 4.3 在结构视图组件样式化之后,配置文件组件的外观。配置文件组件是一个圆角矩形,有一个圆形区域用于配置图像。
4.2.1 设置背景颜色
没有色彩的点缀,用户界面(UI)看起来会显得无聊和单调。你不需要色彩爆炸来使事物看起来有趣,但你确实需要一点。backgroundColor 属性设置元素的背景颜色。此属性接受一个字符串,可以是 表 4.1 中显示的属性之一。在屏幕上渲染文本时也提供相同的颜色。
表 4.1 支持的颜色格式
| 支持的颜色格式 | 示例 |
|---|---|
#rgb |
'#06f' |
#rgba |
'#06fc' |
#rrggbb |
'#0066ff' |
#rrggbbaa |
'#ff00ff00' |
rgb(number, number, number) |
'rgb(0, 102, 255)' |
rgb(number, number, number, alpha) |
'rgba(0, 102, 255, .5)' |
hsl(hue, saturation, lightness) |
'hsl(216, 100%, 50%)' |
hsla(hue, saturation, lightness, alpha) |
'hsla(216, 100%, 50%, .5)' |
| 透明背景 | 'transparent' |
| 任何 CSS3 指定的命名颜色(黑色、红色、蓝色等) | 'dodgerblue' |
幸运的是,支持的色彩格式与 CSS 支持的格式相同。我们不会深入细节,但鉴于这可能是您第一次看到这些格式中的某些格式,这里有一个简要的解释:
-
rgb代表红色、绿色和蓝色。您可以使用 0–255(或十六进制 00–ff)的刻度来指定红色、绿色和蓝色的值。数值越高,每种颜色的含量越多。 -
alpha与不透明度类似(0 是透明的,1 是实心的)。 -
hue代表在 360 度色轮上的 1 度,其中 0 是红色,120 是绿色,240 是蓝色。 -
saturation是从 0% 灰度到 100% 全色的颜色强度。 -
lightness是 0% 到 100% 之间的百分比。0% 是较暗的(接近黑色),100% 是较亮的(接近白色)。
您已经在之前的示例中看到了 backgroundColor 的应用,所以让我们在下一个示例中更进一步。为了使用您的新技能创建一些真实的东西,让我们开始构建 Profile Card。目前,它看起来不会太多,就像您在 图 4.4 中看到的那样——它只是一个 300 × 400 的彩色矩形。

图 4.4 一个简单的 300 × 400 彩色矩形,它是 Profile Card 组件的基础
下面的列表显示了初始代码。不要担心其中大部分与样式无关的事实。我们将逐一讲解每个部分,但您需要有一个起点。
列表 4.7 Profile Card 组件的初始框架
import React, { Component } from 'react';
import { StyleSheet, View} from 'react-native';
export default class App extends Component<{}> {
render() {
return (
<View style={styles.container}> ①
<View style={styles.cardContainer}/> ②
</View>
);
}
}
const profileCardColor = 'dodgerblue'; ③
const styles = StyleSheet.create({
container: { ④
flex: 1,
justifyContent: 'center',
alignItems: 'center'
},
cardContainer: { ⑤
backgroundColor: profileCardColor, ⑥
width: 300,
height: 400
}
});
第一个 View 组件是最外层的元素。它充当围绕其他所有内容的容器。它的唯一目的是在设备显示上居中子组件。第二个 View 组件将是 Profile Card 的容器。目前,它是一个 300 × 400 的彩色矩形。
4.2.2 设置边框属性
给组件应用背景颜色确实可以使它脱颖而出,但如果没有清晰的边框线来界定组件的边缘,它看起来就像是在空间中漂浮。组件之间的清晰界定将帮助用户理解如何与您的移动应用程序交互。
在组件周围添加边框是给屏幕元素一个具体、真实感觉的最佳方式。有很多 border 属性,但从概念上讲,只有四个:borderColor、borderRadius、borderStyle 和 borderWidth。这些属性适用于组件整体。
对于颜色和宽度,每个边都有单独的属性:borderTopColor、borderRightColor、borderBottomColor、borderLeftColor、borderTopWidth、borderRightWidth、borderBottomWidth 和 borderLeftWidth。对于边框半径,每个角落都有属性:borderTopRightRadius、borderBottomRightRadius、borderBottomLeftRadius 和 borderTopLeftRadius。但只有一个 borderStyle。
使用颜色、宽度和样式属性创建边框
要设置border,你必须首先设置borderWidth。borderWidth是边框的大小,它始终是一个数字。你可以设置一个适用于整个组件的borderWidth,或者选择你想要具体设置的borderWidth(顶部、右侧、底部或左侧)。你可以以许多不同的方式组合这些属性,以获得你喜欢的效果。查看图 4.5 以获取一些示例。

图 4.5 边框样式设置的多种组合示例
如你所见,你可以组合边框样式以创建边框效果的组合。下一个列表显示了这是多么容易做到。
列表 4.8 设置各种边框组合
import React, { Component } from 'react';
import { StyleSheet, Text, View} from 'react-native';
export default class App extends Component<{}> {
render() {
return (
<View style={styles.container}>
<Example style={{borderWidth: 1}}> ①
<Text>borderWidth: 1</Text>
</Example>
<Example style={{borderWidth: 3, borderLeftWidth: 0}}> ②
<Text>borderWidth: 3, borderLeftWidth: 0</Text>
</Example>
<Example style={{borderWidth: 3, borderLeftColor: 'red'}}> ③
<Text>borderWidth: 3, borderLeftColor: 'red'</Text>
</Example>
<Example style={{borderLeftWidth: 3}}> ④
<Text>borderLeftWidth: 3</Text>
</Example>
<Example style={{borderWidth: 1, borderStyle: 'dashed'}}> ⑤
<Text>borderWidth: 1, borderStyle: 'dashed'</Text>
</Example>
</View>
);
}
}
const Example = (props) => ( ⑥
<View style={[styles.example,props.style]}>
{props.children}
</View>
);
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center'
},
example: {
marginBottom: 15
}
});
当只指定borderWidth时,borderColor默认为'black',而borderStyle默认为'solid'。如果borderWidth或borderColor在组件级别被设置,这些属性可以通过使用更具体的属性如borderWidthLeft来覆盖;具体性优先于一般性。
使用边框半径创建形状
另一个可以产生良好效果的边框属性是borderRadius。现实世界中的许多物体都有直边,但很少有直线能传达任何风格感。你不会买一辆看起来像箱子的汽车。你希望你的车有漂亮的曲线,看起来流畅。使用borderRadius样式可以让你在应用中添加一些风格。通过在正确的位置添加曲线,你可以制作出许多不同、有趣的外形。
使用borderRadius,你可以定义元素上圆角边框的形状。正如你可能猜到的,borderRadius适用于整个组件。如果你设置了borderRadius但没有设置更具体的值,如borderTopLeftRadius,则所有四个角都会被圆化。查看图 4.6 了解如何将不同的边框圆化以创建酷炫效果。

图 4.6 各种边框半径组合的示例。示例 1:四个角都圆化的正方形。示例 2:右下两个角圆化,形成 D 形状。示例 3:相对的两个角圆化,看起来像树叶。示例 4:边框半径等于边长的一半,结果形成一个圆。
创建图 4.6 中的形状相对简单,如列表 4.9 所示。说实话,这段代码中最棘手的部分是确保文本不要太大或太长。我很快就会在列表 4.10 中展示我的意思。
列表 4.9 设置各种边框半径组合
import React, { Component } from 'react';
import { StyleSheet, Text, View} from 'react-native';
export default class App extends Component<{}> {
render() {
return (
<View style={styles.container}>
<Example style={{borderRadius: 20}}> ①
<CenteredText>
Example 1:{"\n"}4 Rounded Corners ②
</CenteredText>
</Example>
<Example style={{borderTopRightRadius: 60,
borderBottomRightRadius: 60}}> ③
<CenteredText>
Example 2:{"\n"}D Shape
</CenteredText>
</Example>
<Example style={{borderTopLeftRadius: 30,
borderBottomRightRadius: 30}}> ④
<CenteredText>
Example 3:{"\n"}Leaf Shape
</CenteredText>
</Example>
<Example style={{borderRadius: 60}}> ⑤
<CenteredText>
Example 4:{"\n"}Circle
</CenteredText>
</Example>
</View>
);
}
}
const Example = (props) => (
<View style={[styles.example,props.style]}>
{props.children}
</View>
);
const CenteredText = (props) => ( ⑥
<Text style={[styles.centeredText, props.style]}>
{props.children}
</Text>
);
const styles = StyleSheet.create({
container: { ⑦
flex: 1,
flexDirection: 'row',
flexWrap: 'wrap',
marginTop: 75
},
example: {
width: 120,
height: 120,
marginLeft: 20,
marginBottom: 20,
backgroundColor: 'grey',
borderWidth: 2,
justifyContent: 'center'
},
centeredText: { ⑧
textAlign: 'center',
margin: 10
}
});
特别注意居中文本的样式。你很幸运使用了margin: 10。如果你使用的是padding: 10,文本组件的背景会遮挡View组件下层的边框描边(见图 4.7)。

图 4.7 如果centeredText样式使用padding: 10而不是margin: 10来定位文本,图 4.6 将看起来像这样。小圆圈突出了文本组件的边界框与视图组件边框重叠的点。
默认情况下,Text组件继承其父组件的背景颜色。因为Text组件的边界框是一个矩形,所以背景会重叠在漂亮的圆角上。显然,使用margin属性可以解决这个问题,但也可以用另一种方法来补救。你可以在centeredText样式中添加backgroundColor: 'transparent'。使文本组件的背景透明可以让底层的边框显示出来,看起来恢复正常,如图 4.6 所示。

图 4.8 将边框属性整合到“个人资料卡”组件中,将 300 × 400 的彩色矩形转换成更接近你最终想要的“个人资料卡”组件的样子。
为你的“个人资料卡”组件添加边框
通过你对边框属性的新认识,你几乎可以完成“个人资料卡”组件的初始布局。仅使用上一节中的边框属性,你就可以将 300 × 400 的彩色矩形转换成更接近你想要的样子。图 4.8 展示了你可以通过图像和迄今为止学到的技术达到的程度。它包括一个用作个人照片占位符的图像;你可以在源代码中找到它。但圆圈是通过操纵边框半径来创建的,正如前例中所述。
显然,“个人资料卡”有一些布局问题,但你几乎就要完成了。我们将在下一节讨论如何使用边距和填充样式来正确地对齐所有内容。
列表 4.10 将边框属性整合到“个人资料卡”
import React, { Component } from 'react';
import { **Image**, StyleSheet, View} from 'react-native'; ①
export default class App extends Component<{}> {
render() {
return (
<View style={styles.container}>
<View style={styles.cardContainer}>
**<View style={styles.cardImageContainer}>**
**<Image style={styles.cardImage}**
**source={require('./user.png')}/>** ②
**</View>**
</View>
</View>
);
}
}
const profileCardColor = 'dodgerblue';
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center'
},
cardContainer: {
**borderColor: 'black',** ③
**borderWidth: 3,**
**borderStyle: 'solid',**
**borderRadius: 20,**
backgroundColor: profileCardColor,
width: 300,
height: 400
},
**cardImageContainer: {** ④
**backgroundColor: 'white',**
**borderWidth: 3,**
**borderColor: 'black',**
**width: 120,**
**height: 120,**
**borderRadius: 60,**
**},**
**cardImage: {** ⑤
**width: 80,**
**height: 80**
**}**
});
列表 4.10 与之前的“个人资料卡”代码(列表 4.7)之间的差异已被加粗,以突出增量更改。
4.2.3 指定边距和填充

图 4.9 常见的边距、填充和边框相互关系图示
你可以明确地将每个组件放置在屏幕上,并按照你想要的布局排列,但如果布局需要响应用户操作,这将非常繁琐。将项目相对于彼此定位更有意义,这样如果你移动一个组件,其他组件可以根据它们的相对位置做出响应。
边距样式允许您定义组件之间的关系。填充样式让您定义组件相对于其边框的相对位置。使用这些属性一起提供在布局组件时的很大灵活性。您将每天都会使用这些属性,因此理解它们的意义和作用非常重要。
从概念上讲,边距和填充与 CSS 中的工作方式完全相同。边距和填充与边框和内容区域的关系的传统描述仍然适用(见图 4.9)。
在处理边距和填充时,您可能会遇到功能性的问题。您可能会倾向于称它们为“怪癖”,但无论如何它们都是麻烦的。就大多数情况而言,View组件上的边距表现良好,并在 iOS 和 Android 上工作。填充在操作系统之间的工作方式略有不同。在撰写本文时,在 Android 环境中填充文本组件根本不起作用;我怀疑这将在即将发布的版本中改变。
使用边距属性
在布局组件时,首先要解决的问题之一是组件之间的距离。为了避免为每个组件指定一个距离,您需要一种指定相对位置的方法。margin属性允许您定义组件的周界,这决定了元素与上一个或父组件的距离。以这种方式阐述布局允许容器确定组件相对于彼此的位置,而不是您必须计算每个组件的位置。
可用的边距属性有margin、marginTop、marginRight、marginBottom和marginLeft。如果只设置了通用的margin属性,而没有其他更具体的值,如marginLeft或marginTop,则该值应用于组件的所有侧面(顶部、右侧、底部和左侧)。如果同时指定了margin和更具体的margin属性(例如marginLeft),则更具体的margin属性具有优先权。它的工作方式与边框属性完全相同。让我们应用一些这些样式:见图 4.10。

图 4.10 应用边距到组件的示例。在 iOS 中,示例 A 没有应用边距。示例 B 应用了顶部边距。示例 C 应用了顶部和左侧边距。示例 D 应用了负顶部和负左侧边距。在 Android 中,负边距的行为略有不同:组件被父容器裁剪。
所有边距都按预期定位组件,但请注意,当应用负边距时,Android 设备会裁剪组件。如果你计划同时支持 iOS 和 Android,从项目开始就应在每个设备上进行测试。不要只在 iOS 上开发,并认为你设计的样式在 Android 上会有相同的行为。列表 4.11 展示了图 4.10 中示例的代码。
列表 4.11 将各种边距应用于组件
import React, { Component } from 'react';
import { StyleSheet, Text, View} from 'react-native';
export default class App extends Component<{}> {
render() {
return (
<View style={styles.container}>
<View style={styles.exampleContainer}>
<Example> ①
<CenteredText>A</CenteredText>
</Example>
</View>
<View style={styles.exampleContainer}>
<Example style={{marginTop: 50}}> ②
<CenteredText>B</CenteredText>
</Example>
</View>
<View style={styles.exampleContainer}>
<Example style={{marginTop: 50, marginLeft: 10}}> ③
<CenteredText>C</CenteredText>
</Example>
</View>
<View style={styles.exampleContainer}>
<Example style={{marginLeft: -10, marginTop: -10}}> ④
<CenteredText>D</CenteredText>
</Example>
</View>
</View>
);
}
}
const Example = (props) => (
<View style={[styles.example,props.style]}>
{props.children}
</View>
);
const CenteredText = (props) => (
<Text style={[styles.centeredText, props.style]}>
{props.children}
</Text>
);
const styles = StyleSheet.create({
container: {
alignItems: 'center',
flex: 1,
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'center',
marginTop: 75
},
exampleContainer: {
borderWidth: 1,
width: 120,
height: 120,
marginLeft: 20,
marginBottom: 20,
},
example: {
width: 50,
height: 50,
backgroundColor: 'grey',
borderWidth: 1,
justifyContent: 'center'
},
centeredText: {
textAlign: 'center',
margin: 10
}
});
使用填充属性
你可以将边距视为元素之间的距离,但填充表示元素内容与其边框之间的空间。当指定填充时,它允许组件的内容不与边框对齐。在 图 4.9 中,backgroundColor 属性从组件边缘渗透到边框,这是由 padding 定义的空隙。可用于 padding 的属性有 padding、paddingLeft、paddingRight、paddingTop 和 paddingBottom。如果只设置了主要的 padding 属性而没有其他更具体的值,如 paddingLeft 或 paddingTop,则该值将传递到组件的所有侧面(顶部、右侧、底部和左侧)。如果同时指定了 padding 和更具体的 padding 属性,如 paddingLeft,则更具体的 padding 属性具有优先级。这种行为与边框和边距完全相同。
我们不如创建一个新的示例来展示填充与边距的不同,而是重用 列表 4.11 中的代码并进行一些调整。将示例组件上的 margin 样式更改为 padding 样式,并在 Text 组件周围添加边框并更改它们的背景颜色。图 4.11 展示了最终的结果。
列表 4.12 将 列表 4.11 修改为用填充替换边距
import React, { Component } from 'react';
...
<View style={styles.container}>
<View style={styles.exampleContainer}>
<Example style={{}}> ①
<CenteredText>A</CenteredText>
</Example>
</View>
<View style={styles.exampleContainer}>
<Example style={{**paddingTop**: 50}}> ②
<CenteredText>B</CenteredText>
</Example>
</View>
<View style={styles.exampleContainer}>
<Example style={{**paddingTop**: 50, **paddingLeft**: 10}}> ③
<CenteredText>C</CenteredText>
</Example>
</View>
<View style={styles.exampleContainer}>
<Example style={{**paddingLeft**: -10, **paddingTop**: -10}}> ④
<CenteredText>D</CenteredText>
</Example>
</View>
</View>
...
},
centeredText: {
textAlign: 'center',
margin: 10,
**borderWidth: 1,** ⑤
**backgroundColor: 'lightgrey'**
}
});

图 4.11 将前一个示例中的边距样式更改为填充样式。示例 A 没有填充,看起来与没有应用边距时相同。示例 B 显示了应用了 paddingTop 的组件。示例 C 与示例 B 相同,但它还应用了 paddingLeft。示例 D 将负填充值应用于 paddingTop 和 paddingLeft,这些值将被忽略。
与指定组件与其父组件之间空间的不同,填充是从组件的边框应用到其子组件。在示例 B 中,填充是从顶部边框计算的,这会将 Text 组件 B 从顶部边框向下推。示例 C 添加了一个 paddingLeft 值,这也会将 Text 组件 C 从左侧边框向内推。示例 D 将负填充值应用于 paddingTop 和 paddingLeft。
可以得出一些有趣的观察。示例 B 和示例 C 在 Android 设备上都被裁剪了。示例 C 的 Text 组件的宽度被压缩,示例 D 中的 padding 的负值被忽略。
4.2.4 使用位置放置组件
到目前为止,我们所看到的一切都是相对于另一个组件定位的,这是默认的布局位置。有时利用绝对定位并将组件放置在您想要的确切位置是有益的。React Native 中 position 样式的实现与 CSS 类似,但选项较少。默认情况下,所有元素都是相对于彼此布局的。如果 position 设置为 absolute,则元素相对于其父元素进行布局。position 可用的属性是 relative(默认位置)和 absolute。
CSS 有其他值,但在 React Native 中只有这两个。当使用 absolute 定位时,以下属性也是可用的:top、right、bottom 和 left。
让我们通过一个简单的示例来演示相对定位和绝对定位之间的区别。在 CSS 中,定位可能会变得非常复杂,但在 React Native 中,“默认情况下所有内容都具有相对定位”使得定位项目变得容易得多。在 图 4.12 中,块 A、B 和 C 在一行中相对彼此布局。没有任何边距或填充,它们一个接一个地排列。Block D 是 ABC 行的兄弟,这意味着 ABC 行和 Block D 的父容器是主容器。
Block D 被设置为 {position: 'absolute', right: 0, bottom: 0}, 因此它被定位在其容器右下角。Block E 也被设置为 {position: 'absolute', right: 0, bottom: 0}, 但它的父容器是 block B,这意味着 block E 是相对于 block B 绝对定位的。相反,Block E 出现在 block B 的右下角。列表 4.13 展示了这个示例的代码。

图 4.12 一个显示块 A、B 和 C 相对彼此布局的示例。Block D 有绝对位置 right: 0 和 bottom: 0。Block E 也有绝对位置 right: 0 和 bottom: 0,但它的父容器是 block B 而不是主容器,而 D 的父容器是主容器。
列表 4.13 相对和绝对定位比较
import React, { Component } from 'react';
import { StyleSheet, Text, View} from 'react-native';
export default class App extends Component<{}> {
render() {
return (
<View style={styles.container}>
<View style={styles.row}> ①
<Example>
<CenteredText>A</CenteredText>
</Example>
<Example>
<CenteredText>B</CenteredText>
<View style={styles.tinyExample, [ ②
{position: 'absolute',
right: 0,
bottom: 0}]}>
<CenteredText>E</CenteredText>
</View>
</Example>
<Example>
<CenteredText>C</CenteredText>
</Example>
</View>
<Example style={{position: 'absolute', ③
right: 0, bottom: 0}}>
<CenteredText>D</CenteredText>
</Example>
</View>
);
}
}
const Example = (props) => (
<View style={[styles.example,props.style]}>
{props.children}
</View>
);
const CenteredText = (props) => (
<Text style={[styles.centeredText, props.style]}>
{props.children}
</Text>
);
const styles = StyleSheet.create({
container: {
width: 300,
height: 300,
margin: 40,
marginTop: 100,
borderWidth: 1
},
row: { ④
flex: 1,
flexDirection: 'row'
},
example: {
width: 100,
height: 100,
backgroundColor: 'grey',
borderWidth: 1,
justifyContent: 'center'
},
tinyExample: {
width: 30,
height: 30,
borderWidth: 1,
justifyContent: 'center',
backgroundColor: 'lightgrey'
},
centeredText: {
textAlign: 'center',
margin: 10
}
});
我们已经完成了 View 组件的基本样式。你已经了解了一些布局技术:边距、填充和位置。让我们回顾一下 Profile Card 组件,并修复那些还没有正确布局的部分。
4.2.5 Profile Card 定位
以下列表展示了需要修改 列表 4.10 的代码,以正确地间隔圆圈和用户图像,并使一切居中。图 4.13 展示了结果。
列表 4.14 修改 Profile Card 样式以修复布局
...
cardContainer: {
**alignItems: 'center',** ①
borderColor: 'black',
borderWidth: 3,
borderStyle: 'solid',
borderRadius: 20,
backgroundColor: profileCardColor,
width: 300,
height: 400
},
cardImageContainer: {
**alignItems: 'center',** ②
backgroundColor: 'white',
borderWidth: 3,
borderColor: 'black',
width: 120,
height: 120,
borderRadius: 60,
**marginTop: 30,** ③
**paddingTop: 15** ④
},
...

图 4.13 在所有View组件正确排列后的个人资料卡组件
现在,个人资料卡的View组件已经就位。通过使用到目前为止讨论的技术,你已经为组件构建了一个不错的基座,但你还没有完成。你需要添加关于这个人的信息:姓名、职业和简短的个人简介。所有这些信息都是基于文本的,所以接下来你将学习如何对Text组件进行样式设计。
4.3 文本组件的样式
在本节中,我们将讨论如何对Text组件进行样式设计。在你掌握了如何使文本看起来很棒之后,我们将再次查看个人资料卡并添加一些关于用户的信息。图 4.14 是带有用户姓名、职业和简短个人描述的完成后的个人资料卡组件。但在我们重新审视个人资料卡之前,让我们看看将使你能够完成构建它的样式技术。

图 4.14 带有用户姓名、职业和简短个人描述的完成后的个人资料卡
4.3.1 文本组件与 View 组件对比
除了我们尚未涉及的弹性属性外,大多数适用于View元素的样式也可以在Text元素上按预期工作。Text元素可以有边框和背景,并且会受到布局属性如margin、padding和position的影响。
反过来则不然。大多数Text元素可以使用的样式对View元素不起作用,这是完全合理的。如果你曾经使用过文字处理器,你知道你可以为文本使用不同的字体并更改字体颜色;你可以调整文本大小、加粗和斜体;你还可以应用下划线等装饰。
在我们深入文本特定样式之前,让我们谈谈颜色,这是Text和View组件共有的样式。然后你将使用颜色以及迄今为止所学的一切来开始向个人资料卡添加文本。
文本着色
color属性以与View组件完全相同的方式应用于Text组件。正如预期的那样,此属性指定了Text元素中文本的颜色。所有在表 4.1 中列出的颜色格式仍然适用——甚至包括transparent,尽管我想象不出这有什么好处。默认情况下,文本颜色为黑色。
图 4.14 展示了个人资料卡中的三个Text元素:
-
姓名
-
职业
-
个人简介
使用你已经学到的知识,你可以居中文本、定位文本,将姓名的颜色从黑色改为白色,并添加一个简单的边框来区分职业和描述。图 4.15 展示了通过应用你的工具库中的技术你最终会得到什么。
到目前为止,你应该能够跟随列表 4.15 并理解所有发生的事情。如果你没有跟上,不要感到难过——如果需要,可以回过头去重新阅读适当的章节。

图 4.15 使用文本样式默认值和将名称的颜色属性设置为白色添加文本元素的个人资料卡
列表 4.15 向个人资料卡添加文本
import React, { Component } from 'react';
import { Image, StyleSheet, **Text**, View} from 'react-native'; ①
export default class App extends Component<{}> {
render() {
return (
<View style={styles.container}>
<View style={styles.cardContainer}>
<View style={styles.cardImageContainer}>
<Image style={styles.cardImage}
source={require('./user.png')}/>
</View>
**<View>**
**<Text style={styles.cardName}>** ②
**John Doe**
**</Text>**
**</View>**
**<View style={styles.cardOccupationContainer}>** ③
**<Text style={styles.cardOccupation}>** ④
**React Native Developer**
**</Text>**
**</View>**
**<View>**
**<Text style={styles.cardDescription}>** ⑤
**John is a really great JavaScript developer. He**
**loves using JS to build React Native applications**
**for iOS and Android.**
**</Text>**
**</View>**
</View>
</View>
);
}
}
const profileCardColor = 'dodgerblue';
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center'
},
cardContainer: {
alignItems: 'center',
borderColor: 'black',
borderWidth: 3,
borderStyle: 'solid',
borderRadius: 20,
backgroundColor: profileCardColor,
width: 300,
height: 400
},
cardImageContainer: {
alignItems: 'center',
backgroundColor: 'white',
borderWidth: 3,
borderColor: 'black',
width: 120,
height: 120,
borderRadius: 60,
marginTop: 30,
paddingTop: 15
},
cardImage: {
width: 80,
height: 80
},
**cardName: {** ⑥
**color: 'white',**
**marginTop: 30,**
**},**
**cardOccupationContainer: {** ⑦
**borderColor: 'black',**
**borderBottomWidth: 3**
**},**
**cardOccupation: {** ⑧
**marginTop: 10,**
**marginBottom: 10,**
**},**
**cardDescription: {** ⑨
**marginTop: 10,**
**marginRight: 40,**
**marginLeft: 40,**
**marginBottom: 10**
**}**
});
在这个阶段,你已经拥有了个人资料卡的完整内容,但它看起来相当简单。在接下来的几节中,我们将讨论如何设置字体属性并为文本添加装饰样式。
4.3.2 字体样式
如果你曾经使用过文字处理软件或编写过具有丰富文本功能的电子邮件,你曾经能够更改字体、增加或减少字体大小、加粗或斜体化文本等。这些是你将在本节中学到的如何更改的样式。通过调整这些样式,你可以使文本对最终用户更具吸引力和吸引力。我们将讨论以下属性:fontFamily、fontSize、fontStyle 和 fontWeight。
指定字体族
fontFamily 属性看似简单。如果你坚持使用默认值,那么很容易;但如果你想使用特定的字体,你可能会很快遇到麻烦。iOS 和 Android 都自带一组默认字体。对于 iOS,大量可用的字体可以开箱即用。对于 Android,有 Roboto,一个等宽字体,以及一些简单的衬线和无衬线变体。要获取 React Native 中开箱即用的 Android 和 iOS 字体的完整列表,请访问 github.com/dabit3/react-native-fonts。
如果你想在应用程序中使用等宽字体,你不能指定以下任何一个:
-
fontFamily: 'monospace'——iOS 上不支持'monospace'选项,因此在该平台上你会得到错误“未识别的字体族'monospace'。”但在 Android 上,字体将正确渲染而没有任何问题。与 CSS 不同,你不能向fontFamily属性提供多个字体。 -
fontFamily: 'American Typewriter, monospace'——你会在 iOS 上再次遇到错误,“未识别的字体族'American Typewriter, monospace'。”但在 Android 上,当你提供一个它不支持的字体时,它会回退到默认字体。这可能在 Android 的每个版本中都不一定成立,但可以肯定的是,这两种方法都不会奏效。
如果你想要使用不同的字体,你将不得不使用 React Native 的 Platform 组件。我们将在第十章中更详细地讨论 Platform,但我想要介绍它,这样你就可以看到如何解决这个困境。图 4.16 显示了在 iOS 上渲染的 American Typewriter 字体和在 Android 上使用的通用等宽字体。
下面的列表显示了生成此示例的代码。请注意,fontFamily 是如何使用 Platform.select 设置的。

图 4.16 在 iOS 和 Android 上渲染等宽字体的示例
列表 4.16 在 iOS 和 Android 上显示等宽字体
import React, { Component } from 'react';
import { Platform, StyleSheet, Text, View} from 'react-native'; ①
export default class App extends Component<{}> {
render() {
return (
<View style={styles.container}>
<View style={styles.row}>
<CenteredText>
I am a monospaced font on both platforms
</CenteredText>
<BottomText>
{Platform.OS} ②
</BottomText>
</View>
</View>
);
}
}
const CenteredText = (props) => (
<Text style={[styles.centeredText, props.style]}>
{props.children}
</Text>
);
const BottomText = (props) => (
<CenteredText style={{position: 'absolute', bottom: 0}, [ ③
props.style]}>
{props.children}
</CenteredText>
);
const styles = StyleSheet.create({
container: {
width: 300,
height: 300,
margin: 40,
marginTop: 100,
borderWidth: 1
},
row: {
alignItems: 'center',
flex: 1,
flexDirection: 'row',
justifyContent: 'center'
},
centeredText: {
textAlign: 'center',
margin: 10,
fontSize: 24,
...Platform.select({ ④
ios: {
fontFamily: 'American Typewriter'
},
android: {
fontFamily: 'monospace'
}
})
}
});
此示例展示了如何根据操作系统选择字体,但您可用的字体集合仍然仅限于 React Native 默认提供的字体。您可以使用字体文件(TTF、OTF 等)将自定义字体添加到项目中,并将它们作为资源链接到您的应用程序中。理论上这个过程很简单,但成功与否很大程度上取决于操作系统和所使用的字体文件。我想让您知道这是可行的,但如果您想尝试,请打开您选择的搜索引擎并查找 react-native link。
使用 fontSize 调整文本大小
fontSize 非常简单:它调整 Text 元素中文字的大小。您已经使用过很多次了,所以我们不会深入细节,除了默认的 fontSize 是 14。
更改字体样式
您可以使用 fontStyle 将字体样式更改为斜体。默认值为 'normal'。目前只有两个选项:'normal' 和 'italic'。
指定字体粗细
fontWeight 指的是字体的粗细。默认值为 'normal' 或 '400'。fontWeight 的选项有 'normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', 和 '900'。数值越小,文字越细;数值越大,文字越粗。
现在您已经知道如何更改字体样式,您几乎可以完成个人资料卡组件。让我们更改一些字体样式,看看您能接近最终产品到什么程度,如图 4.17 所示。下一列表将展示如何从 列表 4.16 更改样式以实现这种外观。
列表 4.17 在个人资料卡中为文本元素设置字体样式
…
cardName: {
color: 'white',
fontWeight: 'bold', ①
fontSize: 24, ②
marginTop: 30,
},
…
cardOccupation: {
fontWeight: 'bold', ③
marginTop: 10,
marginBottom: 10,
},
cardDescription: {
fontStyle: 'italic', ④
marginTop: 10,
marginRight: 40,
marginLeft: 40,
marginBottom: 10
}
…

图 4.17 应用了字体样式的姓名、职业和描述文本的个人资料卡
修改姓名、职业和描述文本的字体样式有助于区分各个部分,但姓名仍然不够突出。下一节将介绍一些装饰性的文本样式以及如何使用这些技术使姓名在个人资料卡中更加突出。
4.3.3 使用装饰性文本样式
在本节中,您将超越更改字体样式的基础,并开始将装饰性样式应用于文本。我将向您展示如何进行下划线和删除线文本,以及添加阴影等技术。这些技术可以为应用程序添加很多视觉多样性,并帮助文本元素彼此区分。
在本节中,我们将介绍以下属性:
-
iOS 和 Android —
lineHeight,textAlign,textDecorationLine,textShadowColor,textShadowOffset, 和textShadowRadius -
仅限 Android —
textAlignVertical -
仅限 iOS —
letterSpacing,textDecorationColor,textDecorationStyle, 和writingDirection。
注意,一些属性仅适用于一个操作系统或另一个。可以分配给属性的某些值也是操作系统特定的。这一点很重要,尤其是如果您依赖于特定的样式来突出屏幕上特定的文本元素。
指定文本元素的高度
lineHeight 指定 Text 元素的高度。图 4.18 和 列表 4.18 展示了在 iOS 和 Android 上这种行为的不同。将 100 的 lineHeight 应用到 Text B 元素:该行的长度明显大于其他行。同时请注意 iOS 和 Android 在行内定位文本的不同方式。在 Android 上,文本位于行的底部。

图 4.18 在 iOS 和 Android 中使用 lineHeight 的示例。
列表 4.18 在 iOS 和 Android 中将 lineHeight 应用到 Text 元素
import React, { Component } from 'react';
import { Platform, StyleSheet, Text, View} from 'react-native';
export default class App extends Component<{}> {
render() {
return (
<View style={styles.container}>
<TextContainer>
<LeftText>Text A</LeftText>
</TextContainer>
<TextContainer>
<LeftText style={{lineHeight: 100}}> ①
Text B
</LeftText>
</TextContainer>
<TextContainer>
<LeftText>Text C</LeftText>
</TextContainer>
<TextContainer>
<LeftText>{Platform.OS}</LeftText>
</TextContainer>
</View>
);
}
}
const LeftText = (props) => (
<Text style={[styles.leftText, props.style]}>
{props.children}
</Text>
);
const TextContainer = (props) => (
<View style={[styles.textContainer, props.style]}>
{props.children}
</View>
);
const styles = StyleSheet.create({
container: {
width: 300,
height: 300,
margin: 40,
marginTop: 100
},
textContainer: {
borderWidth: 1 ②
},
leftText: {
fontSize: 20
}
});
水平对齐文本
textAlign 指的是元素中文本的水平对齐方式。textAlign 的选项有 'auto'、'center'、'right'、'left' 和 'justify'(仅限 iOS)。
为文本添加下划线或贯穿线
使用 textDecorationLine 属性为给定文本添加下划线或贯穿线。textDecorationLine 的选项有 'none'、'underline'、'line-through' 和 'underline line-through'。默认值是 'none'。当您指定 'underline line-through' 时,引号中的值由单个空格分隔。
文本装饰样式(仅限 iOS)
iOS 支持一些 Android 不支持的文本装饰样式。第一个是 textDecorationColor,它允许您为 textDecorationLine 设置颜色。iOS 还支持对线条本身进行样式化。在 Android 上,线条始终是实线,但在 iOS 上 textDecorationStyle 允许您指定 'solid'、'double'、'dotted' 和 'dashed'。Android 将忽略这些额外的样式。
要使用额外的 iOS 装饰样式,请与主要的 textDecorationLine 样式一起指定。例如:
`{`
`textDecorationLine: 'underline',`
`textDecorationColor: 'red',`
`textDecorationStyle: 'double'`
`}`
为文本添加阴影
您可以使用 textShadowColor、textShadowOffset 和 textShadowRadius 属性为 Text 元素添加阴影。要创建阴影,您需要指定三件事:
-
颜色
-
偏移量
-
半径
偏移量指定了阴影相对于投掷阴影的组件的位置。半径基本上定义了阴影的模糊程度。您可以指定一个文本阴影如下:
`{`
`textShadowColor: 'red',`
`textShadowOffset: {width: -2, height: -2},`
`textShadowRadius: 4`
`}`
控制字母间距(仅限 iOS)
letterSpacing 指定文本字符之间的间距。这不是您每天都会使用的东西,但它可以产生一些有趣的视觉效果。请注意,它仅限 iOS,因此如果您需要,请使用它。
文本样式的示例
我们在本节中介绍了很多不同的样式。图 4.19 展示了应用于 Text 组件的各种样式。
下面是 图 4.19 中每个示例所使用的样式的快速概述:
-
A 使用
{fontStyle: 'italic'}创建斜体文本。 -
B 显示带有下划线和贯穿文本的文本装饰。这种样式的代码是
{textDecorationLine: 'underline line-through'}。 -
C 通过应用一些仅适用于 iOS 的文本样式
{textDecorationColor: 'red', textDecorationStyle: 'dotted'}扩展了示例 B。注意这些样式在 Android 中没有效果。 -
D 使用
{textShadowColor: 'red', textShadowOffset: {width: -2, height: -2}, textShadowRadius: 4}应用阴影。 -
E 使用仅适用于 iOS 的
{letterSpacing: 5},这不会影响 Android。 -
文本 ios 和 android 使用
{textAlign: 'center', fontWeight: 'bold'}进行样式化。

图 4.19 文本组件样式的各种示例
以 列表 4.19 为起点,看看修改样式如何影响结果。
列表 4.19 Text 组件的样式示例
import React, { Component } from 'react';
import { Platform, StyleSheet, Text, View} from 'react-native';
export default class App extends Component<{}> {
render() {
return (
<View style={styles.container}>
<LeftText style={{fontStyle: 'italic'}}>
A) Italic
</LeftText>
<LeftText style={{textDecorationLine: 'underline line-through'}}>
B) Underline and Line Through
</LeftText>
<LeftText style={{textDecorationLine: 'underline line-through',
textDecorationColor: 'red',
textDecorationStyle: 'dotted'}}>
C) Underline and Line Through
</LeftText>
<LeftText style={{textShadowColor: 'red',
textShadowOffset: {width: -2, height: -2},
textShadowRadius: 4}}>
D) Text Shadow
</LeftText>
<LeftText style={{letterSpacing: 5}}>
E) Letter Spacing
</LeftText>
<LeftText style={{textAlign: 'center', fontWeight: 'bold'}}>
{Platform.OS}
</LeftText>
</View>
);
}
}
const LeftText = (props) => (
<Text style={[styles.leftText, props.style]}>
{props.children}
</Text>
);
const styles = StyleSheet.create({
container: {
width: 300,
height: 300,
margin: 40,
marginTop: 100
},
leftText: {
fontSize: 20,
paddingBottom: 10
}
});

图 4.20 完成的个人资料卡片示例。已添加有关使用本节中介绍的文本样式技术的人的文本信息。
现在您已经知道如何创建阴影效果,让我们给人的名字添加阴影,使其从其他文本中突出出来。图 4.20 显示了期望的结果。
下一个提供的是个人资料卡片的完整代码。您只需添加一小段代码来设置名称的文本阴影。
列表 4.20 完成的个人资料卡片示例
import React, { Component } from 'react';
import { Image, StyleSheet, Text, View} from 'react-native';
export default class App extends Component<{}> {
render() {
return (
<View style={styles.container}>
<View style={styles.cardContainer}>
<View style={styles.cardImageContainer}>
<Image style={styles.cardImage}
source={require('./user.png')}/>
</View>
<View>
<Text style={styles.cardName}>
John Doe
</Text>
</View>
<View style={styles.cardOccupationContainer}>
<Text style={styles.cardOccupation}>
React Native Developer
</Text>
</View>
<View>
<Text style={styles.cardDescription}>
John is a really great JavaScript developer.
He loves using JS to build React Native
applications for iOS and Android.
</Text>
</View>
</View>
</View>
);
}
}
const profileCardColor = 'dodgerblue';
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center'
},
cardContainer: {
alignItems: 'center',
borderColor: 'black',
borderWidth: 3,
borderStyle: 'solid',
borderRadius: 20,
backgroundColor: profileCardColor,
width: 300,
height: 400
},
cardImageContainer: {
alignItems: 'center',
backgroundColor: 'white',
borderWidth: 3,
borderColor: 'black',
width: 120,
height: 120,
borderRadius: 60,
marginTop: 30,
paddingTop: 15
},
cardImage: {
width: 80,
height: 80
},
cardName: {
color: 'white',
fontWeight: 'bold',
fontSize: 24,
marginTop: 30,
**textShadowColor: 'black',** ①
**textShadowOffset: {** ②
**height: 2,**
**width: 2**
**},**
**textShadowRadius: 3** ③
**},**
cardOccupationContainer: {
borderColor: 'black',
borderBottomWidth: 3
},
cardOccupation: {
fontWeight: 'bold',
marginTop: 10,
marginBottom: 10,
},
cardDescription: {
fontStyle: 'italic',
marginTop: 10,
marginRight: 40,
marginLeft: 40,
marginBottom: 10
}
});
您可以对这个基本示例进行很多改进,但目标是展示理解样式概念的好处。您不必是一位出色的图形设计师就能制作出美观的组件——一些简单的技巧就能使您的应用程序看起来很棒。
在本章中,我们涵盖了大量的内容,但信不信由你,这只是一个简短的介绍!我们将在第五章探索一些额外的先进主题。
概述
-
样式可以内联应用于组件,或者通过创建可以被组件引用的样式表来应用。
-
样式应该在组件定义之后或外部化到单独的 styles.js 文件中与组件组织在同一文件中。
-
样式是代码。JavaScript 是一个完整的语言,具有变量和函数,这为传统 CSS 提供了许多优势。
-
View组件是 UI 的主要构建块,并且它们具有许多样式属性。 -
您可以使用边框以多种方式增强组件的外观。您甚至可以使用边框创建形状,例如圆形。
-
您可以使用边距和填充将组件相对于彼此定位。
-
绝对定位允许您将组件放置在父容器内的任何位置。
-
根据您设置的边框、边距和填充方式,Android 设备上可能会发生裁剪。
-
指定除默认字体以外的字体可能很棘手。使用
Platform组件选择适合操作系统的适当字体。 -
使用通用的字体样式,如颜色、大小和粗细,来改变
Text组件的大小和外观。 -
操作系统之间存在渲染差异,例如 iOS 和 Android 之间行高的行为不同。
-
文本装饰样式可以为文本添加下划线或阴影效果。可用的样式集合因操作系统而异。
5
深入了解样式
本章 涵盖
-
特定平台的尺寸和样式
-
为组件添加阴影效果
-
在 x 轴和 y 轴上移动和旋转组件
-
缩放和倾斜组件
-
使用 flexbox 进行布局
第四章介绍了样式 React Native 组件。它展示了如何样式化View和Text组件,这些样式你可能会每天使用,并且主要影响组件的外观。本章继续讨论并深入探讨特定平台的样式;阴影效果;使用变换如平移、旋转、缩放和倾斜来操作组件;以及使用 flexbox 动态布局组件。
其中一些主题可能感觉熟悉。你在第四章的几个示例中使用了特定平台的样式和 flexbox。我们没有详细讲解,但你已经在几个代码示例中看到了它们。
本章扩展了这些主题。变换使你能够在二维或三维中操作组件。你可以将组件从一处位置平移到另一处,旋转组件,将组件缩放到不同的大小,以及倾斜组件。变换本身很有用,但在第七章中,它将扮演一个更大的角色,第七章将详细讨论动画。
我们将继续讨论平台之间的差异,并更深入地探讨 flexbox。因为 flexbox 是一个基本概念,正确理解它对于你能够在 React Native 中创建布局和 UI 很重要。你可能会在创建的每个应用程序中使用 flexbox。你将使用一些新的样式技术来继续构建上一章中的ProfileCard示例的新功能。
5.1 特定平台的尺寸和样式
你已经看到了如何使用Platform.select实用函数来选择仅在 iOS 或 Android 上可用的字体。你使用Platform.select来选择每个平台支持的等宽字体。你可能当时并没有太在意,但记住你正在为两个不同的平台开发是很重要的。你应用于组件的样式在两个操作系统之间或甚至在 iOS 和 Android 的不同版本之间可能看起来或表现不同。
你不是为单个设备编码;你甚至不是为单个操作系统编码。React Native 的美丽之处在于你使用 JavaScript 创建可以在 iOS 和 Android 上运行的应用程序。如果你查看 React Native 文档,你会看到许多以 IOS 或 Android 结尾的组件,例如ProgressBarAndroid、ProgressViewIOS和ToolbarAndroid,因此样式也可以是平台特定的,这并不令人惊讶。
你可能没有注意到,你从未为任何东西指定过像素大小,比如width: 300与width: '300px'。这是因为 iOS 和 Android 操作系统之间的大小概念本身就不同。
5.1.1 像素、点和 DP
尺寸可能是一个令人困惑的话题,但如果你需要绝对精确地定位屏幕上的组件时,这一点很重要。即使你不想制作高保真度的布局,了解这些概念也会很有用,以防你在不同设备上的布局中出现小的差异。
让我们从一开始,定义一个像素。一个 像素 是显示上可编程颜色的最小单位。一个像素通常由红色、绿色和蓝色(RGB)颜色成分组成。通过操纵每个 RGB 值的强度,像素会发出你看到的颜色。直到你开始查看显示器的物理属性:屏幕尺寸、分辨率和每英寸点数,像素才告诉你任何信息。
屏幕尺寸 是屏幕对角线的测量值,从一个角落到另一个角落。例如,iPhone 的原始屏幕尺寸为 3.5 英寸,而 iPhone X 的屏幕尺寸为 5.8 英寸。尽管 iPhone X 的尺寸明显更大,但这个尺寸本身并没有什么意义,直到你了解有多少像素可以适应这个屏幕尺寸。
分辨率 是显示中的像素数,通常以设备宽度和高度的像素数来表示。原始 iPhone 的分辨率为 320 × 480,而 iPhone X 的分辨率为 1125 × 2436。
屏幕尺寸和分辨率可以用来计算像素密度:每英寸像素数(PPI)。你经常会看到这个值被表示为每英寸点数(DPI),这是一个来自印刷世界的遗留术语,其中在页面上打印了一个颜色点。PPI 和 DPI 经常被互换使用,尽管这并不完全正确,所以如果你在屏幕的上下文中看到 DPI,要知道 PPI 才是真正被讨论的内容。
PPI 给你一个图像清晰度的度量。想象一下,如果两个屏幕具有相同的分辨率,320 × 480(半 VGA),相同的图像在 3.5 英寸的 iPhone 显示屏上与 17 英寸 HVGA 监视器显示屏上会有什么不同?相同的图像在 iPhone 上看起来会更清晰,因为它的 PPI 是 163,而 CRT 监视器的 PPI 是 34。你可以在原始 iPhone 的相同物理空间中放入近五倍的信息。表 5.1 比较了两个设备的对角线尺寸、分辨率和 PPI。
表 5.1 17 英寸 HVGA 监视器的 PPI 与原始 iPhone 的 PPI 比较
| HVGA 监视器 | 原始 iPhone | |
|---|---|---|
| 对角线尺寸 | 17 英寸 | 3.5 英寸 |
| 分辨率 | 320 × 480 | 320 × 480 |
| PPI | 34 | 163 |
这有什么关系呢?因为 iOS 和 Android 都不是使用实际的物理测量值来将内容渲染到设备的屏幕上。iOS 使用一个抽象的点作为测量单位,而 Android 使用一个类似的抽象密度无关像素作为测量单位。
当 iPhone 4 出现时,它与前辈们具有相同的物理尺寸;但它有一个新潮的 Retina 屏幕,分辨率为 640 × 960,是原始设备的四倍。如果 iPhone 以 1:1 的比例渲染现有应用程序的图像,那么所有内容在新的 Retina 显示屏上都会以四分之一的大小绘制。对于苹果来说,做出这样的改变并破坏所有现有应用程序是一个疯狂的建议。
相反,苹果引入了逻辑概念上的“点”。点是一个可以独立于设备分辨率进行缩放的距离单位,因此一个 320 × 480 的图像,在原始 iPhone 上占据了整个屏幕,可以被放大 2 倍以完全适应 Retina 显示屏。图 5.1 提供了几个 iPhone 型号的像素密度可视化。
原始 iPhone 的 163 PPI 是 iOS 点的基准。iOS 点是一英寸的 1/163。不深入细节,Android 使用一个类似的度量单位,称为“设备无关像素”(DIP,通常缩写为 DP)。Android DP 是一英寸的 1/160。
当在 React Native 中定义样式时,你使用逻辑上的像素概念,iOS 上的点,以及 Android 上的 DP。当在本地级别工作时,你偶尔可能需要通过将逻辑像素乘以屏幕缩放(例如,2x,3x)来与设备像素一起工作。

图 5.1 iPhone 像素密度与点数比较的可视化。原始 iPhone 的分辨率为 320 × 480。iPhone 4 的分辨率为 640 × 960,是原始设备的四倍。iPhone 4 的 PPI(326 比 163)是双倍,因此说图像被放大了 2 倍。
5.1.2 使用 ShadowPropTypesIOS 和 Elevation 创建阴影
在第四章中,你使用了文本阴影属性为 ProfileCard 标题添加阴影。iOS 和 Android 都支持为 Text 组件添加阴影。通过为卡片和圆形图像容器添加阴影来美化 ProfileCard 会很好,但两个平台之间没有用于 View 组件的通用样式属性。
这并不意味着一切都失去了。ShadowPropTypesIOS 样式可以用于在 iOS 设备上添加阴影;它不会影响组件的 z 调序。在 Android 上,你可以使用 Elevation 样式来模拟阴影,但它 确实 影响组件的 z 调序。
使用 ShadowPropTypesIOS 在 iOS 中创建阴影
让我们看看如何使用 ShadowPropTypesIOS 样式为几个视图组件添加阴影。 图 5.2 展示了可以实现的多种阴影效果。表 5.2 列出了实现每种阴影效果的具体设置。重要的要点如下:
-
如果你没有为
shadowOpacity提供值,你将看不到阴影。 -
阴影偏移以宽度和高度来表示,但你可以将其视为在 x 和 y 方向上移动阴影。你甚至可以指定宽度和高度的负值。
-
shadowOpacity的值为 1 时是完全不透明的,而值为 0.2 时则更加透明。 -
shadowRadius的值会有效地模糊阴影的边缘。阴影更加扩散。

图 5.2 iOS 特定的示例,展示如何将 ShadowPropTypesIOS 样式应用于 View 组件。示例 1 应用了阴影但没有设置不透明度,导致阴影不显示。示例 2 具有相同的阴影效果,但设置了不透明度为 1。示例 3 具有稍大的阴影,示例 4 具有相同大小的阴影并设置了阴影半径。示例 5 具有相同的阴影大小,但将不透明度从 1 更改为 0.2。示例 6 改变了阴影的颜色。示例 7 仅在一个方向上应用阴影,示例 8 在相反方向上应用阴影。
表 5.2 用于创建 图 5.2 中示例的阴影属性
| shadowOffset | ||||
|---|---|---|---|---|
| 示例 | shadowColor | width (x) | height (y) | shadowOpacity |
| --- | --- | --- | --- | --- |
| 1 | 黑色 | 10 | 10 | |
| 2 | 黑色 | 10 | 10 | 1 |
| 3 | 黑色 | 20 | 20 | 1 |
| 4 | 黑色 | 20 | 20 | 1 |
| 5 | 黑色 | 20 | 20 | 0.2 |
| 6 | 红色 | 20 | 20 | 1 |
| 7 | 黑色 | 20 | 1 | |
| 8 | 黑色 | -5 | -5 | 1 |
此图的代码可以在 git 仓库的 chapter5/figures/Figure-5.2-ShadowPropTypesIOS 目录下找到。如果你要运行此示例的代码,请记住在 iOS 模拟器中运行。在 Android 设备上,你将只看到八个无聊的、带圆角的正方形。ShadowPropTypesIOS 样式在 Android 上被忽略。
在 Android 设备上近似实现具有高度的下阴影
你如何在 Android 设备上获得相同的效果?事实是,你无法做到。你可以使用 Android 的 elevation 样式来影响组件的 z-顺序。如果有两个或更多组件占据相同的空间,你可以通过赋予它更大的高度(elevation)和因此更大的 z-index 来决定哪个应该在前,这将创建一个小的下阴影,但它远不如在 iOS 上可以实现的阴影效果那么引人注目。请注意,这仅适用于 Android,因为 iOS 不支持 elevation 样式,如果指定了它,iOS 会乐意忽略它。
尽管如此,让我们看看 elevation 的实际应用。为了做到这一点,你将创建一个包含三个盒子的 View 组件,每个盒子都绝对定位。你将赋予它们三个不同的高度——1、2 和 3——然后你将反转高度分配,看看这如何影响布局。图 5.3 展示了这些高度调整的结果。
表 5.3 展示了用于每个盒子组的绝对位置和高度。请注意,除了分配给每个盒子的高度之外,没有其他任何变化。iOS 忽略样式,始终将盒子 C 放在盒子 B 的上方,盒子 B 放在盒子 A 的上方。但 Android 尊重样式,并反转渲染盒子的顺序,因此盒子 A 现在位于盒子 B 的上方,盒子 B 位于盒子 C 的上方。

图 5.3 在 iOS 和 Android 上使用 elevation 样式的示例。在 iOS 上,高度被忽略;所有组件保留相同的 z-顺序,因此布局中最后出现的组件位于顶部。在 Android 上,使用高度,z-顺序被改变;在第二个示例中,高度分配被反转,A 位于顶部。
表 5.3 图 5.3 的高度设置
| 示例 | 颜色 | 顶部 | 左侧 | 高度 |
|---|---|---|---|---|
| A | 红色 | 0 | 0 | 1 |
| B | 橙色 | 20 | 20 | 2 |
| C | 蓝色 | 40 | 40 | 3 |
| A | 红色 | 0 | 0 | 3 |
| B | 橙色 | 20 | 20 | 2 |
| C | 蓝色 | 40 | 40 | 1 |
5.1.3 实践应用:ProfileCard 中的下阴影
让我们回到上一章的 ProfileCard 示例,并添加一些在 iOS 上看起来很棒而在 Android 上不那么出色的下阴影。你将为整个 ProfileCard 容器和圆形图像容器添加下阴影。图 5.4 展示了在 iOS 上你追求的效果以及在 Android 上你将得到的结果。
注意,即使在 Android 上应用了elevation,你也看不到很多阴影。实际上,在 Android 上,你永远无法接近 React Native 默认在 iOS 上产生的阴影效果。如果你真的需要在 Android 上实现阴影效果,那么我建议在 npm 或 yarn 上寻找一个可以满足你需求的组件。尝试不同的组件,看看你是否可以使 Android 版本看起来与 iOS 版本一样清晰。我没有任何推荐;我避免使用阴影效果或接受差异。

图 5.4 在 iOS 和 Android 上添加到卡片容器和圆形图像容器后的ProfileCard。iOS 上的阴影是通过 iOS 特定的阴影属性创建的:shadowColor、shadowOffset和shadowOpacity。在 Android 上,使用elevation属性来尝试创建深度。它只产生微弱的阴影效果,远不如 iOS 上产生的阴影。
本章中的代码从列表 4.20 开始:第四章中完成的ProfileCard示例。列表 5.1 仅显示了应用阴影效果到组件上所需更改的部分。你不需要添加很多代码就能在 iOS 上实现阴影效果。查看 Android 设备上的列表,看看elevation设置如何导致最微弱的阴影。
列表 5.1 向ProfileCard添加阴影效果
import React, { Component } from 'react';
import { Image, **Platform**, StyleSheet, Text, View} from 'react-native'; ①
...
cardContainer: {
...
height: 400,
**...Platform.select({** ②
**ios: {**
**shadowColor: 'black',**
**shadowOffset: {**
**height: 10**
**},**
**shadowOpacity: 1**
**},**
**android: {**
**elevation: 15**
**}**
**})**
},
cardImageContainer: {
...
paddingTop: 15,
**...Platform.select({** ③
**ios: {**
**shadowColor: 'black',**
**shadowOffset: {**
**height: 10,**
**},**
**shadowOpacity: 1**
**},**
**android: {**
**borderWidth: 3,**
**borderColor: 'black',**
**elevation: 15**
**}**
**})**
},
...
正如第四章中字体选择一样,你使用Platform.select函数根据平台(iOS 或 Android)应用不同的样式到组件上。在某些情况下,比如阴影效果,一个平台可能比另一个平台表现得好得多;但在大多数情况下,样式在两个平台上表现相同,这是 React Native 的一个惊人的好处。
5.2 使用变换来移动、旋转、缩放和倾斜组件
到目前为止,我们讨论的样式主要影响了组件的外观。你学习了如何设置样式、粗细、大小和颜色等属性,比如边框和字体的样式。你应用了背景颜色和阴影效果,并看到了如何通过使用边距和填充来操纵组件相对于彼此的外观。但我们还没有探讨如何独立于其他一切来操纵组件在屏幕上的位置或方向。你如何移动屏幕上的组件,或者如何使组件在圆周上旋转?
答案是变换。React Native 提供了一系列有用的变换,允许你在 3D 空间中修改组件的形状和位置。你可以将组件从一个位置移动到另一个位置,围绕所有三个轴旋转组件,以及在 x 和 y 方向上缩放和倾斜组件。单独使用变换可以产生一些有趣的效果,但它们的真正力量来自于将它们按顺序组合起来形成动画。
本节将帮助你牢固地理解变换以及它们如何影响所应用的组件。如果你清楚地理解了它们的作用,你将能够更好地将它们结合起来,以创建有意义的动画。
transform样式接受一个变换属性的数组,该数组定义了如何将变换应用于组件。例如,要旋转组件 90 度并缩小 50%,请将此变换应用于组件:
transform: [{rotate: '90deg', scale: .5}]
transform样式支持以下属性:
-
perspective -
translateX和translateY -
rotateX、rotateY和rotateZ(rotate) -
scale、scaleX和scaleY -
skewX和skewY
5.2.1 具有透视效果的 3D 效果
perspective通过影响 z 平面与用户之间的距离为元素提供 3D 空间。这与其他属性一起使用以产生 3D 效果。perspective值越大,组件的 z-index 就越高,使其看起来更靠近用户。如果 z-index 为负,组件看起来就离用户更远。
5.2.2 使用 translateX 和 translateY 沿 x 轴和 y 轴移动元素
平移属性将元素沿着 x 轴(translateX)或 y 轴(translateY)从当前位置移动。这在正常开发中并不非常实用,因为你已经有margin、padding和其他位置属性可用。但这对动画很有用,可以将组件从一个位置移动到屏幕上的另一个位置。
让我们看看如何使用translateX和translateY样式属性移动一个正方形。在图 5.5 中,一个正方形放置在显示器的中心,然后向四个主要方向和四个次要方向之一移动:西北(左上),北(顶部),东北(右上),西(左侧),东(右侧),西南(左下),南(底部),和东南(右下)。在每种情况下,正方形的中心在 x 或 y 方向上移动 1.5 倍正方形的大小,或者同时在两个方向上移动。
在学习几何时,你通常看到正 y 轴向上绘制而不是向下。但在移动设备上,惯例是正 y 轴向下穿过屏幕,这反映了最常见的滚动屏幕以查看更多内容的交互方式。结合这一点知识,很容易看出在图 5.5 中将中心正方形沿正 x 方向和正 y 方向移动会导致正方形最终出现在右下角。通过组合translateX和translateY,你可以在笛卡尔平面(x-y 平面)的任何方向上移动组件。

图 5.5 展示了中心正方形在四个主要方向和四个次要方向上的移动:西北(左上),北(顶部),东北(右上),西(左侧),东(右侧),西南(左下),南(底部),和东南(右下)
在 z 平面上的移动没有相应的平移。z 轴垂直于设备的表面,这意味着你正对着它。如果没有相应的大小变化,向前或向后移动组件将不明显。perspective变换旨在处理这种视觉效果。
在下一节中,我们将使用相同的例子,并关注中心行,其中中心正方形被向左和向右平移。你会看到当你沿着每个轴旋转组件时会发生什么。
5.2.3 使用 rotateX、rotateY 和 rotateZ(rotate)旋转元素
旋转属性确实如其名所示:它们旋转元素。旋转沿着一个轴发生:x、y 或 z。旋转的起点是在应用任何变换之前元素的中心点,所以如果你使用translateX或translateY,请记住旋转将围绕原始位置的轴进行。旋转量可以用度(deg)或弧度(rad)指定。示例使用度:
transform: [{ rotate: '45deg' }]
transform: [{ rotate: '0.785398rad' }]
图 5.6 显示了每个轴的旋转正负方向。rotate变换与rotateZ变换做的是同样的事情。

图 5.6 每个轴的旋转正负方向
让我们以 35°的增量围绕 x 轴旋转 100 × 100 的正方形,如图图 5.7 所示。每个正方形都画了一条中心线,这样更容易看到正方形是如何旋转的。你可以将 x 轴的正向旋转想象为正方形从顶部旋转到页面内。当顶部远离你时,底部会靠近你。

图 5.7 以 35°的增量围绕 x 轴旋转 100 × 100 的正方形。在 90°之后,“ROTATION”标签可以通过元素看到,是颠倒的。
在 90°时,你看到的是正方形的边缘(因为它没有厚度,你看不到任何东西)。当正方形旋转超过 90°标记后,你开始看到正方形的背面。如果你仔细看图 5.7,你会看到“ROTATION”标签是颠倒的,因为你正在透过正方形的背面看。

图 5.9 以 35°的增量围绕 z 轴旋转 100 × 100 的正方形。正旋转是顺时针,负旋转是逆时针。
下一个例子将围绕 y 轴旋转相同的 100 × 100 正方形,而不是继续使用 35° 的增量来展示旋转(见图 5.8)。想象正方形的右侧远离你,进入页面。当正方形旋转超过 90° 标记后,你可以看到“旋转”标签穿过组件。因为你正在通过组件的背面看,所以文本看起来是反的。
将 图 5.8 与 图 5.7 进行比较。本质上,围绕 y 轴的旋转与围绕 x 轴的旋转没有区别。我在 图 5.8 中垂直排列正方形,这样你可以很容易地看到旋转轴。我喜欢通过想象一本书的开合来可视化 y 轴的旋转:如果你在打开一本书,封面是逆时针旋转的。如果你在合上书,那么你是在顺时针旋转封面。

图 5.8 以 35° 的增量围绕 y 轴旋转 100 × 100 的正方形。在 90° 之后,可以看到“旋转”标签穿过元素,向后。
关于 z 轴的旋转最容易可视化。正向旋转会使物体顺时针旋转,而负向旋转会使正方形逆时针旋转。在本例中,如图 5.9 所示,旋转轴用正方形中心的点表示,因为 z 轴基本上是你的视线;它直接进入屏幕。

图 5.10 应用变换:[{translateY: 50},{translateX: 150},{rotate: '45deg'}] 到原始正方形

图 5.11 应用变换:[{translateY: 50},{rotate: '45deg'},{translateX: 150}] 到原始正方形上。旋转正方形会改变 x 轴和 y 轴的方向,因此当正方形沿 +x 方向平移 150 个点时,它就会沿着对角线向下并移出视口。
现在,旋转变换的工作原理应该相当明显了。理解正负旋转对物体影响的方向可能是最复杂的部分。但是,当你开始将其他变换与旋转结合使用时,你可能会对结果感到惊讶。记住,变换属性是一个变换数组,因此可以一次性提供多个变换,顺序很重要!指定一个变换并改变数组中元素的顺序将产生不同的结果。
让我们调查改变变换指定顺序如何影响最终布局。让我们对一个正方形应用三个不同的变换:沿 y 方向平移 50 点,沿 x 方向平移 150 点,以及旋转正方形 45°。图 5.10 按照上述顺序指定了变换。正方形原始/之前的位置有虚线边框,而正方形的新位置有实线轮廓,这样你可以看到变换如何影响原始正方形的位置和方向。
图 5.10 中的结果基本上符合预期,但如果你在将正方形沿 y 方向移动后再应用旋转,会发生什么?请看图 5.11 并找出答案。
哇,发生了什么?变换应用后,正方形完全离开了屏幕!这可能不是立即显而易见的,这就是为什么图 5.11 上标注了新的轴方向。
旋转之后,+x-和+y-轴在屏幕上不再垂直和水平对齐:它们旋转了 45°。当应用translateX变换时,正方形在+x 方向上移动了 150 点,但现在+x 方向与原始 x 轴成 45°角。
下一个部分展示了旋转变换的另一个有趣方面。
5.2.4 在元素旋转超过 90°时设置可见性
如果你回顾图 5.7 和 5.8,当你围绕 x-或 y-轴旋转正方形并超过 90°点时,你仍然可以看到正方形前脸上的文本。backfaceVisibility属性决定了当元素旋转超过 90°时元素是否可见。此属性可以设置为'visible'或'hidden'。此属性不是变换,但它让你能够在查看对象的背面时隐藏或显示元素。*****
backfaceVisibility属性默认为'visible',但如果将backfaceVisibility更改为'hidden',一旦组件在 x 或 y 方向旋转超过 90°,你就完全看不到该元素。在图 5.7 和 5.8 中,对应 105°和 140°旋转的正方形将消失。如果这听起来很困惑,请看图 5.12

图 5.12 展示了将backfaceVisibility属性设置为'hidden'如何隐藏旋转超过 90°的元素。左边的立方体显示了 2、4 和 5 面,这些面都旋转了 180°。右边的立方体隐藏了这些面。
在图中,你可以很容易地看到将backfaceVisibility设置为'hidden'的效果。这也很容易看出这种行为在动画中可能是有益的。当立方体的面旋转出视线时,你希望它们被隐藏。
5.2.5 使用 scale、scaleX 和 scaleY 在屏幕上缩放对象
本节讨论了在屏幕上缩放对象。缩放有许多实际用途,许多模式也利用了其功能。例如,缩放可以用来创建对象的缩略图。你已经在许多应用中看到了这一点;用户轻触缩略图,动画逐渐将对象放大到全尺寸。这是一种常见的过渡技术,提供了良好的视觉效果。
你将学习缩放对象的基础知识,然后使用这些技能创建一个当按下时可以打开到全尺寸的ProfileCard缩略图。稍后,本章将讨论 flexbox 及其如何用于在画廊界面中管理多个ProfileCard缩略图,你可以从中按下个人资料以查看更多详细信息。
scale通过传递给它的数字乘以元素的大小,默认值为 1。为了使元素看起来更大,传递一个大于 1 的值;为了使其看起来更小,传递一个小于 1 的值。
元素也可以使用scaleX或scaleY沿着单个轴进行缩放。scaleX沿着 x 轴水平拉伸元素,而scaleY沿着 y 轴垂直拉伸元素。让我们创建几个正方形来展示缩放的效果:参见图 5.13。

图 5.13 展示了缩放变换如何改变原始正方形的例子。所有正方形都以与 A 相同的大小和形状开始,A 具有默认的缩放比例 1。B 将正方形缩放为 0.5,使其缩小。C 将正方形缩放为 2,使其放大。D 使用scaleX,沿着 x 轴将正方形变换为 3 倍。E 使用scaleY,沿着 y 轴将正方形变换为 1.5 倍。
没有什么异常发生;缩放对象相当直接。列表 5.2 展示了这有多么简单。
列表 5.2 使用scale、scaleX和scaleY缩放正方形
import React, { Component } from 'react';
import { StyleSheet, Text, View} from 'react-native';
export default class App extends Component<{}> {
render() {
return (
<View style={styles.container}>
<Example style={{}}>A,1</Example> ①
<Example style={{transform: [{scale: 0.5}]}}>B,0.5</Example> ②
<Example style={{transform: [{scale: 2}]}}>C,2</Example> ③
<Example style={{transform: [{scaleX: 3}]}}>D,X3</Example> ④
<Example style={{transform: [{scaleY: 1.5}]}}>E,Y1.5</Example> ⑤
</View>
);
}
}
const Example = (props) => (
<View style={[styles.example,props.style]}>
<Text>
{props.children}
</Text>
</View>
);
const styles = StyleSheet.create({
container: {
marginTop: 75,
alignItems: 'center',
flex: 1
},
example: {
width: 50,
height: 50,
borderWidth: 2,
margin: 15,
alignItems: 'center',
justifyContent: 'center'
},
});
5.2.6 使用缩放变换创建 ProfileCard 的缩略图
现在你已经看到了缩放的实际应用,让我们使用这个技术来创建ProfileCard的缩略图。通常你会对即将展示的内容进行动画处理,以避免闪烁,但让我们看看如何以实际的方式使用缩放。图 5.14(Figure 5.14)显示了ProfileCard组件的一个小型、缩小的版本——缩略图。如果你按下缩略图,组件将恢复到全尺寸。如果你按下全尺寸组件,它将折叠回缩略图视图。
从列表 5.1 中的代码开始。就样式而言,你只需要添加一个新样式来执行从全尺寸到缩略图的缩放变换。代码的其余部分重新组织了组件的各个部分,使其成为一个更可重用的结构,并提供了处理onPress事件的触摸能力。

图 5.14 将全尺寸 ProfileCard 缩小 80%为缩略图。按下缩略图将 ProfileCard 恢复到原始大小,按下全尺寸组件将组件折叠成缩略图。
列表 5.3 将ProfileCard从全尺寸缩放到缩略图
import React, { Component } from 'react';
import PropTypes from 'prop-types'; ①
import update from 'immutability-helper'; ②
import { Image, Platform, StyleSheet, Text,
TouchableHighlight, View} from 'react-native'; ③
const userImage = require('./user.png');
constdata = { [ ④
image: userImage,
name: 'John Doe',
occupation: 'React Native Developer',
description: 'John is a really great Javascript developer. ' +
'He loves using JS to build React Native applications ' +
'for iOS and Android',
showThumbnail: true
}
];
const ProfileCard = (props) => { ⑤
const { image, name, occupation,
description, onPress, showThumbnail } = props;
let containerStyles = [styles.cardContainer];
if (showThumbnail) { ⑥
containerStyles.push(styles.cardThumbnail);
}
return (
<TouchableHighlight onPress={onPress}> ⑦
<View style={[containerStyles]}>
<View style={styles.cardImageContainer}>
<Image style={styles.cardImage} source={image}/>
</View>
<View>
<Text style={styles.cardName}>
{name}
</Text>
</View>
<View style={styles.cardOccupationContainer}>
<Text style={styles.cardOccupation}>
{occupation}
</Text>
</View>
<View>
<Text style={styles.cardDescription}>
{description}
</Text>
</View>
</View>
</TouchableHighlight>
)
};
ProfileCard.propTypes = {
image: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
occupation: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
showThumbnail: PropTypes.bool.isRequired,
onPress: PropTypes.func.isRequired
};
export default class App extends Component<{}> {
constructor(props, context) {
super(props, context);
this.state = { ⑧
data: data
}
}
handleProfileCardPress = (index) => { ⑨
const showThumbnail = !this.state.data[index].showThumbnail;
this.setState({
data: update(this.state.data,
{[index]: {showThumbnail: {$set: showThumbnail}}})
});
};
render() {
const list = this.state.data.map(function(item, index) { ⑩
const { image, name, occupation, description, showThumbnail } = item;
return <ProfileCard key={'card-' + index}
image={image}
name={name}
occupation={occupation}
description={description}
onPress={this.handleProfileCardPress.bind(this, index)}
showThumbnail={showThumbnail}/>
}, this);
return (
<View style={styles.container}>
{list} ⑪
</View>
);
}
}
...
cardThumbnail: { ⑫
transform: [{scale: 0.2}]
},
...
通过重新组织组件的结构,你可以更好地处理向应用程序中添加更多ProfileCard组件。在第 5.3 节中,你将添加更多ProfileCard并了解如何将它们组织成画廊布局。
5.2.7 使用 skewX 和 skewY 沿 x 轴和 y 轴倾斜元素
在我们离开变换并讨论布局之前,让我们看看skewX和skewY变换。在生成图 5.12(github chapter5/figures/Figure-5.12-BackfaceVisibility)中backfaceVisibility示例的立方体源代码中,你可以看到倾斜正方形对于产生立方体面的三维效果是至关重要的。让我们讨论skewX和skewY的作用,这样当你详细查看源代码时,你会明白你所看到的内容。
skewX属性沿 x 轴倾斜元素。同样,skewY属性沿 y 轴倾斜元素。图 5.15 展示了以下倾斜正方形的结果:
-
正方形 A 未应用任何变换。
-
正方形 B 沿 x 轴倾斜 45°。
-
正方形 C 沿 x 轴倾斜-45°。
-
正方形 D 沿 y 轴倾斜 45°。
-
正方形 E 沿 y 轴倾斜-45°。
与缩放类似,倾斜一个元素相对简单:提供一个角度,并指定轴。下一个列表提供了所有详细信息。

图 5.15 在 iOS 上沿 x 轴和 y 轴倾斜正方形的示例。正方形 A 未应用任何变换。正方形 B 沿 x 轴倾斜 45°。正方形 C 沿 x 轴倾斜-45°。正方形 D 沿 y 轴倾斜 45°,正方形 E 沿 y 轴倾斜-45°。
列表 5.4 展示如何通过倾斜变换正方形
import React, { Component } from 'react';
import { StyleSheet, Text, View} from 'react-native';
export default class App extends Component<{}> {
render() {
return (
<View style={styles.container}>
<Example style={{}}>A</Example>
<Example style={{transform: [{skewX: '45deg'}]}}> ①
B X45
</Example>
<Example style={{transform: [{skewX: '-45deg'}]}}> ②
C X-45
</Example>
<Example style={{transform: [{skewY: '45deg'}]}}> ③
D Y45
</Example>
<Example style={{transform: [{skewY: '-45deg'}]}}> ④
E Y-45
</Example>
</View>
);
}
}
const Example = (props) => (
<View style={[styles.example,props.style]}>
<Text>
{props.children}
</Text>
</View>
);
const styles = StyleSheet.create({
container: {
marginTop: 50,
alignItems: 'center',
flex: 1
},
example: {
width: 75,
height: 75,
borderWidth: 2,
margin: 20,
alignItems: 'center',
justifyContent: 'center'
},
});
5.2.8 变换关键点
在本节中,我们讨论了许多变换思想!其中一些相对简单,而其他一些可能一开始难以可视化。我没有展示很多结合变换的例子,所以你可以专注于单个变换的作用。我鼓励你尝试任何例子,并添加额外的变换,以进行实验并看看会发生什么。
在第七章中,当我们讨论动画时,你会看到变换如何使事物栩栩如生。现在,记住这些关键点:
-
x 轴和 y 轴的原点在左上角,这意味着 y 轴的正方向是向下屏幕。你在上一章的绝对定位中看到了这一点,但可能与你习惯的相反,这可能会使推理变换将做什么变得困难。
-
旋转和变换的起点始终在元素的原始位置。你不能在 x 或 y 方向上平移一个对象,然后再围绕新的中心点旋转它。
变换是移动组件在屏幕上的好方法,但你不会在日常基础上使用它们。最常见的是使用 Yoga,这是一个实现了 W3C 的 flexbox 网络规范大部分内容的布局引擎。在下一节中,我们将详细讨论 Yoga 的 flexbox 实现。
5.3 使用 flexbox 布局组件

图 5.16 使用 flex 属性的三种布局示例。顶部示例是 1:1,A = {flex: 1}和 B = {flex: 1},结果每个都占用 50%的空间。中间示例是 1:2,C = {flex: 1}和 D = {flex: 2},结果 C 占用 33%的空间,D 占用 66%。底部示例是 1:3,E = {flex: 1}和 F = {flex: 3},结果 E 占用 25%的空间,F 占用 75%的空间。
Flexbox 是 React Native 用来提供用户创建 UI 和控制定位的高效布局实现。React Native 的 flexbox 实现基于 W3C 的 flexbox 网络规范,但并不完全共享 100%的 API。它的目标是提供一个简单的方法来推理、对齐和分配布局中项目之间的空间,即使它们的尺寸未知或动态。
你已经在前面的许多示例中看到了 flexbox 的使用。它功能强大,使得布局项目比其他方法更容易,以至于很难不使用它。花时间理解本节的内容将对你大有裨益。以下是用于控制 flexbox 布局的对齐属性:flex、flexDirection、justifyContent、alignItems、alignSelf和flexWrap。
5.3.1 使用 flex 调整组件的尺寸
flex属性指定了一个组件改变其尺寸以填充其所在容器空间的能力。这个值相对于同一容器中其他项目的flex属性是相对的。
如果你有一个高度为 300 和宽度为 300 的View元素,并且一个子View元素具有flex: 1属性,那么子视图将完全填充父视图。如果你决定添加另一个具有flex属性为flex: 1的子元素,每个视图将在父容器中占用相等的空间。flex数字仅相对于占用相同空间的其它flex项目而言是重要的。
另一种看待这个问题的方式是将flex属性视为百分比。例如,如果你想子组件分别占用 66.6%和 33.3%,你可以使用flex:66和flex:33。而不是flex:66和flex:33,你可以指定flex:2和flex:1,以达到相同的布局效果。
为了更好地理解其工作原理,让我们看看 图 5.16 中展示的几个示例。这些可以通过在单个元素上设置适当的 flex 值轻松实现。以下列表显示了创建此类布局所需的步骤。
列表 5.5 具有比例 1:1、1:2 和 1:3 的 flex 视图
…
render() {
return (
<View style={styles.container}>
<View style={[styles.flexContainer]}>
<Example style={[{flex: 1},styles.darkgrey]}>A 50%</Example> ①
<Example style={[{flex: 1}]}>B 50%</Example>
</View>
<View style={[styles.flexContainer]}> ②
<Example style={[{flex: 1},styles.darkgrey]}>C 33%</Example>
<Example style={{flex: 2}}>D 66%</Example>
</View>
<View style={[styles.flexContainer]}> ③
<Example style={[{flex: 1},styles.darkgrey]}>E 25%</Example>
<Example style={{flex: 3}}>F 75%</Example>
</View>
</View>
);
}
…
5.3.2 使用 flexDirection 指定 flex 的方向
在前面的示例中,flex 容器中的项目按列(y 轴)布局,意味着从上到下。A 堆叠在 B 上,C 堆叠在 D 上,E 堆叠在 F 上。使用 flexDirection 属性,你可以更改布局的主轴,从而改变布局的方向。flexDirection 应用于包含示例组件的父视图中。
实现图 5.17 中的布局只需在 flexContainer 样式中添加一行代码,这是每个示例组件的父容器。更改此容器的 flexDirection 影响其所有 flex 子项的布局。向样式添加 flexDirection: 'row',并查看它如何更改布局。

图 5.17 与 图 5.16 相同的示例,但 flexDirection 设置为 'row'。现在项目在行内水平占用空间,而不是在列内垂直占用空间。
列表 5.6 向父容器添加 flexDirection: 'row'
flexContainer: { ①
width: 150,
height: 150,
borderWidth: 1,
margin: 10,
**flexDirection: 'row'** ②
},
子元素现在从左到右显示。flexDirection 有两个选项:'row' 和 'column'。默认设置是 'column'。如果你没有指定 flexDirection 属性,内容将以列的形式布局。当你在 React Native 中开发应用程序时,你将大量使用此属性,因此理解它及其工作原理非常重要。
5.3.3 使用 justifyContent 定义组件周围空间的使用方式
使用 flex 属性,你可以指定每个组件在其父容器中占用的空间量;但如果你不是试图占用整个空间,怎么办?你如何使用 flexbox 使用组件的原始大小来布局组件?
justifyContent 定义了在容器的主轴(即 flex 方向)上如何分配和围绕 flex 项目之间的空间。justifyContent 在父容器上声明。有五种选项可用:
-
center使子元素在父容器内居中。空闲空间分布在子元素群集的两侧。 -
flex-start根据分配给flexDirection的值将组件分组在 flex 列或行的开始处。flex-start是justifyContent的默认值。 -
flex-end以相反的方式起作用:它将项目组合在容器的末尾。 -
space-around尝试在元素周围均匀分配空间。不要将其与在容器中均匀分布元素混淆;空间是围绕元素分配的。如果它是基于元素的,你会期望
空间 – 元素 – 空间 – 元素 – 空间
相反,flexbox 在元素的每一侧分配相同数量的空间,从而得到
空间 – 元素 – 空间 – 空间 – 元素 – 空间
在这两种情况下,空白量的数量是相同的;但在后者中,元素之间的空间更大。
space-between不会在容器的开始或结束处应用间距。任何两个连续元素之间的空间与任何其他两个连续元素之间的空间相同。
图 5.18 展示了每个 justifyContent 属性如何在 flex 元素之间和周围分配空间。每个示例都使用两个元素来帮助描述正在发生的情况。
列表 5.7 展示了生成 图 5.18 所使用的代码。仔细查看它,以了解其工作原理,然后尝试以下操作:向每个示例添加更多元素,以查看项目数量增加时会发生什么;将 flexDirection 设置为 row,以查看项目水平布局而不是垂直布局时会发生什么。
列表 5.7 展示 justifyContent 选项的示例
...
render() {
return (
<View style={styles.container}>
<FlexContainer style={[**{justifyContent: 'center'}**]}> ①
<Example>center</Example>
<Example>center</Example>
</FlexContainer>
<FlexContainer style={[**{justifyContent: 'flex-start'}**]}> ②
<Example>flex-start</Example>
<Example>flex-start</Example>
</FlexContainer>
<FlexContainer style={[**{justifyContent: 'flex-end'}**]}> ③
<Example>flex-end</Example>
<Example>flex-end</Example>
</FlexContainer>
<FlexContainer style={[**{justifyContent: 'space-around'}**]}> ④
<Example>space-around</Example>
<Example>space-around</Example>
</FlexContainer>
<FlexContainer style={[**{justifyContent: 'space-between'}**]}> ⑤
<Example>space-between</Example>
<Example>space-between</Example>
</FlexContainer>
</View>
);
}
...

图 5.18 展示了 justifyContent 如何影响每个支持选项(居中、flex-start、flex-end、space-around 和 space-between)下弹性子元素之间的空间分布。

图 5.19 使用非默认的 alignItems 属性(居中、flex-start 和 flex-end)修改后的示例,来自 图 5.16
5.3.4 在容器中使用 alignItems 对齐子元素
alignItems 定义了如何沿其容器的次要轴对齐子元素。此属性在父视图中声明,并影响其弹性子元素,就像 flexDirection 一样。alignItems 有四个可能的值:stretch、center、flex-start 和 flex-end。
stretch 是默认值,用于图 5.17 和 5.18。每个示例组件都被拉伸以填充其父容器。图 5.19 回顾了 图 5.16,并展示了使用其他选项(center、flex-start 和 flex-end)会发生什么。因为示例组件没有指定精确的宽度,它们只占据渲染其内容所需的水平空间,而不是拉伸以填充空间。在第一种情况下,alignItems 设置为 'center'。在第二种情况下,alignItems 设置为 'flex-start'。最后,alignItems 设置为 'flex-end'。使用 列表 5.8 来更改每个示例从 列表 5.5 中的对齐方式。
列表 5.8 使用非默认的 alignItems 属性
render() {
return (
<View style={styles.container}>
<View style={[styles.flexContainer,
**{alignItems: 'center'}**]}> ①
<Example style={[styles.darkgrey]}>A 50%</Example>
<Example>B 50%</Example>
</View>
<View style={[styles.flexContainer,
**{alignItems: 'flex-start'}**]}> ②
<Example style={[styles.darkgrey]}>C 33%</Example>
<Example style={{flex: 2}}>D 66%</Example>
</View>
<View style={[styles.flexContainer,
**{alignItems: 'flex-end'}**]}> ③
<Example style={[styles.darkgrey]}>E 25%</Example>
<Example style={{flex: 3}}>F 75%</Example>
</View>
</View>
);
}
既然你已经看到了如何使用其他 alignItems 属性及其对默认列布局的影响,为什么不将 flexDirection 设置为 'row' 并看看会发生什么?
5.3.5 使用 alignSelf 覆盖父容器的对齐方式

图 5.20 当父容器中的 alignItems 属性设置为默认值 stretch 时,每个 alignSelf 属性如何影响布局
到目前为止,所有弹性属性都应用于父容器。alignSelf 直接应用于单个弹性子元素。
使用 alignSelf,你可以访问容器内单个元素的 alignItems 属性。本质上,alignSelf 给你覆盖父容器上设置的任何对齐方式的能力,因此子对象可以独立于其同伴进行对齐。可用的选项有 auto、stretch、center、flex-start 和 flex-end。默认值是 auto,它从父容器的 alignItems 设置中获取值。其余属性以与 alignItems 上相应属性相同的方式影响布局。
在 图 5.20 中,父容器没有设置 alignItems,因此默认为 stretch。在第一个例子中,auto 值从其父容器继承 stretch。接下来的四个例子布局正如你所期望的那样。最后一个例子没有设置 alignSelf 属性,因此默认为 auto 并与第一个例子以相同的方式布局。
列表 5.9 做了一些不同的事情。它不是直接将样式应用到 Example 元素上,而是创建一个新的组件属性:align。这个属性被传递到 Example 组件中,并用来设置 alignSelf。否则,这个例子与本章中的许多其他例子相同;它探讨了将每个值应用到样式上的效果。
列表 5.9 使用 alignSelf 覆盖父元素的 alignItems
import React, { Component } from 'react';
import { StyleSheet, Text, View} from 'react-native';
export default class App extends Component<{}> {
render() {
return (
<View style={styles.container}>
<FlexContainer style={[]}>
<Example **align='auto'**>auto</Example> ①
<Example **align='stretch'**>stretch</Example> ②
<Example **align='center'**>center</Example> ③
<Example **align='flex-start'**>flex-start</Example> ④
<Example **align='flex-end'**>flex-end</Example> ⑤
<Example>default</Example> ⑥
</FlexContainer>
</View>
);
}
}
const FlexContainer = (props) => (
<View style={[styles.flexContainer,props.style]}>
{props.children}
</View>
);
const Example = (props) => (
<View style={styles.example,
styles.lightgrey,
**{alignSelf: props.align || 'auto'}**, [ ⑦
props.style
]}>
<Text>
{props.children}
</Text>
</View>
);
const styles = StyleSheet.create({
container: {
marginTop: 50,
alignItems: 'center',
flex: 1
},
flexContainer: {
backgroundColor: '#ededed',
width: 120,
height: 180,
borderWidth: 1,
margin: 10
},
example: {
height: 25,
marginBottom: 5,
backgroundColor: '#666666'
},
});
5.3.6 使用 flexWrap 防止项目被裁剪
在本节之前,你已经了解到 flexDirection 属性有两个值:column(默认值)和 row。column 水平排列项目,而 row 垂直排列项目。但你还没有看到项目因为不合适而溢出屏幕的情况。
flexWrap 有两个值:nowrap 和 wrap。默认值是 nowrap,这意味着如果项目不合适,它们将溢出屏幕。项目将被裁剪,用户无法看到它们。为了解决这个问题,请使用 wrap 值。
在 图 5.21 中,第一个例子使用了 nowrap,正方形溢出屏幕。正方形行在右侧被截断。第二个例子使用了 wrap,正方形环绕并开始新的一行。列表 5.10 展示了代码。

图 5.21 两个溢出容器的示例:一个将 flexWrap 设置为 nowrap,另一个设置为 wrap
列表 5.10 flexWrap 值影响布局的示例
import React, { Component } from 'react';
import { StyleSheet, Text, View} from 'react-native';
export default class App extends Component<{}> {
render() {
return (
<View style={styles.container}>
<NoWrapContainer> ①
<Example>A nowrap</Example>
<Example>1</Example>
<Example>2</Example>
<Example>3</Example>
<Example>4</Example>
</NoWrapContainer>
<WrapContainer> ②
<Example>B wrap</Example>
<Example>1</Example>
<Example>2</Example>
<Example>3</Example>
<Example>4</Example>
</WrapContainer>
</View>
);
}
}
const NoWrapContainer = (props) => (
<View style={[**styles.noWrapContainer**,props.style]}> ③
{props.children}
</View>
);
const WrapContainer = (props) => (
<View style={[**styles.wrapContainer**,props.style]}> ④
{props.children}
</View>
);
const Example = (props) => (
<View style={[styles.example,props.style]}>
<Text>
{props.children}
</Text>
</View>
);
const styles = StyleSheet.create({
container: {
marginTop: 150,
flex: 1
},
noWrapContainer: {
backgroundColor: '#ededed',
**flexDirection: 'row',** ⑤
**flexWrap: 'nowrap',**
borderWidth: 1,
margin: 10
},
wrapContainer: {
backgroundColor: '#ededed',
**flexDirection: 'row',** ⑥
**flexWrap: 'wrap',**
borderWidth: 1,
margin: 10
},
example: {
width: 100,
height: 100,
margin: 5,
backgroundColor: '#666666'
},
});
在布局瓷砖时,很容易看出哪种行为更可取,但你可能会遇到一种情况,其中 nowrap 会更适合你。无论如何,你现在应该对弹性盒子及其在 React Native 中构建响应式布局的多种方式有清晰的理解。
摘要
-
在为显示调整项目大小时,iOS 使用点,而 Android 使用密度无关像素。测量系统不同,但除非你需要像素完美的图形,否则应该对开发影响不大。
-
一些样式仅在某个平台上可用。
ShadowPropTypeIOS仅在 iOS 上可用,而elevation仅在 Android 上被识别。 -
可以使用
translateX和translateY变换在 x 和 y 方向上移动组件。 -
可以使用
rotateX、rotateY和rotateZ在 x、y 和 z 轴上围绕组件旋转。旋转点是应用任何转换之前对象的原始位置。 -
组件可以在 x 和 y 方向上缩放,以使组件增长或缩小。
-
组件也可以在 x 和 y 方向上倾斜。
-
可以同时应用多个转换,但它们指定的顺序很重要。旋转一个组件会改变该组件在后续转换中的方向。
-
flexDirection属性定义了主轴,默认为column(y 轴)。 -
justifyContent属性定义了项目应该如何沿着主轴排列。 -
alignItems属性定义了项目应该如何沿着次轴排列。 -
可以使用
alignSelf属性来覆盖由父容器指定的alignItems属性。 -
flexWrap属性告诉弹性盒子如何处理通常会溢出屏幕的项目。** **# 6
导航
本章 涵盖了**
-
React Native 与网页中的导航
-
使用标签、堆栈和抽屉进行导航
-
管理嵌套导航器
-
在路由间传递数据和方法
任何移动应用的核心功能之一是导航。在构建应用之前,我建议你花些时间策略性地考虑你希望应用如何处理导航和路由。本章涵盖了移动应用中典型的三种主要导航类型:基于标签、基于堆栈和基于抽屉的导航。
基于标签的导航通常标签位于屏幕的顶部或底部;按下标签会跳转到与标签相关的屏幕。许多流行的应用,如 Twitter、Instagram 和 Facebook,都在主屏幕上实现了这种类型的导航。
基于堆栈的导航从一个屏幕转换到另一个屏幕,替换当前屏幕,并通常实现某种形式的动画转换。然后你可以后退或继续向前移动堆栈。你可以将基于堆栈的导航想象成一个组件数组:将新的组件推入数组会带你到新组件的屏幕。要返回,你需要从堆栈中弹出最后一个屏幕,然后导航到上一个屏幕。大多数导航库都会为你处理弹出和推入。
基于抽屉的导航通常是一个从屏幕的左侧或右侧弹出并显示选项列表的侧边菜单。当你按下某个选项时,抽屉关闭,你将被带到新的屏幕。
React Native 框架不包含导航库。在构建 React Native 应用的导航时,你必须选择一个第三方导航库。有一些好的导航库可供选择,但在这个章节中,我选择使用 React Navigation 作为构建演示应用的导航库。React Navigation 库由 React Native 团队推荐,并由 React 和 React Native 社区的许多人维护。
React Navigation 是一个基于 JavaScript 的导航实现。所有的转换和控制都由 JavaScript 处理。有些团队可能因为多种原因而偏好原生解决方案:例如,他们可能正在将 React Native 添加到现有的原生应用中,并希望整个应用中的导航保持一致。如果你对原生导航解决方案感兴趣,可以查看 React Native Navigation,这是一个由 Wix 的工程师构建和维护的开源 React Native 导航库。
6.1 React Native 导航与网络导航的比较
因为网络导航的范式与 React Native 的范式大不相同,所以导航对于许多刚开始接触 React Native 的开发者来说是一个难题。在网络上,我们习惯于使用 URL 进行操作。根据框架或环境的不同,有多种方式可以导航到新的路由,但通常你希望将用户发送到新的 URL,如果需要的话,还可以添加一些 URL 参数。
在 React Native 中,路由是基于组件的。你使用你正在工作的导航器来加载或显示一个组件。根据它是基于标签、基于堆栈、基于抽屉还是这些的组合,路由也会有所不同。我们将在下一节构建演示应用时详细说明这些内容。
你还需要在整个路由中跟踪数据和状态,并可能访问应用中其他地方定义的方法,因此围绕数据和方法的共享策略非常重要。你可以通过在定义导航的顶层管理数据和方法,或者使用如 Redux 或 MobX 这样的状态管理库来管理数据和方法。在示例中,你将在应用顶层的类中管理数据和方法的。
6.2 构建基于导航的应用
在本章中,你将学习如何通过构建一个同时使用基于标签页和基于堆栈的导航的应用来实施导航。你将创建的应用程序名为 Cities;它在图 6.1 中显示。它是一个旅行应用,让你能够跟踪你访问或想要访问的所有城市。你还可以在每个你想要访问的城市中添加位置。

图 6.1 完成的 Cities 应用,包含添加城市、列出城市、查看城市详情和查看城市内位置的功能界面
主要导航是基于标签页的,其中一个标签页包括基于堆栈的导航。左侧标签页显示你创建的城市列表,右侧标签页包含创建新城市的表单。在左侧标签页,你可以点击单个城市来查看它,以及查看和创建城市内的位置。
要开始,创建一个新的 React Native 应用。在你的终端中,导航到一个空目录,并使用 React Native CLI 安装新的 React Native 应用:
react-native init CitiesApp
接下来,导航到新目录,并安装两个依赖项:React Navigation 和 uuid。React Navigation 是导航库,uuid 将被用于为城市创建唯一的 ID,以便唯一地识别它们:
cd CitiesApp
npm install react-navigation uuid
现在,让我们开始创建组件!在应用的根目录下创建一个新的主目录,命名为 src。这个目录将包含应用的大部分新代码。在这个新目录中,添加三个主要子目录:Cities、AddCity 和 components。
由于主要导航是基于标签页的,所以你会将主要应用分为两个主要组件(Cities和AddCity),每个组件都有自己的标签页。AddCity 文件夹将只包含一个组件,AddCity.js。Cities 文件夹将包含两个组件:Cities.js 用于查看城市列表,City.js 用于查看单个城市。components 文件夹将包含任何可重用组件;在这个例子中,它将包含一个组件。
你还将拥有 src/index.js 和 src/theme.js 文件。src/index.js 将包含所有的导航配置,而 theme.js 将用于保存可主题化的配置——在这个例子中,是一个主色调配置。图 6.2 显示了项目的完整文件夹结构。
现在你已经创建了文件夹结构和安装了必要的依赖项,让我们开始编写代码。你将首先处理的是 src/theme.js 文件。在这里,你将设置主色调并使其可导出以在应用中使用。我为应用选择的主色调是蓝色,但你可以自由选择任何颜色;如果你更改这个文件中的颜色值,应用的工作方式将保持不变。

图 6.2 完整的 src 文件夹结构
列表 6.1 创建包含主色调的主题文件
const colors = {
primary: '#1976D2'
}
export {
colors
}
如果你愿意,可以在整个应用中导入这个主色调,并且可以选择在一个地方更改它。
然后,编辑 src/index.js 以创建主导航配置。你将在这里创建两个导航实例:基于标签的导航和基于堆栈的导航。
列表 6.2 创建导航配置
import React from 'react'
import Cities from './Cities/Cities' ①
import City from './Cities/City' ①
import AddCity from './AddCity/AddCity' ①
import { colors } from './theme' ②
import { createBottomTabNavigator,
createStackNavigator } from 'react-navigation' ③
const options = { ④
navigationOptions: {
headerStyle: {
backgroundColor: colors.primary
},
headerTintColor: '#fff'
}
}
const CitiesNav = createStackNavigator({ ⑤
Cities: { screen: Cities },
City: { screen: City }
}, options)
const Tabs = createBottomTabNavigator({ ⑥
Cities: { screen: CitiesNav },
AddCity: { screen: AddCity }
})
export default Tabs
当你创建 options 对象时,堆栈导航器会自动在每个路由的顶部放置一个标题。标题通常是放置当前路由标题以及像返回按钮这样的按钮的地方。options 对象还定义了标题的背景颜色和色调颜色。
对于第一个导航实例,createStackNavigator 接受两个参数:路由配置以及有关应用导航的样式配置等。你将两个路由作为第一个参数传递,并将 options 对象作为第二个参数传递。
接下来,更新 App.js 以包含新的导航并作为主入口点渲染。除了渲染导航组件外,App.js 还将包含并控制任何要提供给应用的方法和数据。
列表 6.3 更新 App.js 以使用导航配置
import React, { Component } from 'react';
import {
Platform,
StyleSheet,
Text,
View
} from 'react-native';
import Tabs from './src' ①
export default class App extends Component {
state = { ②
cities: []
}
addCity = (city) => { ③
const cities = this.state.cities
cities.push(city)
this.setState({ cities })
}
addLocation = (location, city) => { ④
const index = this.state.cities.findIndex(item => {
return item.id === city.id
})
const chosenCity = this.state.cities[index]
chosenCity.locations.push(location)
const cities = [
...this.state.cities.slice(0, index),
chosenCity,
...this.state.cities.slice(index + 1)
]
this.setState({
cities
})
}
render() {
return (
<Tabs ⑤
screenProps={{
cities: this.state.cities,
addCity: this.addCity,
addLocation: this.addLocation
}}
/>
)
}
}
App.js 包含三个主要功能。它创建应用初始状态:一个名为 cities 的空数组。每个城市将是一个对象,包含名称、国家、ID 和位置数组。addCity 方法允许你向状态中存储的 cities 数组添加新的城市。addLocation 方法识别你想要添加位置的特定城市,更新该城市,并使用新数据重置状态。
React Navigation 有一种方法可以将这些方法和状态传递给导航器使用的所有路由。为此,传递一个名为 screenProps 的属性,包含你想要访问的内容。然后,从任何路由内部,this.props.screenProps 提供了对数据或方法的访问。
接下来,你将创建一个名为 CenterMessage 的可重用组件,它在 Cities.js 和 City.js(src/components/CenterMessage.js)中使用。当数组为空时,它将显示消息。例如,当应用首次启动时,它不会有任何城市要列出;你可以显示如图 6.3 所示的消息,而不是只显示一个空白屏幕。
列表 6.4 CenterMessage 组件
import React from 'react'
import {
Text,
View,
StyleSheet
} from 'react-native'
import { colors } from '../theme'
const CenterMessage = ({ message }) => (
<View style={styles.emptyContainer}>
<Text style={styles.message}>{message}</Text>
</View>
)
const styles = StyleSheet.create({
emptyContainer: {
padding: 10,
borderBottomWidth: 2,
borderBottomColor: colors.primary
},
message: {
alignSelf: 'center',
fontSize: 20
}
})
export default CenterMessage

图 6.3 可重用的 CenterMessage 组件在显示区域中居中显示消息。

图 6.4 AddCity 标签页允许用户输入新的城市名称和国家名称。
此组件很简单。它是一个无状态组件,仅接收一个消息作为属性,并显示该消息以及一些样式。
接下来,在 src/AddCity/AddCity.js 中,创建 AddCity 组件,该组件将允许你向 cities 数组添加新城市(参见 图 6.4)。这个组件将包含一个表单,其中包含两个文本输入:一个用于保存城市名称,另一个用于保存国家名称。此外,一个按钮将调用 App.js 中的 addCity 方法。
列表 6.5 AddCity 选项卡(功能)
import React from 'react'
import {
View,
Text,
StyleSheet,
TextInput,
TouchableOpacity
} from 'react-native'
import uuidV4 from 'uuid/v4'
import { colors } from '../theme'
export default class AddCity extends React.Component {
state = { ①
city: '',
country: '',
}
onChangeText = (key, value) => { ②
this.setState({ [key]: value })
}
submit = () => { ③
if (this.state.city === '' || this.state.country === '') {
alert('please complete form')
}
const city = {
city: this.state.city,
country: this.state.country,
id: uuidV4(),
locations: []
}
this.props.screenProps.addCity(city)
this.setState({
city: '',
country: ''
}, () => {
this.props.navigation.navigate('Cities')
})
}
render() {
return (
<View style={styles.container}>
<Text style={styles.heading}>Cities</Text>
<TextInput
placeholder='City name'
onChangeText={val => this.onChangeText('city', val)}
style={styles.input}
value={this.state.city}
/>
<TextInput
placeholder='Country name'
onChangeText={val => this.onChangeText('country', val)}
style={styles.input}
value={this.state.country}
/>
<TouchableOpacity onPress={this.submit}>
<View style={styles.button}>
<Text style={styles.buttonText}>Add City</Text>
</View>
</TouchableOpacity>
</View>
)
}
}
首先,你检查确保城市和国家都不是空字符串。如果任一或两个都是空的,你将返回,因为你不想在不填写两个字段的情况下存储数据。接下来,你创建一个对象来保存要添加到 cities 数组中的城市。从状态中获取现有的城市和国家值,并使用 uuidV4 方法添加一个 ID 值以及一个空的位置数组。调用 this.props.screenProps.addCity,传入新的城市。然后重置状态以清除状态中存储的任何值。最后,通过调用 this.props.navigation.navigate 并传入要导航到的路由字符串——在这种情况下,是 'Cities'——将用户导航到城市选项卡以显示他们添加了新城市后的城市列表。
每个在导航器中作为屏幕的组件自动可以访问两个属性:screenProps 和 navigation。在 列表 6.3 中,当你创建导航组件时,你传递了三个 screenProps。在 submit 方法中,你调用了 this.props.screenProps.addCity,访问并调用这个 screenProps 方法。你同样通过调用 this.props.navigation.navigate 来访问导航属性。navigate 是你在 React Navigation 中用于在路由间导航的方法。
接下来,为这个组件添加样式。这段代码位于 src/AddCity/AddCity.js 中的类定义下方。
列表 6.6 AddCity 选项卡(样式)
const styles = StyleSheet.create({
button: {
height: 50,
backgroundColor: '#666',
justifyContent: 'center',
alignItems: 'center',
margin: 10
},
buttonText: {
color: 'white',
fontSize: 18
},
heading: {
color: 'white',
fontSize: 40,
marginBottom: 10,
alignSelf: 'center'
},
container: {
backgroundColor: colors.primary,
flex: 1,
justifyContent: 'center'
},
input: {
margin: 10,
backgroundColor: 'white',
paddingHorizontal: 8,
height: 50
}
})
现在,创建 src/Cities/Cities.js 以列出应用程序存储的所有城市并允许用户导航到单个城市(参见 图 6.5)。功能在下面的列表中显示,样式在 列表 6.8 中。

图 6.5 Cities.js 显示了已添加到应用程序中的城市列表。
列表 6.7 Cities 路由(功能)
import React from 'react'
import {
View,
Text,
StyleSheet,
TouchableWithoutFeedback,
ScrollView
} from 'react-native'
import CenterMessage from '../components/CenterMessage' ①
import { colors } from '../theme'
export default class Cities extends React.Component {
static navigationOptions = { ②
title: 'Cities',
headerTitleStyle: {
color: 'white',
fontSize: 20,
fontWeight: '400'
}
}
navigate = (item) => {
this.props.navigation.navigate('City', { city: item }) ③
}
render() {
const { screenProps: { cities } } = this.props ④
return (
<ScrollView contentContainerStyle={[!cities.length && { flex: 1 }]}>
<View style={[!cities.length &&
{ justifyContent: 'center', flex: 1 }]}>
{
!cities.length && <CenterMessage message='No saved cities!'/> ⑤
}
{
cities.map((item, index) => ( ⑥
<TouchableWithoutFeedback
onPress={() => this.navigate(item)} key={index} >
<View style={styles.cityContainer}>
<Text style={styles.city}>{item.city}</Text>
<Text style={styles.country}>{item.country}</Text>
</View>
</TouchableWithoutFeedback>
))
}
</View>
</ScrollView>
)
}
}
在这个列表中,你首先导入 CenterMessage 组件。React Navigation 有一种方法可以控制路由内的某些导航选项。为此,你可以在类上声明一个静态的 navigationOptions 属性并声明路由的配置。在这种情况下,你想要设置一个标题并设置标题的样式,因此给配置添加一个 title 和 headerTitleStyle 属性。
navigate方法调用this.props.navigation.navigate,并传入路由名称以及要访问的City路由中的城市。传入城市作为第二个参数;在City路由中,你将能够访问props.navigation.state.params中的此属性。渲染方法访问并解构cities数组。它还包括检查cities数组是否为空的逻辑;如果是,向用户显示适当的消息。你映射数组中的所有城市,显示城市名称和国家名称。将navigate方法附加到TouchableWithoutFeedback组件,允许用户通过在城市上的任何地方点击来导航到城市。
列表 6.8 Cities 路由(样式)
const styles = StyleSheet.create({
cityContainer: {
padding: 10,
borderBottomWidth: 2,
borderBottomColor: colors.primary
},
city: {
fontSize: 20,
},
country: {
color: 'rgba(0, 0, 0, .5)'
},
})

图 6.6 City.js 显示了城市内的地点。
接下来,创建City组件(src/Cities/City.js),用于存储每个城市的地点以及一个允许用户在 cities 中创建新地点的表单;参见图 6.6。此组件将从screenProps中访问城市,并使用screenProps中的addLocation方法向城市添加地点。
列表 6.9 City 路由(功能)
import React from 'react'
import {
View,
Text,
StyleSheet,
ScrollView,
TouchableWithoutFeedback,
TextInput,
TouchableOpacity
} from 'react-native'
import CenterMessage from '../components/CenterMessage'
import { colors } from '../theme'
class City extends React.Component {
static navigationOptions = (props) => { ①
const { city } = props.navigation.state.params
return {
title: city.city,
headerTitleStyle: {
color: 'white',
fontSize: 20,
fontWeight: '400'
}
}
}
state = {
name: '',
info: ''
}
onChangeText = (key, value) => {
this.setState({
[key]: value
})
}
addLocation = () => { ②
if (this.state.name === '' || this.state.info === '') return
const { city } = this.props.navigation.state.params
const location = {
name: this.state.name,
info: this.state.info
}
this.props.screenProps.addLocation(location, city)
this.setState({ name: '', info: '' })
}
render() {
const { city } = this.props.navigation.state.params ③
return (
<View style={{ flex: 1 }}>
<ScrollView
contentContainerStyle={
[!city.locations.length && { flex: 1 }]
}>
<View style={[
styles.locationsContainer,
!city.locations.length && { flex: 1,
justifyContent: 'center' }
]}>
{
!city.locations.length &&
<CenterMessage message='No locations for this city!' />
}
{
city.locations.map((location, index) => ( ④
<View key={index} style={styles.locationContainer}>
<Text style={styles.locationName}>{location.name}</Text>
<Text style={styles.locationInfo}>{location.info}</Text>
</View>
))
}
</View>
</ScrollView>
<TextInput ⑤
onChangeText={val => this.onChangeText('name', val)}
placeholder='Location name'
value={this.state.name}
style={styles.input}
placeholderTextColor='white'
/>
<TextInput
onChangeText={val => this.onChangeText('info', val)}
placeholder='Location info'
value={this.state.info}
style={[styles.input, styles.input2]}
placeholderTextColor='white'
/>
<View style={styles.buttonContainer}>
<TouchableOpacity onPress={this.addLocation}>
<View style={styles.button}>
<Text style={styles.buttonText}>Add Location</Text>
</View>
</TouchableOpacity>
</View>
</View>
)
}
}
此代码首先创建navigationOptions属性。你使用回调函数来返回一个对象,而不是直接声明一个对象,因为你需要访问属性,以便访问通过导航传递下来的城市信息。你需要知道城市标题,用作路由标题,而不是硬编码的字符串。
addLocation方法从this.props.navigation.state.params中解构city对象,以便在函数中稍后使用。然后创建一个包含地点名称和信息的location对象。调用this.props.screenProps.addLocation将地点添加到当前查看的城市,并重置状态。再次,从导航状态中解构city。你需要city以便在城市的地点上映射,并在创建新地点时作为参数使用,以标识你引用的城市。最后,你映射城市,返回一个显示城市名称和城市信息的组件,并创建一个包含两个文本输入和一个按钮的表单。
6.3 持久化数据
你已经完成,应该能够运行应用程序。在应用程序中尝试添加城市和地点,然后刷新它。注意,当你刷新时,所有城市都会消失。这是因为你只将数据存储在内存中。让我们使用AsyncStorage来持久化状态,这样如果用户关闭或刷新应用程序,他们的数据仍然可用。
要做到这一点,你将在App组件的 App.js 中工作,并执行以下操作:
-
每次添加新城市时,将城市数组存储在
AsyncStorage中。 -
每次向城市添加新地点时,将城市数组存储在
AsyncStorage中。 -
当用户打开应用时,检查
AsyncStorage以查看是否存储了任何城市。如果是,则使用这些城市更新状态。 -
AsyncStorage只接受字符串作为存储值。因此,在存储值时,如果该值不是字符串,请调用JSON.stringify,如果在使用前需要解析存储的值,请调用JSON.parse。
打开 App.js 并进行以下更改:
- 导入
AsyncStorage并创建一个键变量。
import {
#omitting previous imports
**AsyncStorage**
} from 'react-native';
**const key = 'state'**
export default class App extends Component {
#omitting class definition
- 创建一个
componentDidMount函数,该函数将检查AsyncStorage并获取您设置的键值存储的任何项:
async componentDidMount() {
try {
let cities = await AsyncStorage.getItem(key)
cities = JSON.parse(cities)
this.setState({ cities })
} catch (e) {
console.log('error from AsyncStorage: ', e)
}
}
- 在
addCity方法中,在创建新的cities数组后,将其存储在AsyncStorage中:
addCity = (city) => {
const cities = this.state.cities
cities.push(city)
this.setState({ cities })
**AsyncStorage.setItem(key, JSON.stringify(cities))**
.then(() => console.log('storage updated!'))
.catch(e => console.log('e: ', e))
}
- 更新
addLocation方法,在调用setState后存储城市数组。
addLocation = (location, city) => {
#previous code omitted
**this.setState({**
cities
}, () => {
AsyncStorage.setItem(key, JSON.stringify(cities))
.then(() => console.log('storage updated!'))
.catch(e => console.log('e: ', e))
})
}
现在,当用户关闭应用后再打开它,他们的数据仍然可用。
6.4 使用 DrawerNavigator 创建基于抽屉的导航
我们已经介绍了如何创建基于堆栈和基于标签的导航。让我们看看创建基于抽屉导航的 API。
抽屉导航器的 API 与堆栈和标签导航器非常相似。您将使用 React Navigation 中的 createDrawerNavigator 函数来创建基于抽屉的导航。首先定义要使用的路由:
import Page1 from './routeToPage1'
import Page2 from './routeToPoage2'
接下来,定义您想要在导航器中使用的屏幕:
const screens = {
Page1: { screen: Page1 },
Page2: { screen: Page2 }
}
现在,您可以使用屏幕配置来定义导航器,并在应用中使用它:
const DrawerNav = createDrawerNavigator(screens)
// somewhere in our app
<DrawerNav />
摘要
-
在构建应用之前,花时间策略化您希望它如何处理导航和路由。
-
许多导航库适用于 React Native,但最推荐的两个是 React Navigation 和 React Native Navigation。React Navigation 是一个基于 JavaScript 的导航库,而 React Native Navigation 是一个原生实现。
-
主要有三种类型的导航器:
-
基于标签的导航通常在屏幕的顶部或底部有标签。当您按下标签时,您将被带到与该标签相关的屏幕。例如,
createBottomTabNavigator在屏幕底部创建标签。 -
基于堆栈的导航从一个屏幕切换到另一个屏幕,替换当前屏幕。您可以在堆栈中向后或继续向前移动。基于堆栈的导航通常实现某种动画过渡。您使用
createStackNavigator函数创建基于堆栈的导航。 -
基于抽屉的导航通常是一个从屏幕的左侧或右侧弹出的菜单,显示一系列选项。当您按下选项时,抽屉关闭,您将被带到新的屏幕。您使用
createDrawerNavigator函数创建基于抽屉的导航。 -
根据您使用的导航类型——基于标签、基于堆栈、基于抽屉或这些类型的组合——路由也会有所不同。React Navigation 库管理的每个路由或屏幕都有一个
navigation属性,您可以使用它来控制导航状态。 -
使用
AsyncStorage持久化状态,以便如果用户关闭或刷新应用程序,他们的数据仍然可用。
7
动画
本章 涵盖
-
使用
Animated.timing创建基本动画 -
使用插值与动画值
-
创建动画和并行
-
使用
Animated.stagger交错动画 -
使用原生驱动程序将动画卸载到原生 UI 线程
React Native 的其中一个优点是能够轻松地使用 Animated API 创建动画。这是 React Native 中更稳定且易于使用的 API 之一,并且在 React Native 生态系统中,与导航和状态管理等领域不同,几乎有 100%的共识来解决一个问题。
动画通常用于增强应用程序的 UI,并为现有设计带来更多活力。有时,平均用户体验与优秀用户体验之间的差异可以归因于在正确的时间使用正确的动画,从而将应用程序与其他类似的应用程序区分开来。
本章中涵盖的实际用例包括以下内容:
-
扩展用户输入,当聚焦时进行动画
-
动画欢迎界面比基本的静态欢迎界面更具活力
-
自定义动画加载指示器
在本章中,我们将深入探讨如何创建动画。我们将涵盖您需要了解的一切,以充分利用 Animated API。
7.1 介绍 Animated API
Animated API 随 React Native 一起提供,因此要使用它,您只需像导入任何其他 React Native API 或组件一样导入它。在创建动画时,您始终需要执行以下四个操作:
-
从 React Native 导入 Animated。
-
使用 Animated API 创建一个可动画化的值。
-
将值附加到组件作为样式。
-
使用一个函数动画化可动画化的值。
默认情况下,Animated API 附带四种可动画化组件:
-
View -
ScrollView -
Text -
Image
本章中的示例在这些组件上工作方式完全相同。在第 7.5 节中,我们还介绍了如何使用createAnimatedComponent创建任何元素或组件的自定义动画组件。
让我们快速看一下使用 Animated 的基本动画可能是什么样子。在示例中,您将动画化一个框的顶部边距(见图 7.1)。
列表 7.1 使用 Animated 和更新marginTop属性
import React, { Component } from 'react';
import {
StyleSheet,
View,
Animated, ①
Button
} from 'react-native';
export default class RNAnimations extends Component {
marginTop = new Animated.Value(20); ②
animate = () => { ③
Animated.timing(
this.marginTop,
{
toValue: 200,
duration: 500,
}
).start();
}
render() {
return (
<View style={styles.container}>
<Button ④
title='Animate Box'
onPress={this.animate}
/>
<Animated.View
style={[styles.box, { marginTop: this.marginTop } ]} /> ⑤
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 10,
paddingTop: 50,
},
box: {
width: 150,
height: 150,
backgroundColor: 'red'
}
});

图 7.1 使用 Animated 动画化一个正方形框的顶部边距
本例使用timing函数来动画化一个值。timing函数接受两个参数:一个起始值和一个配置对象。配置对象传递一个toValue来设置动画应该动画化的值,以及一个以毫秒为单位的持续时间来设置动画的长度。
与View组件不同,你使用Animated.View。Animated 有四个可以开箱即用的可动画组件:View、Image、ScrollView和Text。在Animated.View的样式化中,你传入一个包含基本样式(styles.box)和动画样式(marginTop)的样式数组。
现在你已经创建了一个基本的动画组件,你将使用可能有用的真实世界用例创建更多动画。
7.2 动画化表单输入以在聚焦时扩展
在这个例子中,你将创建一个基本的表单输入,当用户聚焦时它会扩展,当输入失焦时会收缩。这是一个流行的 UI 模式。
除了在这本书中与TextInput组件一起使用的属性,例如value、placeholder和onChangeText,你还可以使用onFocus和onBlur在输入聚焦和失焦时调用函数。这就是你将实现这种动画(如图 7.2 所示)的方式。
列表 7.2 动画化 TextInput 以在输入聚焦时扩展
import React, { Component } from 'react';
import {
StyleSheet,
View,
Animated,
Button,
TextInput,
Text,
} from 'react-native';
export default class RNAnimations extends Component {
animatedWidth = new Animated.Value(200); ①
animate = (value) => { ②
Animated.timing(
this.animatedWidth,
{
toValue: value,
duration: 750,
}
).start()
}
render() {
return (
<View style={styles.container}>
<Animated.View style={{ width: this.animatedWidth }}> ③
<TextInput ④
style={[styles.input]}
onBlur={() => this.animate(200)}
onFocus={() => this.animate(325)}
ref={input => this.input = input}
/>
</Animated.View>
<Button
title='Submit'
onPress={() => this.input.blur()}
/>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 10,
paddingTop: 50,
},
input: {
height: 50,
marginHorizontal: 15,
backgroundColor: '#ededed',
marginTop: 10,
paddingHorizontal: 9,
},
});

图 7.2 当输入聚焦时动画化TextInput组件
7.3 使用插值创建自定义加载动画
许多时候,你需要创建无限循环的动画,例如加载指示器和活动指示器。创建此类动画的一种简单方法是用Animated.loop函数。在本节中,你将使用Animated.loop与 Easing 模块一起创建一个加载指示器,无限循环地旋转图像!
到目前为止,我们只看了如何使用Animated.timing调用动画。在这个例子中,你希望动画持续运行而不停止。为此,你将使用一个新的静态方法loop。Animated.loop会持续运行给定的动画:每次到达末尾时,它会重置到开始并重新开始。
你将比过去以不同的方式处理样式。在列表 7.1 和 7.2 中,你直接在组件的style属性中使用动画值。在后续的例子中,你将在变量中存储这些动画值,并在使用新的插值变量在style属性之前对值进行插值。因为你要创建一个旋转效果,所以你会使用字符串而不是数字:例如,你将引用一个如360deg的值用于style。
Animated 有一个名为interpolate的类方法,你可以使用它来操作动画值,将它们转换为你可以使用的其他值。interpolate方法接受一个包含两个键的配置对象:inputRange(数组)和outputRange(也是一个数组)。inputRange是在类中处理的原始动画值,而outputRange指定了原始值应更改到的值。
最后,您将更改动画的缓动值。缓动基本上允许您控制动画的运动。在这个例子中,您希望旋转效果有一个平滑、均匀的运动,所以您将使用线性缓动函数。
React Native 有一个内置的方式来实现常见的缓动函数。就像您导入其他 API 和组件一样,您可以导入 Easing 模块并使用它,与 Animated 一起使用。Easing 可以在配置对象中配置,在Animated.timing的第二个参数中设置值,如toValue和duration。让我们看看一个名为animatedMargin的动画值的例子。将animatedMargin设置为 0 并将值动画化到 200,通常通过在时间函数中直接将值从 0 到 200 动画化来实现缓动效果。使用插值,您可以在时间函数中动画化 0 到 1 之间的值,然后使用 Animated 的interpolate类方法通过使用另一个变量来插值该值,并将该值保存到另一个变量中,然后在样式(通常在渲染方法中)中引用那个变量:
const marginTop = animatedMargin.interpolate({
inputRange: [0, 1],
outputRange: [0, 200],
});
现在,使用插值来创建加载指示器。当应用程序加载时,您将显示指示器;在componentDidMount中,您将调用setTimeout,该调用在 2,000 毫秒后取消加载状态(参见图 7.3)。这里使用的图标位于github.com/dabit3/react-native-in-action/blob/chapter7/assets/35633-200.png;您可以自由使用它或任何其他您想要的图像。

图 7.3 使用插值和动画循环创建旋转加载指示器
列表 7.3 创建无限旋转的加载动画
import React, { Component } from 'react';
import {
Easing,
StyleSheet,
View,
Animated,
Button,
Text,
} from 'react-native';
export default class RNAnimations extends Component {
state = {
loading: true, ①
}
componentDidMount() { ②
this.animate();
setTimeout(() => this.setState({ loading: false }), 2000)
}
animatedRotation = new Animated.Value(0); ③
animate = () => { ④
Animated.loop(
Animated.timing(
this.animatedRotation,
{
toValue: 1,
duration: 1800,
easing: Easing.linear,
}
)
).start()
}
render() {
const rotation = this.animatedRotation.interpolate({ ⑤
inputRange: [0, 1], ⑥
outputRange: ['0deg', '360deg'], ⑦
});
const { loading } = this.state;
return (
<View style={styles.container}>
{
loading ? ( ⑧
<Animated.Image
source={require('./pathtoyourimage.png')}
style={{ width: 40,
height: 40,
transform: [{ rotate: rotation }] }}
/>
) : (
<Text>Welcome</Text>
)
}
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 10,
paddingTop: 50,
},
input: {
height: 50,
marginHorizontal: 15,
backgroundColor: '#ededed',
marginTop: 10,
paddingHorizontal: 9,
},
});
animate类方法将Animated.timing传递给Animated.loop的调用。在配置中,您将toValue设置为 1,duration设置为 1800,easing设置为Easing.linear,以创建平滑的旋转运动。
animatedRotation值通过interpolate方法创建一个新的名为rotation的值。inputRange给出动画的开始和结束值,outputRange给出inputRange应映射到的值:开始值为 0 度,最终值为 360 度,创建一个完整的 360 度旋转。
在return语句中,首先检查loading是否为true。如果是,显示动画加载指示器(更新此路径为您的应用程序中图像的路径);如果是false,则显示欢迎信息。将rotation变量附加到Animated.Image的样式中的transform rotate值。
7.4 创建多个并行动画
有时候您需要同时创建多个动画并使它们同时运行。Animated 库有一个名为parallel的类方法,您可以使用它来做这件事。parallel同时启动一个动画数组。

图 7.4 使用并行动画的欢迎屏幕(动画完成后显示)
例如,要使两个消息和一个按钮同时出现在欢迎屏幕上,你可以创建三个独立的动画,并对每个动画调用.start()。但更高效的方法是使用Animated.parallel函数,并将要同时运行的动画数组传递给它。
在这个例子中,你将创建一个欢迎屏幕,当组件挂载时,屏幕上的两个消息和按钮会进行动画。因为你使用了Animated.parallel,所以所有三个动画将同时开始。你将在配置中添加一个delay属性来控制两个动画的开始时间。
列表 7.4 创建动画欢迎屏幕
import React, { Component } from 'react';
import {
Easing,
StyleSheet,
View,
Animated,
Text,
TouchableHighlight,
} from 'react-native';
export default class RNAnimations extends Component {
animatedTitle = new Animated.Value(-200); ①
animatedSubtitle = new Animated.Value(600); ①
animatedButton = new Animated.Value(800); ①
componentDidMount() {
this.animate(); ②
}
animate = () => { ③
Animated.parallel( [ ③
Animated.timing( ③
this.animatedTitle,
{
toValue: 200,
duration: 800,
}
),
Animated.timing( ③
this.animatedSubtitle,
{
toValue: 0,
duration: 1400,
delay: 800,
}
),
Animated.timing( ③
this.animatedButton,
{
toValue: 0,
duration: 1000,
delay: 2200,
}
)
]).start();
}
render() {
return (
<View style={styles.container}>
<Animated.Text style={[styles.title,
{ marginTop: this.animatedTitle}]}>
Welcome
</Animated.Text> ④
<Animated.Text style={[styles.subTitle,
{ marginLeft: this.animatedSubtitle }]}>
Thanks for visiting our app!
</Animated.Text> ④
<Animated.View style={{ marginTop: this.animatedButton }}> ④
<TouchableHighlight style={styles.button}>
<Text>Get Started</Text>
</TouchableHighlight>
</Animated.View>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
title: {
textAlign: 'center',
fontSize: 20,
marginBottom: 12,
},
subTitle: {
width: '100%',
textAlign: 'center',
fontSize: 18,
opacity: .8,
},
button: {
marginTop: 25,
backgroundColor: '#ddd',
height: 55,
justifyContent: 'center',
alignItems: 'center',
marginHorizontal: 10,
}
});
7.5 创建动画序列
动画序列是一系列依次发生的动画,每个动画在开始之前都要等待前一个动画完成。你可以使用sequence创建动画序列。与parallel类似,sequence也接受一个动画数组:
Animated.sequence([
animationOne,
animationTwo,
animationThree
]).start()
在这个例子中,你将创建一个序列,将数字 1、2 和 3 每隔 500 毫秒依次降落到屏幕上(图 7.5)。

图 7.5 创建数字的动画序列
列表 7.5 创建动画序列
import React, { Component } from 'react';
import {
StyleSheet,
View,
Animated ①
} from 'react-native';
export default class RNAnimations extends Component {
componentDidMount() {
this.animate(); ②
}
AnimatedValue1 = new Animated.Value(-30); ③
AnimatedValue2 = new Animated.Value(-30); ③
AnimatedValue3 = new Animated.Value(-30); ③
animate = () => {
const createAnimation = (value) => { ④
return Animated.timing(
value, {
toValue: 290,
duration: 500
})
}
Animated.sequence( [ ⑤
createAnimation(this.AnimatedValue1), ⑤
createAnimation(this.AnimatedValue2), ⑤
createAnimation(this.AnimatedValue3) ⑤
]).start() ⑤
}
render() {
return (
<View style={styles.container}>
<Animated.Text style={[styles.text,
{ marginTop: this.AnimatedValue1}]}> ⑥
1
</Animated.Text>
<Animated.Text style={[styles.text,
{ marginTop: this.AnimatedValue2}]}> ⑥
2
</Animated.Text>
<Animated.Text style={[styles.text,
{ marginTop: this.AnimatedValue3}]}> ⑥
3
</Animated.Text>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
flexDirection: 'row',
},
text: {
marginHorizontal: 20,
fontSize: 26
}
});
这个例子使用初始动画值为-30,因为它们是文本元素的marginTop值:文本在动画开始之前被拉出屏幕并隐藏。createAnimation函数也接收一个动画值作为其参数。
7.6 使用 Animated.stagger 错开动画开始时间
我们将要介绍的最后一种动画类型是Animated.stagger。与parallel和sequence类似,stagger也接受一个动画数组。动画数组以并行方式开始,但所有动画的开始时间均匀错开。与parallel和sequence不同,stagger的第一个参数是错开时间,第二个参数是动画数组:
Animated.stagger(
100,
[
Animation1,
Animation2,
Animation3
]
).start()
在这个例子中,你将动态创建大量动画,用于将一系列红色框错开显示在屏幕上(图 7.6)。

图 7.6 使用 Animated.stagger 创建错开动画数组
列表 7.6 使用Animated.stagger来错开一系列动画的开始时间
import React, { Component } from 'react'
import {
StyleSheet,
View,
Animated ①
} from 'react-native'
export default class RNAnimations extends Component {
constructor () {
super()
this.animatedValues = [] ②
for (let i = 0; i < 1000; i++) { ②
this.animatedValues[i] = new Animated.Value(0) ②
} ②
this.animations = this.animatedValues.map(value => { ③
return Animated.timing( ③
value, ③
{ ③
toValue: 1, ③
duration: 6000 ③
} ③
) ③
})
}
componentDidMount() {
this.animate() ④
}
animate = () => {
Animated.stagger(15, this.animations).start() ⑤
}
render() {
return (
<View style={styles.container}>
{
this.animatedValues.map((value, index) => ( ⑥
<Animated.View key={index}
style={[{opacity: value},
styles.box]} /> ⑥
)) ⑥
}
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
flexDirection: 'row',
flexWrap: 'wrap'
},
box: {
width: 15,
height: 15,
margin: .5,
backgroundColor: 'red'
}
})
7.7 使用 Animated 库的其他有用技巧
除了我们已经介绍过的 Animated API 的部分之外,还有一些其他技巧值得了解:重置动画值、调用回调、将动画卸载到原生线程以及创建自定义可动画组件。本节将简要介绍这些内容。
7.7.1 重置动画值
如果你正在调用动画,你可以使用 setValue(value) 将值重置为你想要的任何值。如果你已经对一个值调用过动画,需要再次调用动画,并且希望将值重置为原始值或新值,这很有用:
animate = () => {
this.animatedValue.setValue(300);
#continue here with the new animated value
}
7.7.2 调用回调
当动画完成时,可以触发一个可选的回调函数,如下所示:
Animated.timing(
this.animatedValue,
{
toValue: 1,
duration: 1000
}
).start(() => console.log('animation is complete!'))
7.7.3 将动画卸载到原生线程
默认情况下,Animated 库使用 JavaScript 线程执行动画。在大多数情况下,这没问题,你不太会遇到性能问题。但如果有任何操作阻塞了 JavaScript 线程,你可能会看到帧被跳过,导致动画卡顿或跳跃。
有一种绕过使用 JavaScript 线程的方法:你可以使用一个名为 useNativeDriver 的配置布尔值。useNativeDriver 将动画卸载到原生 UI 线程,然后原生代码可以直接在 UI 线程上更新视图,如下所示:
Animated.timing(
this.animatedValue,
{
toValue: 100,
duration: 1000,
useNativeDriver: true
}
).start();
并非所有动画都可以使用 useNativeDriver 卸载,所以当你使用它时,务必检查 Animated API 文档。截至本文撰写时,只有非布局属性可以使用此方法进行动画化;flexbox 属性以及诸如边距和填充之类的属性不能进行动画化。
7.7.4 使用 createAnimatedComponent 创建自定义可动画化组件
我们在 7.1 节中提到,默认情况下唯一可动画化的组件是 View、Text、Image 和 ScrollView。还有一种方法可以从任何现有或自定义 React Native 元素或组件创建动画组件。你可以通过将组件包裹在 createAnimatedComponent 的调用中来实现这一点。以下是一个示例:
const Button = Animated.createAnimatedComponent(TouchableHighlight)
<Button onPress={somemethod} style={styles.button}>
<Text>Hello World</Text>
</Button>
现在,你可以像使用常规 React Native 组件一样使用按钮。
摘要
-
内置的 Animated API 是在 React Native 中创建动画的推荐方式。
-
Animated.timing是使用 Animated 库创建动画的主要方法。 -
默认情况下,唯一可动画化的组件是
View、Text、ScrollView和Image,但你可以使用createAnimatedComponent创建自定义的可动画化组件。 -
要插值和重用动画值,请使用 Animated 的
interpolate方法。 -
要同时创建和触发一组动画,请使用
Animated.parallel。 -
要创建无限循环的动画,请使用
Animated.loop。 -
使用
Animated.sequence创建一系列依次执行的动画。 -
使用
Animated.stagger创建一个并行发生的动画数组,但其开始时间基于传入的时间进行错位。
8
使用 Redux 数据架构库
本章 涵盖
-
React 上下文 API 的工作原理
-
创建 Redux
store -
如何使用 Redux 动作和 Reducers 管理全局状态
-
使用
combineReducers进行 Reducer 组合
在现实世界中构建 React 和 React Native 应用程序时,你会很快学到,如果处理不当,数据层可能会变得复杂且难以管理。处理数据的一种方法是将它保存在组件状态中,并作为 props 传递,正如我们在本书中所做的那样。另一种方法是使用数据架构模式或库。本章介绍了 Redux 库:它是 React 生态系统中处理数据最广泛采用的方法,由维护 React 和 React Native 的 Facebook 团队维护。
8.1 什么是 Redux?
在 Redux 文档中,该库被描述为“JavaScript 应用程序的可预测状态容器。”Redux 基本上是一个全局状态对象,它是应用程序中的唯一真相来源。这个全局状态对象作为 props 被接收进 React Native 组件。每当 Redux 状态中的某个数据被更改时,整个应用程序都会以 props 的形式接收这个新数据。
Redux 通过将所有状态移动到一个称为store的地方来简化应用程序状态;这使得推理和理解变得更加容易。当你需要某个值时,你将知道在 Redux 应用程序中确切地在哪里查找,并且可以预期在应用程序的其他地方也能获得相同且最新的值。
那么,Redux 是如何工作的呢?它利用了 React 的一个特性,称为上下文,这是一种创建和管理全局状态的机制。
8.2 在 React 应用程序中使用上下文创建和管理全局状态
上下文是 React API,它创建可以在应用程序的任何地方访问的全局变量,只要接收上下文的组件是创建它的组件的子组件。通常,你需要通过将 props 传递到组件结构的每一层来实现这一点。使用上下文,你不需要使用 props。你可以在应用程序的任何地方使用上下文,并访问它而无需将其传递到每一层。
让我们看看如何在三个组件的基本组件结构中创建上下文:Parent、Child1和Child2。这个例子展示了如何从父级应用应用范围的主题,如果需要,这可以使得控制整个应用程序的样式成为可能。
列表 8.1 创建上下文
const ThemeContext = React.createContext() ①
class Parent extends Component {
state = { themeValue: 'light' } ②
toggleThemeValue = () => { ③
const value = this.state.themeValue === 'dark' ? 'light' : 'dark' ③
this.setState({ themeValue: value }) ③
} ③
render() {
return (
<ThemeContext.Provider ④
value={{
themeValue: this.state.themeValue,
toggleThemeValue: this.toggleThemeValue
}}
>
<View style={styles.container}>
<Text>Hello World</Text>
</View>
<Child1 />
</ThemeContext.Provider>
);
}
}
const Child1 = () => <Child2 /> ⑤
const Child2 = () => ( ⑥
<ThemeContext.Consumer>
{(val) => (
<View style={[styles.container,
val.themeValue === 'dark' &&
{ backgroundColor: 'black' }]}>
<Text style={styles.text}>Hello from Component2</Text>
<Text style={styles.text}
onPress={val.toggleThemeValue}>
Toggle Theme Value
</Text>
</View>
)}
</ThemeContext.Consumer>
)
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
text: {
fontSize: 22,
color: '#666'
}
})
Child2 无状态函数返回一个被ThemeContext.Consumer包裹的组件。ThemeContext.Consumer需要一个函数作为其子元素。该函数接收一个包含任何可用上下文的参数(在这个例子中,包含两个属性的val对象)。现在你可以在组件中使用这些上下文值。
当你使用 Redux 与 React 结合时,你会利用一个名为connect的函数,该函数基本上将上下文的片段转换为组件的 props。理解上下文应该会使学习 Redux 变得更加容易!
8.3 在 React Native 应用程序中实现 Redux
现在你已经了解了 Redux 的基础知识,并看到了在上下文背后的操作,让我们创建一个新的 React Native 应用程序并开始添加 Redux。你将创建一个基本的列表应用程序,你可以用它来跟踪你读过的书籍(参见 图 8.1)。按照以下步骤操作:

图 8.1 完成的图书列表应用

图 8.2 RNRedux src 文件夹结构
- 创建一个新的 React Native 应用程序,并将其命名为 RNRedux:
react-native init RNRedux
- 切换到新目录:
cd RNRedux
- 安装你需要的 Redux 特定依赖项:
npm i redux react-redux –-save
- 在目录的根目录下创建一个名为 src 的文件夹,并向其中添加以下文件:Books.js 和 actions.js。此外,在 src 中创建一个名为 reducers 的文件夹,包含两个文件:bookReducer.js 和 index.js。src 文件夹结构现在应该看起来像 图 8.2。
下一步是创建第一个 Redux 状态。你将在 bookReducer.js 中做这件事。在第 8.1 节中,我将 Redux 描述为一个全局对象。要创建这个全局对象,你将使用所谓的 Reducer 将更小的对象拼接在一起。
8.4 创建 Redux Reducer 以存储 Redux 状态
Reducer 是一个返回对象的函数;当与其他 Reducer 结合时,它们会创建全局状态。Reducer 可以更容易地被视为数据存储。每个存储包含一块数据,这正是 Redux 架构中 Reducer 所做的。
在 reducers 文件夹中有两个文件:bookReducer.js 和 index.js。在 index.js 中,你将结合应用中的所有 Reducer 来创建全局状态。应用一开始将只有一个 Reducer (bookReducer),因此全局状态对象将看起来像这样:
{
bookReducer: {}
}
你还没有决定在 bookReducer 中放置什么。一个用于存储图书列表的数组将是一个好的开始。这个 Reducer 将创建并返回一个状态,你稍后将从 Redux 存储中访问它。在 reducers/bookReducer.js 中,创建你的第一个 Reducer。这段代码创建了一个函数,其唯一目的(目前)是返回状态。
列表 8.2 创建 Reducer
const initialState = { ①
books: [{ name: 'East of Eden', author: 'John Steinbeck' }] ①
} ①
const bookReducer = (state = initialState) => { ②
return state ③
}
export default bookReducer
initialState 对象将保存初始状态。在这种情况下,那是一个包含 name 和 author 属性的对象数组,你将用这些对象填充它。你创建一个函数,该函数接受一个参数 state,并将默认值设置为初始状态。当这个函数第一次被调用时,state 将是未定义的,并将返回 initialState 对象。此时,这个函数的唯一目的是返回状态。
现在你已经创建了第一个 Reducer,进入 rootReducer(reducers/index.js)并创建将成为全局状态的内容。根 Reducer 收集应用中的所有 Reducer,并允许你通过将它们组合起来创建一个全局存储(状态对象)。
列表 8.3 创建根 Reducer
import { combineReducers } from 'redux' ①
import bookReducer from './bookReducer' ②
const rootReducer = combineReducers({ ③
bookReducer
})
export default rootReducer
接下来,为了将这些组件连接起来,你将进入 App.js,创建 Redux 存储,并使用几个 Redux 和 React-Redux 辅助函数使存储对所有子组件可用。
8.5 添加提供者并创建存储
在本节中,你将在应用中添加一个 provider。provider 通常是一个父组件,它将某种类型的数据传递给所有子组件。在 Redux 中,provider 将全局状态/存储传递给应用的其他部分。在 App.js 中,按照以下方式更新代码。

图 8.3 从 Redux 存储渲染书籍列表
列表 8.4 添加提供者和存储
import React from 'react'
import Books from './src/Books' ①
import rootReducer from './src/reducers' ②
import { Provider } from 'react-redux' ③
import { createStore } from 'redux' ④
const store = createStore(rootReducer) ⑤
export default class App extends React.Component {
render() {
return (
<Provider store={store} > ⑥
<Books /> ⑥
</Provider> ⑥
)
}
}
Provider 包装器用于包装主组件。Provider 的任何子组件都将能够访问 Redux 存储。createStore 是 Redux 中的一个实用工具,你通过传递 rootReducer 来创建 Redux 存储。你已经完成了基本的 Redux 设置,现在你可以通过 Redux 和 React-Redux 的几个辅助函数在应用中访问 Redux 存储。
在 Books 组件中,你将连接到 Redux 存储,提取 books 数组,并对书籍进行映射,在 UI 中显示它们(图 8.3)。因为 Books 是 Provider 的子组件,它可以访问 Redux 存储中的任何内容。
8.6 使用 connect 函数访问数据
你可以通过使用 react-redux 中的 connect 函数从子组件访问 Redux 存储。connect 的第一个参数是一个函数,它给你提供访问整个 Redux 状态的权限。然后你可以返回一个对象,包含你想要访问的存储的任何部分。
connect 是一个 curried 函数,这意味着在最基本的意义上,它是一个返回另一个函数的函数。你将有两组参数,一个看起来像这样的蓝图:connect(args)(args)。从 connect 的第一个参数返回的对象中的属性然后作为 props 传递给组件。
让我们通过查看你在 Books.js 组件中将使用的 connect 函数来了解这意味着什么。
列表 8.5 Books.js 中的 connect 函数
connect(
(state) => { ①
return { ②
books: state.bookReducer.books
}
}
)(Books) ③
connect 函数的第一个参数是一个函数,它将全局 Redux state 对象作为参数传递。然后你可以引用这个 state 对象,并访问 Redux 状态中的任何内容。从这个函数中返回一个对象。对象中返回的任何键都将成为你包装的组件(在这个例子中是 Books)中的 props。你将 Books 作为 connect 函数第二个函数调用的唯一参数传递。
通常,你会将这个函数分离并存储在一个变量中,以便更容易阅读。
const mapStateToProps = state => ({
books: state.bookReducer.books
})
在这个连接组件中有一个新的属性叫做 this.props.books,这是来自 bookReducer 的 books 数组。将这些全部结合起来,访问 books 数组,并对书籍进行映射,以显示在 UI 中,如下所示(Books.js)。
列表 8.6 访问 Redux 存储 和 bookReducer 数据
import React from 'react'
import {
Text,
View,
ScrollView,
StyleSheet
} from 'react-native'
import { connect } from 'react-redux' ①
class Books extends React.Component<{}> {
render() {
const { books } = this.props ②
return (
<View style={styles.container}>
<Text style={styles.title}>Books</Text>
<ScrollView
keyboardShouldPersistTaps='always'
style={styles.booksContainer}
>
{
books.map((book, index) => ( ③
<View style={styles.book} key={index}> ③
<Text style={styles.name}>{book.name}</Text> ③
<Text style={styles.author}>{book.author}</Text> ③
</View> ③
)) ③
}
</ScrollView>
</View>
)
}
}
const styles = StyleSheet.create({
container: {
flex: 1
},
booksContainer: {
borderTopWidth: 1,
borderTopColor: '#ddd',
flex: 1
},
title: {
paddingTop: 30,
paddingBottom: 20,
fontSize: 20,
textAlign: 'center'
},
book: {
padding: 20
},
name: {
fontSize: 18
},
author: {
fontSize: 14,
color: '#999'
}
})
const mapStateToProps = (state) => ({ ④
books: state.bookReducer.books ④
}) ④
export default connect(mapStateToProps)(Books) ⑤
你首先从 react-redux 中导入 connect。在 列表 8.5 中,你直接在函数中返回了属性。此列表将其分离并命名为 mapStateToProps,遵循 Redux 生态系统的约定。这种命名约定非常有意义,因为你实际上是在将 Redux 状态映射到组件属性。此函数接受 Redux 状态作为参数,并返回一个包含一个键的对象,该键包含 bookReducer 中的 books 数组。最后,你导出 connect 函数,将 mapStateToProps 作为 connect 的第一个参数传递,并将 Books 作为 connect 第二组参数中的唯一参数。
启动应用程序后,你应该会看到一个基本的书籍列表,如图 8.3 中所示。
8.7 添加动作
现在你已经可以访问 Redux 状态,下一步的逻辑是添加一些功能,允许你向 Redux 存储 books 数组中添加书籍。为此,你将使用 动作。动作基本上是返回对象的函数,这些对象将数据发送到存储并更新 reducer;它们是更改存储的唯一方式。每个动作都应该包含一个 type 属性,以便 reducer 能够使用它们。以下是一些动作的示例:
function fetchBooks() {
return {
type: 'FETCH_BOOKS'
}
}
function addBook(book) {
return {
type: 'Add_BOOK',
book: book
}
}
当使用 Redux 的 dispatch 函数调用动作时,这些动作作为第二个参数发送到应用程序中的所有 reducer。(我们将在本章后面介绍如何附加 Redux 的 dispatch 函数。)当 reducer 接收到动作时,你检查动作的 type 属性,并根据动作是否是它正在监听的动作来更新 reducer 返回的内容。
在这个例子中,你只需要为下一步添加一个 addBook 动作,以便向书籍数组中添加额外的书籍。在 actions.js 文件中,创建以下动作。
列表 8.7 创建第一个动作
export const ADD_BOOK = 'ADD_BOOK' ①
export function addBook (book) { ②
return {
type: ADD_BOOK,
book
}
}
接下来,将 bookReducer 连接到使用 addBook 动作。
列表 8.8 更新 bookReducer 以使用 addBook 动作
import { ADD_BOOK } from '../actions' ①
const initialState = {
books: [{ name: 'East of Eden', author: 'John Steinbeck' }]
}
const bookReducer = (state = initialState, action) => { ②
switch(action.type) { ③
case ADD_BOOK:
return {
books: [ ④
...state.books,
action.book
]
}
default: ⑤
return state
}
}
export default bookReducer
在列表中,如果动作类型等于 ADD_BOOK,你将返回一个新的 books 数组,包含数组中的所有先前项。你是通过创建一个新的数组,使用扩展运算符将现有 books 数组的元素添加到新数组中,并将动作的 book 属性作为新项添加到数组中。
在 Redux 配置中,你需要做的就这些来使它工作。最后一步是进入 UI 并将其全部连接起来。为了获取用户的书籍信息,你需要创建一个表单。图 8.4 展示了 UI 将会是什么样子。
此表单有两个输入:一个用于书籍名称,一个用于作者名称。它还有一个提交按钮。当用户在表单中输入时,你需要跟踪本地状态中的值。然后,当用户点击提交按钮时,你可以将这些值传递给动作。

图 8.4 添加了文本输入以捕获书籍和作者名称的 UI
打开Books.js,导入实现此功能所需的额外组件,以及从 actions 中导入的addBook函数。你还将创建一个initialState变量,用作本地组件状态。
列表 8.9 Books.js 中的额外导入
import React from 'react'
import {
Text,
View,
ScrollView,
StyleSheet,
TextInput, ①
TouchableOpacity ①
} from 'react-native'
import { addBook } from './actions' ②
import { connect } from 'react-redux'
const initialState = { ③
name: '', ③
author: '' ③
}
...
接下来,在类的主体中,你需要创建三样东西:组件状态,一个当textInput值改变时跟踪组件状态的方法,以及一个当提交按钮被按下时将包含书籍值(名称和作者)的动作发送到 Redux 的方法。在render方法之前,添加以下代码。
列表 8.10 向 Books.js 添加状态和类方法
class Books extends React.Component {
state = initialState ①
updateInput = (key, value) => { ②
this.setState({
...this.state,
[key]: value
})
}
addBook = () => { ③
this.props.dispatchAddBook(this.state)
this.setState(initialState)
}
...
addBook方法调用一个你可以通过connect函数访问的 props 中的函数:dispatchAddBook。这个函数接受整个状态作为参数,它是一个具有name和author属性的对象。在调用 dispatch 动作之后,然后通过将其重置为initialState值来清除组件状态。
在功能实现后,你可以创建 UI 并将这些方法连接到它。在Books.js中的ScrollView关闭标签下添加表单 UI。
列表 8.11 添加表单的 UI
class Books extends React.Component {
...
render() {
...
</ScrollView>
<View style={styles.inputContainer}>
<View style={styles.inputWrapper}>
<TextInput ①
value={this.state.name}
onChangeText={value => this.updateInput('name', value)}
style={styles.input}
placeholder='Book name'
/>
<TextInput ①
value={this.state.author}
onChangeText={value => this.updateInput('author', value)}
style={styles.input}
placeholder='Author Name'
/>
</View>
<TouchableOpacity onPress={this.addBook}> ②
<View style={styles.addButtonContainer}>
<Text style={styles.addButton}>+</Text>
</View>
</TouchableOpacity>
</View>
</View>
}
}
const styles = StyleSheet.create({
inputContainer: { ③
padding: 10,
backgroundColor: '#ffffff',
borderTopColor: '#ededed',
borderTopWidth: 1,
flexDirection: 'row',
height: 100
},
inputWrapper: {
flex: 1
},
input: {
height: 44,
padding: 7,
backgroundColor: '#ededed',
borderColor: '#ddd',
borderWidth: 1,
borderRadius: 10,
flex: 1,
marginBottom: 5
},
addButton: {
fontSize: 28,
lineHeight: 28
},
addButtonContainer: {
width: 80,
height: 80,
backgroundColor: '#ededed',
marginLeft: 10,
justifyContent: 'center',
alignItems: 'center',
borderRadius: 20
},
...
}
const mapDispatchToProps = { ④
dispatchAddBook: (book) => addBook(book)
}
export default connect(mapStateToProps, mapDispatchToProps)(Books) ⑤
}
在mapDispatchToProps对象中,你可以声明你希望在组件的 props 中访问的函数。你创建一个新的函数名为dispatchAddBook,并让它调用addBook动作,传入book作为参数。类似于mapStateToProps将状态映射到组件 props,mapDispatchToProps将需要分发到 reducer 的动作映射到组件 props。为了使一个动作能被 Redux 的 reducer 识别,它必须在mapDispatchToProps对象中声明。你将mapDispatchToProps作为connect函数的第二个参数传入。
现在你应该能够轻松地将书籍添加到书单中。
8.8 在 reducer 中从 Redux 存储中删除项目
下一个合乎逻辑的步骤是添加一种方法来移除你已经读过的书籍。考虑到你已经构建的一切,这不会需要太多额外的工作(图 8.5)。

图 8.5 在 Books.js UI 中添加移除按钮
当从类似这样的数组中移除项时,首先要考虑的是如何识别一个书籍作为唯一的。目前,一个用户可能有多个作者相同的书籍或多个名称相同的书籍,所以使用现有的属性将不起作用。相反,你可以使用如 uuid 这样的库来动态创建唯一标识符。为了开始设置,从命令行,将 uuid 库安装到node_modules:
npm i uuid --save
接下来,你将在 reducer 中为initialState中的books数组项实现一个唯一标识符。在reducers/bookReducer.js中,更新导入和initialState以类似于以下列表。
列表 8.12 导入和使用 uuid
import uuidV4 from 'uuid/v4' ①
import { ADD_BOOK } from '../actions'
const initialState = { ②
books: [{ name: 'East of Eden', author: 'John Steinbeck', id: uuidV4() }]
}
uuid 库提供了一些算法可供选择。在这里,你只导入 v4 算法,它创建一个随机的 32 位字符字符串。然后你向 initialState 的 books 数组添加一个新的属性 id,并通过调用 uuidV4() 生成一个新的唯一标识符。
现在你有了在 books 数组中唯一标识项目的方法,你就可以继续实现其余的功能。下一步是在 actions.js 中创建一个新的动作;当你想要删除一本书时,你会调用它。你还需要更新 addBook 动作,以便为新创建的书籍添加一个 ID。
列表 8.13 创建 removeBook 动作
export const ADD_BOOK = 'ADD_BOOK'
export const REMOVE_BOOK = 'REMOVE_BOOK' ①
import uuidV4 from 'uuid/v4' ②
export function addBook (book) {
return {
type: ADD_BOOK,
book: {
...book,
id: uuidV4() ③
}
}
}
export function removeBook (book) { ④
return {
type: REMOVE_BOOK,
book
}
}
接下来,reducer 需要意识到新的动作。在 reducers/bookReducer.js 中,创建一个新的类型监听器,这个监听器用于 REMOVE_BOOK,并添加必要的功能以从 Redux 状态中存储的书籍数组中删除一本书。
列表 8.14 在 Redux reducer 中从数组中移除一个项目
import uuidV4 from 'uuid/v4'
import { ADD_BOOK, REMOVE_BOOK } from '../actions' ①
const initialState = {
books: [{ name: 'East of Eden', author: 'John Steinbeck', id: uuidV4() }]
}
const bookReducer = (state = initialState, action) => {
switch(action.type) {
...
case REMOVE_BOOK: ②
const index = state.books.findIndex(
book => book.id === action.book.id) ③
return {
books: [ ④
...state.books.slice(0, index),
...state.books.slice(index + 1)
]
}
...
}
}
export default bookReducer
最后一件要做的事情是在 Books 组件(Books.js)的 UI 中实现这个新的 removeBook 功能。你会导入 removeBook 动作,为每个渲染的项目添加一个删除按钮,并将删除按钮连接到 removeBook 动作。
列表 8.15 添加 removeBook 功能
...
import { addBook, removeBook } from './actions' ①
...
removeBook = (book) => { ②
this.props.dispatchRemoveBook(book) ②
}
...
{
books.map((book, index) => ( ③
<View style={styles.book} key={index}> ③
<Text style={styles.name}>{book.name}</Text> ③
<Text style={styles.author}>{book.author}</Text> ③
<Text onPress={() => this.removeBook(book)}> ③
Remove ③
</Text> ③
</View> ③
))
}
...
const mapDispatchToProps = {
dispatchAddBook: (book) => addBook(book),
dispatchRemoveBook: (book) => removeBook(book) ④
}
...
摘要
-
使用上下文,你可以在 React Native 应用程序中向子组件传递属性和数据,而无需显式地将属性传递给每个单独的子组件。
-
Reducer 在某种程度上类似于传统的数据存储,因为它们会跟踪并返回数据,同时也允许你更新存储中的数据。
-
你可以创建并使用动作来更新 Redux 存储。
-
使用
connect函数,你可以将 Redux 状态中的数据作为 props 访问,并且可以创建与 reducer 通过动作交互的 dispatch 函数。 -
任何需要在 reducer 中更改数据的时候,都必须通过使用一个动作来完成。**
第三部分
API 参考
React Native 提供了丰富的 API。本部分章节涵盖了跨平台 API 以及特定于 iOS 和 Android 平台的 API。
在第九章中,我们探讨了使用 React Native 的跨平台 API:可以在 iOS 或 Android 上使用的 API,用于创建提醒;检测应用是否在前台、后台或非活动状态;持久化、检索和删除数据;存储和更新文本到设备剪贴板;以及执行许多其他有用任务。在第十章和第十一章中,我们将探讨 React Native 针对 iOS 平台或 Android 平台的特定 API。
9
实现跨平台 API
本章内容涵盖
-
创建原生应用程序警告对话框
-
检测应用是否处于前台、后台或非活动状态
-
将文本存储和更新到设备剪贴板
-
使用地理位置获取并使用用户设备的纬度、经度、速度和高度
-
检测设备属性,如屏幕的高度和宽度以及连接类型
使用 React Native 的一个关键好处是,可以轻松地使用 JavaScript 访问和使用原生 API。在本章中,我们将介绍框架中大多数可用的跨平台 API。当访问这些 API 时,您将能够使用单个代码库在 iOS 和 Android 上实现特定于平台的行为。
本章讨论的原生 API 与原生组件之间的主要区别在于,原生组件通常与 UI 有关,例如显示特定的 UI 元素。另一方面,API 更多地涉及访问手机中的原生功能和硬件,例如与设备中的数据(地理位置、应用状态等)交互或访问。
本章涵盖了以下跨平台 API:
-
Alert
-
AppState
-
AsyncStorage
-
Clipboard
-
Dimensions
-
地理位置服务
-
键盘
-
NetInfo
-
PanResponder
虽然 React Native 提供了其他跨平台 API,但你会发现这些是最有用的。
除了跨平台 API 之外,React Native 还提供了特定于平台的 API(即仅在 iOS 或 Android 上工作的 API)。我们将在第十章中介绍特定于 iOS 的 API,在第十一章中介绍特定于 Android 的 API。
9.1 使用 Alert API 创建跨平台通知
Alert 通过调用alert方法(Alert.alert)来启动具有标题、消息和可选方法的平台特定警告对话框,这些方法可以在按下警告按钮时调用。Alert 可以通过调用alert方法触发,该方法接受四个参数(见表 9.1):
Alert.alert(title, message, buttons, options)
表 9.1 Alert.alert方法参数
| 参数 | 类型 | 描述 |
|---|---|---|
title |
字符串 | 提醒按钮的主要信息 |
message |
字符串 | 提醒按钮的次要信息 |
buttons |
数组 | 按钮数组,每个按钮都是一个具有两个键的对象:title(字符串)和onPress(函数) |
options |
对象 | 包含可取消布尔值的对象(选项:{ cancelable: true }) |
9.1.1 警报的使用用例
警报是在网页和移动设备上常见的 UI 模式,并且是让用户了解应用程序中发生的事情(如错误或成功)的简单方式。很多时候,如果下载完成、发生错误或异步过程(如登录)完成,就会使用警报。
9.1.2 使用警报的示例
你可以通过调用 Alert.alert() 方法并传入一个或多个参数来触发一个警报。在这个示例中,你将创建一个带有两个选项的警报:取消和显示消息(见图 9.1)。如果按下取消,你将关闭警报;如果按下显示消息,你将更新状态以显示消息。
列表 9.1 将警报绑定到触摸事件
import React, { Component } from 'react'
import { TouchableHighlight, View, Text, StyleSheet, Alert }
from 'react-native' ①
let styles = {}
export default class App extends Component {
constructor () {
super()
this.state = { ②
showMessage: false ②
}
this.showAlert = this.showAlert.bind(this)
}
showAlert () { ③
Alert.alert(
'Title',
'Message!',
{
text: 'Cancel',
onPress: () => console.log('Dismiss called...'),
style: 'destructive'
},
{
text: 'Show Message', [ ④
onPress: () => this.setState({ showMessage: true }) ④
}
]
)
}
render () {
const { showMessage } = this.state
return (
<View style={styles.container}>
<TouchableHighlight onPress={this.showAlert} style={styles.button}>
<Text>SHOW ALERT</Text>
</TouchableHighlight>
{ ⑤
showMessage && <Text>Showing message - success</Text> ⑤
} ⑤
</View>
)
}
}
styles = StyleSheet.create({
container: {
justifyContent: 'center',
flex: 1
},
button: {
height: 70,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#ededed'
}
})

图 9.1 按压警报带有两个选项:取消和显示消息(左:iOS,右:Android)
9.2 使用 AppState API 检测当前应用程序状态
AppState 会告诉你应用是活动状态、非活动状态还是后台状态。它基本上会在应用状态改变时调用一个方法,允许你根据应用的状态执行操作或调用其他方法。
当应用状态改变时,AppState 会触发并返回 active、inactive 或 background。要响应应用状态改变,添加事件监听器并在事件触发时调用一个方法。AppState 用于响应的事件是 change 和 memorywarning。本节的示例使用 change,因为在现实场景中你主要会用到它。
9.2.1 AppState 的使用用例
AppState 是一个有用的 API,并且经常派上用场。很多时候,当应用被拉回前台时,你可能想做一些事情,比如从你的 API 中获取新鲜数据——这就是 AppState 的一个很好的用例。
另一个用例是身份验证。当应用置于前台时,你可能想添加另一层安全措施,例如 PIN 码或指纹。
如果你正在进行轮询,例如每 15 秒左右击打一次数据库以检查新数据,那么当用户将应用推入后台时,你可能想禁用轮询。AppState 也是这种情况的一个很好的用例。
9.2.2 使用 AppState 的示例
在这个示例中,你将在 componentDidMount 中添加一个事件监听器,监听 change 事件,然后在控制台显示当前状态。
列表 9.2 使用 AppState 记录当前应用状态
import React, { Component } from 'react'
import { AppState, View, Text, StyleSheet } from 'react-native' ①
let styles = {}
class App extends Component {
componentDidMount () { ②
AppState.addEventListener('change', this.handleAppStateChange) ②
}
handleAppStateChange (currentAppState) { ③
console.log('currentAppState:', currentAppState)
}
render () {
return (
<View style={styles.container}>
<Text>Testing App State</Text>
</View>
)
}
}
styles = StyleSheet.create({
container: {
justifyContent: 'center',
flex: 1
}
})
export default App
运行项目,并通过在 iOS 模拟器中按 CMD-Shift-H 或在 Android 模拟器中按主页按钮来测试它。控制台应记录当前应用状态(活动、非活动或后台)。
9.3 使用 AsyncStorage API 持久化数据
接下来是 AsyncStorage。AsyncStorage 是持久化和存储数据的好方法:它是异步的,这意味着你可以使用 promise 或async await检索数据,并且它使用键值系统来存储和检索数据。
当你使用一个应用然后关闭它时,它的状态将在你下次打开时重置。AsyncStorage 的主要优点之一是它允许你直接将数据存储到用户的设备上,并在需要时检索它!
AsyncStorage 的方法和参数列于表 9.2 中。
表 9.2 AsyncStorage 方法和参数
| 方法 | 参数 | 描述 |
|---|---|---|
setItem |
key, value, callback |
在 AsyncStorage 中存储一个项 |
getItem |
key, callback |
从 AsyncStorage 中检索一个项 |
removeItem |
key, callback |
从 AsyncStorage 中删除一个项 |
mergeItem |
key, value, callback |
将一个现有值与另一个现有值合并(两个值都必须是字符串化的 JSON) |
clear |
callback |
删除 AsyncStorage 中的所有值 |
getAllKeys |
callback |
获取应用中存储的所有键 |
flushGetRequests |
无 | 清除任何挂起的请求 |
multiGet |
[keys], callback |
允许你使用键数组获取多个值 |
multiSet |
[keyValuePairs], callback |
允许你一次性设置多个键值对 |
multiRemove |
[keys], callback |
允许你使用键数组删除多个值 |
multiMerge |
[keyValuePairs], callback |
允许你将多个键值对合并到一个方法中 |
9.3.1 AsyncStorage 的使用场景
AsyncStorage 常用于认证目的,持久化用户数据和信息,防止在应用关闭时丢失。例如,当用户登录并从 API 获取他们的名字、用户 ID、头像等信息时,你不想每次他们打开应用时都强制他们登录。当他们第一次登录时,你可以将他们的信息保存到 AsyncStorage 中,然后从那时起,使用原始信息,并在必要时更新它。
另一个用例是当你处理大量数据集或慢速 API 时,不想多次等待。例如,如果一个数据集需要几秒钟来检索,你可能想在 AsyncStorage 中缓存该数据,在用户打开应用时显示它,并在后台进程中刷新数据,这样用户就不必等待开始与数据或 UI 交互。
9.3.2 AsyncStorage 使用示例
在这个例子中,你将取一个用户对象并将其存储到 AsyncStorage 的componentDidMount中。然后,你将使用一个按钮从 AsyncStorage 中提取数据,用数据填充状态,并将其渲染到视图中。
列表 9.3 使用 AsyncStorage 持久化和检索数据
import React, { Component } from 'react'
import { TouchableHighlight, AsyncStorage, View,
Text, StyleSheet } from 'react-native' ①
let styles = {}
const person = { ②
name: 'James Garfield',
age: 50,
occupation: 'President of the United States'
}
const key = 'president' ③
export default class App extends Component {
constructor () {
super()
this.state = {
person: {} ④
}
this.getPerson= this.getPerson.bind(this)
}
componentDidMount () {
AsyncStorage.setItem(key, JSON.stringify(person)) ⑤
.then(() => console.log('item stored...'))
.catch((err) => console.log('err: ', err))
}
getPerson () { ⑥
AsyncStorage.getItem(key) ⑦
.then((res) => this.setState({ person: JSON.parse(res) })) ⑧
.catch((err) => console.log('err: ', err))
}
render () {
const { person } = this.state
return (
<View style={styles.container}>
<Text style={{textAlign: 'center'}}>Testing AsyncStorage</Text>
<TouchableHighlight onPress={this.getPerson}
style={styles.button}> ⑨
<Text>Get President</Text>
</TouchableHighlight>
<Text>{person.name}</Text>
<Text>{person.age}</Text>
<Text>{person.occupation}</Text>
</View>
)
}
}
styles = StyleSheet.create({
container: {
justifyContent: 'center',
flex: 1,
margin: 20
},
button: {
justifyContent: 'center',
marginTop: 20,
marginBottom: 20,
alignItems: 'center',
height: 55,
backgroundColor: '#dddddd'
}
})
如你所见,promises 被用来设置和从 AsyncStorage 返回值。还有另一种方法来做这件事:让我们看看async await。
列表 9.4 使用 async await 异步获取数据
async componentDidMount () {
try {
await AsyncStorage.setItem(key, JSON.stringify(person))
console.log('item stored')
} catch (err) {
console.log('err:', err)
}
}
async getPerson () {
try {
var data = await AsyncStorage.getItem(key)
var person = await data
this.setState({ person: JSON.parse(person) })
} catch (err) {
console.log('err: ', err)
}
}
async await 首先需要您通过在函数名前添加 async 关键字来标记函数为异步。然后您可以使用 await 关键字等待函数返回的值,允许您像编写同步代码一样编写基于 promise 的代码。当您等待一个 promise 时,函数会等待直到 promise 解决,但它以非阻塞的方式进行;然后它将值赋给变量。
9.4 使用剪贴板 API 将文本复制到用户的剪贴板
剪贴板允许您在 iOS 和 Android 上保存和检索剪贴板内容。剪贴板有两个方法:getString() 和 setString()(见 表 9.3)。
表 9.3 剪贴板方法
| 方法 | 参数 | 描述 |
|---|---|---|
getString |
None | 获取剪贴板的内容 |
setString |
content |
设置剪贴板的内容 |
9.4.1 剪贴板的用例
剪贴板最常见的用例是当用户需要复制一段文本字符串时。用户不必记住它,可以使用剪贴板复制到剪贴板,然后将其粘贴到任何他们想要使用信息的地方!
9.4.2 使用剪贴板的示例
在此示例中,您将在 componentDidMount 中设置一个初始剪贴板值“Hello World”,然后使用附加到 TextInput 的方法更新剪贴板。您将添加一个按钮,将当前的 ClipboardValue 推送到一个数组并将其渲染到视图。
列表 9.5 保存和替换剪贴板内容
import React, { Component } from 'react'
import { TextInput, Clipboard, TouchableHighlight, View,
Text, StyleSheet } from 'react-native' ①
let styles = {}
export default class App extends Component {
constructor() {
super()
this.state = {
clipboardData: [] ②
}
this.pushClipboardToArray = this.pushClipboardToArray.bind(this)
}
componentDidMount () {
Clipboard.setString('Hello World! '); ③
}
updateClipboard (string) {
Clipboard.setString(string); ④
}
async pushClipboardToArray() { ⑤
const { clipboardData } = this.state
var content = await Clipboard.getString(); ⑥
clipboardData.push(content) ⑦
this.setState({clipboardData}) ⑧
}
render () {
const { clipboardData } = this.state
return (
<View style={styles.container}>
<Text style={{textAlign: 'center'}}>Testing Clipboard</Text>
<TextInput style={styles.input}
onChangeText={
(text) => this.updateClipboard(text)
} /> ⑨
<TouchableHighlight onPress={this.pushClipboardToArray}
style={styles.button}> ⑩
<Text>Click to Add to Array</Text>
</TouchableHighlight>
{
clipboardData.map((d, i) => { ⑪
return <Text key={i}>{d}</Text>
})
}
</View>
)
}
}
styles = StyleSheet.create({
container: {
justifyContent: 'center',
flex: 1,
margin: 20
},
input: {
padding: 10,
marginTop: 15,
height: 60,
backgroundColor: '#dddddd'
},
button: {
backgroundColor: '#dddddd',
justifyContent: 'center',
alignItems: 'center',
height: 60,
marginTop: 15,
}
})
9.5 使用维度 API 获取用户的屏幕信息
维度提供了一种获取设备屏幕高度和宽度的方法。这是一种根据屏幕尺寸进行计算的好方法。
9.5.1 维度 API 的用例
许多时候,您可能需要知道用户设备的精确尺寸,以便创建完美的 UI。在创建全局主题时,拥有宽度和高度来设置全局变量(如字体大小)是提供跨设备一致样式的好方法。使用设备宽度来创建一致的网格元素是另一种轻松创建一致体验的方法。总之:无论何时需要设备屏幕的高度和宽度,请使用维度。
9.5.2 使用维度 API 的示例
要使用维度,从 React Native 导入 API,然后调用 get() 方法,传入 window 或 screen 作为参数。返回 width、height 或两者。
列表 9.6 使用维度检索设备的宽度和高度
import React, { Component } from 'react'
import { View, Text, Dimensions, StyleSheet } from 'react-native' ①
let styles = {}
const { width, height } = Dimensions.get('window') ②
const windowWidth = Dimensions.get('window').width ③
const App = () => (
<View style={styles.container}> ④
<Text>{width}</Text>
<Text>{height}</Text>
<Text>{windowWidth}</Text>
</View>
)
styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center'
}
})
获取维度的一种方式是解构从调用 Dimensions.get 在窗口返回的内容,在这种情况下是 width 和 height。您还可以获取窗口的缩放比例。另一种方式是调用 Dimensions.get 并直接访问对象属性,在 Dimensions.get 上调用 .width。
9.6 使用 Geolocation API 获取用户当前位置信息
在 React Native 中,使用与浏览器中相同的 API 实现地理位置,navigator.geolocation 全局变量在应用程序的任何地方都可用。您不需要导入任何内容即可开始使用,因为它再次作为全局变量可用。
9.6.1 Geolocation API 的用例
如果您正在构建需要用户纬度和经度的应用程序,那么您将需要使用地理位置。react-native-maps,这是由 Airbnb 创建并开源的地图组件,是地理位置的一个很好的用例。很多时候,您希望地图加载到用户的当前位置;要做到这一点,您必须传入正确的坐标。使用地理位置来获取这些坐标。
9.6.2 使用 Geolocation 的示例
要开始使用地理位置,如果您为 Android 开发(iOS 默认启用),则必须启用其在应用程序中使用:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

图 9.2 地理位置返回的坐标对象
表 9.4 列出了可用的方法。
表 9.4 地理位置方法
| 方法 | 参数 | 描述 |
|---|---|---|
getCurrentPosition |
successcallback, errcallback, optionsobject{enableHighAccuracy: Boolean, timeout: number, maximumAge: number} |
获取当前位置。成功返回一个包含 coords 对象和时间的对象。 |
watchPosition |
successcallback, errcallback, optionsobject{enableHighAccuracy: Boolean, timeout: number, maximumAge: number} |
获取当前位置,并在设备位置改变时自动调用。 |
clearWatch |
watchId |
取消一个监视。在创建时将 watchPosition 方法存储在变量中以访问 watchId。 |
stopObserving |
无 | 取消所有已设置的地定位监视。 |
getCurrentPosition 和 watchPosition 返回一个包含有关当前用户位置信息的对象(见 图 9.2)。返回的信息不仅包含纬度和经度,还包括速度、海拔以及其他一些数据点。
要看到这个动作,您将设置一个 Geolocation 实例 getCurrentPosition 和 watchPosition。您还将有一个按钮来调用 clearWatch,这将清除由 watchPosition 调用启用的监视位置功能。
watchPosition 仅在您物理改变坐标时才会更改。例如,如果您在设备上运行此代码并四处走动,您应该会看到坐标更新。您可以通过调用 navigator.geolocation.clearWatch(id) 来在任何时候取消此监视,传入您想要取消的监视的 ID。然后您将显示原始坐标以及更新后的坐标(纬度和经度)。
列表 9.7 使用地理位置 API 获取用户坐标
import React, { Component } from 'react'
import { TouchableHighlight, View, Text, StyleSheet } from 'react-native'
let styles = {}
export default class App extends Component {
constructor () {
super()
this.state = { ①
originalCoords: {}, ①
updatedCoords: {}, ①
id: '' ①
}
this.clearWatch = this.clearWatch.bind(this)
}
componentDidMount() {
navigator.geolocation.getCurrentPosition( ②
(success) => {
this.setState({originalCoords: success.coords}) ③
},
(err) => console.log('err:', err)
)
let id = navigator.geolocation.watchPosition( ④
(success) => {
this.setState({ ⑤
id, ⑤
updatedCoords: success.coords
})
},
(err) => console.log('err:', err)
)
}
clearWatch () { ⑥
navigator.geolocation.clearWatch(this.state.id)
}
render () {
const { originalCoords, updatedCoords } = this.state
return (
<View style={styles.container}> ⑦
<Text>Original Coordinates</Text> ⑦
<Text>Latitude: {originalCoords.latitude}</Text> ⑦
<Text>Longitude: {originalCoords.longitude}</Text> ⑦
<Text>Updated Coordinates</Text> ⑦
<Text>Latitude: {updatedCoords.latitude}</Text> ⑦
<Text>Longitude: {updatedCoords.longitude}</Text> ⑦
<TouchableHighlight
onPress={this.clearWatch}
style={styles.button}>
<Text>Clear Watch</Text>
</TouchableHighlight>
</View>
)
}
}
styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
padding: 20,
},
button: {
height: 60,
marginTop: 15,
backgroundColor: '#ededed',
justifyContent: 'center',
alignItems: 'center'
}
})
9.7 使用 Keyboard API 控制原生键盘的位置和功能
Keyboard API 让您可以访问原生键盘。您可以使用它来监听键盘事件(并根据这些事件调用方法)或关闭键盘。键盘方法列在表 9.5 中。
表 9.5 键盘方法
| 方法 | 参数 | 描述 |
|---|---|---|
addListener |
event, callback |
将方法连接到基于原生键盘事件(如keyboardWillShow, keyboardDidShow, keyboardWillHide, keyboardDidHide, keyboardWillChangeFrame, 和 keyboardDidChangeFrame)调用的功能 |
removeAllListeners |
eventType |
移除指定类型的所有监听器 |
dismiss |
无 | 关闭键盘 |
9.7.1 Keyboard API 的使用场景
许多时候,文本输入和键盘的默认行为正是您想要的,但并非总是如此。如果您使用其他类型的组件模拟文本输入,键盘不会滑动。在这种情况下,您可以导入 Keyboard 并手动和细致地控制键盘何时显示和隐藏。
在某些情况下,即使文本输入处于焦点状态,您可能也想要手动关闭键盘。例如,如果 PIN 号码输入接受四个数字并在最后一个输入值上自动检查输入值是否正确,您可能希望提供一个在最后一个值输入后检索或检查的 UI。隐藏键盘可能是有意义的,您可以使用 Keyboard API 实现这一点。
9.7.2 使用 Keyboard API 的示例
在此示例中,您将设置一个文本输入并监听所有可用的事件。当事件被触发时,您将在控制台记录事件。您还将有两个按钮:一个用于关闭键盘,另一个用于移除在componentWillMount中设置的 所有事件监听器。
列表 9.8 使用 Keyboard API 控制设备键盘
import React, { Component } from 'react'
import { TouchableHighlight, Keyboard, TextInput, View,
Text, StyleSheet } from 'react-native' ①
let styles = {}
export default class App extends Component {
componentWillMount () { ②
this.keyboardWillShowListener =
Keyboard.addListener('keyboardWillShow',
() => this.logEvent('keyboardWillShow')) ②
this.keyboardDidShowListener =
Keyboard.addListener('keyboardDidShow',
() => this.logEvent('keyboardDidShow')) ②
this.keyboardWillHideListener =
Keyboard.addListener('keyboardWillHide',
() => this.logEvent('keyboardWillHide')) ②
this.keyboardDidHideListener =
Keyboard.addListener('keyboardDidHide',
() => this.logEvent('keyboardDidHide')) ②
this.keyboardWillChangeFrameListener =
Keyboard.addListener('keyboardWillChangeFrame',
() => this.logEvent('keyboardWillChangeFrame')) ②
this.keyboardDidChangeFrameListener =
Keyboard.addListener('keyboardDidChangeFrame',
() => this.logEvent('keyboardDidChangeFrame')) ②
}
logEvent(event) { ③
console.log('event: ', event) ③
}
dismissKeyboard () { ④
Keyboard.dismiss() ④
}
removeListeners () { ⑤
Keyboard.removeAllListeners('keyboardWillShow') ⑤
Keyboard.removeAllListeners('keyboardDidShow') ⑤
Keyboard.removeAllListeners('keyboardWillHide') ⑤
Keyboard.removeAllListeners('keyboardDidHide') ⑤
Keyboard.removeAllListeners('keyboardWillChangeFrame') ⑤
Keyboard.removeAllListeners('keyboardDidChangeFrame') ⑤
}
render () {
return (
<View style={styles.container}>
<TextInput style={styles.input} />
<TouchableHighlight ⑥
onPress={this.dismissKeyboard} ⑥
style={styles.button}> ⑥
<Text>Dismiss Keyboard</Text> ⑥
</TouchableHighlight> ⑥
<TouchableHighlight ⑦
onPress={this.removeListeners} ⑦
style={styles.button}> ⑦
<Text>Remove Listeners</Text> ⑦
</TouchableHighlight> ⑦
</View>
)
}
}
styles = StyleSheet.create({
container: {
flex: 1,
marginTop: 150,
},
input: {
margin: 10,
backgroundColor: '#ededed',
height: 50,
padding: 10
},
button: {
height: 50,
backgroundColor: '#dddddd',
margin: 10,
justifyContent: 'center',
alignItems: 'center'
}
})
9.8 使用 NetInfo 获取用户的当前在线/离线状态
NetInfo 是一个 API,允许您访问描述设备是否在线或离线的数据。要在 Android 上使用 NetInfo API,您需要在 AndroidManifest.xml 中添加所需的权限:
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
iOS 和 Android 有不同的连接类型,列在表 9.6 中。访问它们取决于用户连接的实际连接类型。为了确定连接类型,您可以使用表 9.7 中的方法。
表 9.6 跨平台和 Android 特定连接类型
| 跨平台(iOS 和 Android) | Android |
|---|---|
none |
bluetooth |
wifi |
ethernet |
cellular |
wimax |
unknown |
表 9.7 NetInfo 方法
| 方法 | 参数 | 描述 |
|---|---|---|
isConnectionExpensive |
无 | 返回一个 Promise,该 Promise 返回一个布尔值,指定连接是否昂贵 |
isConnected |
None | 返回一个指定设备是否连接的布尔值的 Promise |
addEventListener |
eventName, callback |
为指定的事件添加事件监听器 |
removeEventListener |
eventName, callback |
移除指定事件的监听器 |
getConnectionInfo |
None | 返回一个 Promise,该 Promise 返回一个包含类型和有效类型的对象。 |
9.8.1 NetInfo 的使用场景
NetInfo 通常用于防止其他 API 调用发生,或者提供一个离线 UI,该 UI 提供了一些但不是所有在线应用程序的功能。例如,假设你有一个项目列表,当按下时,会显示一个新视图,其中包含有关该项目的获取信息。当设备离线时,你可以显示应用程序离线的某些指示,并且不导航到项目详情。NetInfo 将为你提供此类设备信息,允许你以有用的方式与用户交互。
另一个用例是根据连接类型设置不同的 API 配置。例如,在 Wi-Fi 上,你可能希望允许请求和发送的数据量更慷慨:如果用户在蜂窝网络中,你可能一次只获取 10 个项目;但在 Wi-Fi 上,你将把这个数字提高到 20。使用 NetInfo,你可以确定用户是否有连接类型。
9.8.2 使用 NetInfo 的示例
让我们设置一个NetInfo.getConnectionInfo方法来获取初始连接信息。然后,你将设置一个监听器来记录当前的 NetInfo,如果它发生变化的话。
列表 9.9 使用 NetInfo 获取和显示用户连接类型
import React, { Component } from 'react'
import { NetInfo, View, Text, StyleSheet } from 'react-native' ①
class App extends Component {
constructor () {
super()
this.state = { ②
connectionInfo: {} ②
}
this.handleConnectivityChange =
this.handleConnectivityChange.bind(this)
}
componentDidMount () {
NetInfo.getConnectionInfo().then((connectionInfo) => { ③
console.log('type: ' + connectionInfo.type +
', effectiveType: ' + connectionInfo.effectiveType) ③
this.setState({connectionInfo}) ③
})
NetInfo.addEventListener('connectionChange',
this.handleConnectivityChange) ④
}
handleConnectivityChange (connectionInfo) { ⑤
console.log('new connection:', connectionInfo) ⑤
this.setState({connectionInfo}) ⑤
}
render () {
return (
<View style={styles.container}>
<Text>{this.state.connectionInfo.type}</Text> ⑥
</View>
)
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center'
}
})
9.9 使用 PanResponder 获取触摸和手势事件信息
PanResponder API 提供了一种使用触摸事件数据的方法。通过它,你可以根据单个和多个触摸事件(如滑动、点击、捏合、滚动等)精细地响应和操作应用程序状态。
9.9.1 PanResponder API 的使用场景
由于 PanResponder 的基本功能是确定用户设备上当前发生的触摸,因此使用场景是无限的。在我的经验中,我经常使用此 API 来完成以下类似的事情:
-
创建一个可滑动的卡片堆叠,当项目从视图中滑动出去时(想想 Tinder),从堆叠中移除该项目
-
创建一个可动画的覆盖层,用户可以通过点击按钮关闭它,或者通过向下滑动将其移出视图
-
通过按下列表项的一部分并将其移动到所需位置,使用户能够重新排列列表中的项目
PanResponder 的使用场景很多,但最明显和最常使用的是根据用户的按下/滑动位置在 UI 中移动项目。
让我们看看使用 onPanResponderMove(event, gestureState) 的基本手势事件,它提供了有关触摸事件当前位置的数据,包括当前位置、当前位置与原始位置之间的累积差异等:
onPanResponderMove(evt, gestureState) {
console.log(evt.nativeEvent)
console.log(gestureState)
}
要使用此 API,你首先在 componentWillMount 方法中创建 PanResponder 的一个实例。在这个实例中,你可以设置 PanResponder 的所有配置和回调方法,使用这些方法来操作状态和 View。
让我们看看 create 方法,这是 PanResponder 唯一可用的方法。它为 PanResponder 实例创建配置。表 9.8 展示了 create 方法可用的配置选项。
表 9.8 PanResponder create 方法的配置参数
| 配置属性 | 描述 |
|---|---|
onStartShouldSetPanResponder |
确定是否启用 PanResponder。在元素被触摸后调用。 |
onMoveShouldSetPanResponder |
确定是否启用 PanResponder。在初始触摸第一次移动后调用。 |
onPanResponderReject |
如果 PanResponder 没有注册,则调用。 |
onPanResponderGrant |
如果 PanResponder 注册,则调用。 |
onPanResponderStart |
在 PanResponder 注册后调用。 |
onPanResponderEnd |
在 PanResponder 完成后调用。 |
onPanResponderMove |
当 PanResponder 移动时调用。 |
onPanResponderTerminationRequest |
当其他东西想要成为响应者时调用。 |
onPanResponderRelease |
当触摸被释放时调用。 |
onPanResponderTerminate |
此响应者已被另一个响应者接管。 |
每个配置选项都提供了原生事件和手势状态。表 9.9 描述了 evt.nativeEvent 和 gestureState 的所有可用属性。
表 9.9 evt 和 gestureState 属性
| evt.nativeEvent 属性 | 描述 |
|---|---|
changedTouches |
自上次事件以来所有已更改的触摸事件的数组 |
identifier |
触摸的 ID |
locationX |
相对于元素的触摸 X 位置 |
locationY |
相对于元素的触摸 Y 位置 |
pageX |
相对于根元素的触摸 X 位置 |
pageY |
相对于根元素的触摸 Y 位置 |
target |
接收触摸事件的元素的节点 ID |
timestamp |
触摸的时间标识符;对于速度计算很有用 |
touches |
屏幕上所有当前触摸的数组 |
| gestureState 属性 | 描述 |
stateID |
gestureState 的 ID,只要屏幕上至少有一个触摸,就会持续存在 |
moveX |
最近移动的触摸的最新屏幕坐标 |
moveY |
最近移动的触摸的最新屏幕坐标 |
x0 |
响应者的屏幕坐标 |
y0 |
响应者的屏幕坐标 |
dx |
自触摸开始以来手势的累积距离 |
dy |
自触摸开始以来手势的累积距离 |
vx |
手势的当前速度 |
vy |
手势的当前速度 |
numberActiveTouches |
当前屏幕上的触摸次数 |
9.9.2 使用 PanResponder 的示例
在这个例子中,您将创建一个可拖动的正方形,并在视图中显示其 x 和 y 坐标。结果如图 9.3 所示。

图 9.3 用于使正方形可拖动的 PanResponder
列表 9.10 使用 PanResponder 创建可拖动元素
import React, { Component } from 'react'
import { Dimensions, TouchableHighlight, PanResponder, TextInput,
View, Text, StyleSheet } from 'react-native' ①
const { width, height } = Dimensions.get('window') ②
let styles = {}
class App extends Component {
constructor () {
super()
this.state = {
oPosition: { ③
x: (width / 2) - 100,
y: (height / 2) - 100,
},
position: { ④
x: (width / 2) - 100,
y: (height / 2) - 100,
},
}
this._handlePanResponderMove = this._handlePanResponderMove.bind(this)
this._handlePanResponderRelease =
this._handlePanResponderRelease.bind(this)
}
componentWillMount () {
this._panResponder = PanResponder.create({ ⑤
onStartShouldSetPanResponder: () => true, ⑤
onPanResponderMove: this._handlePanResponderMove, ⑤
onPanResponderRelease: this._handlePanResponderRelease ⑤
})
}
_handlePanResponderMove (evt, gestureState) { ⑥
let ydiff = gestureState.y0 - gestureState.moveY ⑥
let xdiff = gestureState.x0 - gestureState.moveX ⑥
this.setState({ ⑥
position: { ⑥
y: this.state.oPosition.y - ydiff, ⑥
x: this.state.oPosition.x - xdiff ⑥
}
})
}
_handlePanResponderRelease () { ⑦
this.setState({ ⑦
oPosition: this.state.position ⑦
})
}
render () {
return (
<View style={styles.container}>
<Text style={styles.positionDisplay}>
x: {this.state.position.x} y:{this.state.position.y}
</Text> ⑧
<View
{...this._panResponder.panHandlers} ⑨
style={[styles.box,
{ marginLeft: this.state.position.x,
marginTop: this.state.position.y } ]}
/> ⑩
</View>
)
}
}
styles = StyleSheet.create({
container: {
flex: 1,
},
positionDisplay: {
textAlign: 'center',
marginTop: 50,
zIndex: 1,
position: 'absolute',
width
},
box: {
position: 'absolute',
width: 200,
height: 200,
backgroundColor: 'red'
}
})
摘要
-
Alert 允许您在应用中提示或警告用户有关重要信息或事件。
-
AppState 提供了有关当前应用是否正在使用的相关信息。然后您可以在应用中以有用的方式使用这些信息。
-
AsyncStorage 允许您将数据持久化到用户的设备上,这样即使用户关闭应用,您仍然可以访问这些数据。
-
剪贴板将信息复制到用户的设备剪贴板,以便他们稍后访问。
-
Dimensions 提供了有关用户设备的有用信息,最重要的是屏幕宽度和高度。
-
地理定位提供了用户设备的位置以及其他重要信息,并允许您在用户移动时检查位置数据。
-
NetInfo 提供了用户的当前连接信息,包括连接类型以及他们是否当前连接。
-
PanResponder 提供了用户设备上发生的当前触摸位置。您可以使用这些信息来增强 UX 和 UI。
10
实现 iOS 特定组件和 API
本章涵盖
-
有效地定位平台特定代码的策略
-
使用选择器组件,
DatePickerIOS和PickerIOS -
使用
ProgressViewIOS显示加载进度 -
使用
SegmentedControlIOS和TabBarIOS选择视图 -
使用
ActionSheetIOS调用和选择动作表中的项目
React Native 项目的其中一个最终目标是拥有尽可能少的平台特定逻辑和代码。大多数 API 都可以构建,使得平台特定的代码通过框架抽象化,从而为您提供一种单一的方式来与之交互,并轻松创建跨平台功能。
不幸的是,总会有一些平台特定的 API 无法通过跨平台有意义的方案完全抽象化。因此,您将需要至少使用一些平台特定的 API 和组件。在本章中,我们将介绍 iOS 特定的 API 和组件,讨论它们的 props 和方法,并创建模拟功能性和逻辑的示例,以帮助您快速上手。
10.1 定位平台特定代码
平台特定代码的主要思想是以一种方式编写组件和文件,根据你所在的平台渲染 iOS 或 Android 特定的代码。有一些技术可以实现根据应用程序运行的平台显示组件,我们在这里介绍其中两种最有用的技术:使用正确的文件扩展名和使用 Platform API。
10.1.1 iOS 和 Android 文件扩展名
定位平台特定代码的第一种方法是使用正确的文件扩展名命名文件,这取决于你希望针对的平台。例如,iOS 和 Android 之间差异相当大的一个组件是 DatePicker。如果你想在 DatePicker 周围应用特定的样式,将所有代码都写在一个主组件中可能会变得冗长且难以维护。相反,你可以创建两个文件——DatePicker.ios.js 和 DatePicker.android.js——并将它们导入到主组件中。当你运行项目时,React Native 会自动根据你使用的平台选择正确的文件并渲染它。让我们看看列表 10.1、10.2 和 10.3 中的基本示例。(注意,这个示例会抛出错误——DatePicker 需要属性和方法才能正确运行。)
列表 10.1 iOS 平台特定代码
import React from 'react'
import { View, Text, DatePickerIOS } from 'react-native'
export default () => (
<View>
<Text>This is an iOS specific component</Text>
<DatePickerIOS />
</View>
)
列表 10.2 Android 平台特定代码
import React from 'react'
import { View, Text, DatePickerAndroid } from 'react-native'
export default () => (
<View>
<Text>This is an Android specific component</Text>
<DatePickerAndroid />
</View>
)
列表 10.3 渲染跨平台组件
import React from 'react'
import DatePicker from './DatePicker'
const MainComponent = () => (
<View>
...
<DatePicker />
...
</View>
)
你导入日期选择器时没有指定特定的文件扩展名。React Native 会根据平台知道要导入哪个组件。从那里,你可以在应用程序中使用它,而无需担心你所在的平台。
10.1.2 使用 Platform API 检测平台
另一种检测和基于平台执行逻辑的方法是使用 Platform API。Platform 有两个属性。第一个是一个 OS 键,它读取 ios 或 android,这取决于平台。
列表 10.4 使用 Platform.OS 属性检测平台
import React from 'react'
import { View, Text, Platform } from 'react-native'
const PlatformExample = () => (
<Text
style={{ marginTop: 100, color: Platform.OS === 'ios' ? 'blue' : 'green' }}
>
Hello { Platform.OS }
</Text>
)
在这里,你检查 Platform.OS 的值是否等于字符串 'ios',如果是,则返回 'blue' 颜色。如果不是,则返回 'green'。
Platform 的第二个属性是一个名为 select 的方法。select 接收一个包含 Platform.OS 字符串(ios 或 android)作为键的对象,并返回你正在运行的平台的值。
列表 10.5 使用 Platform.select 根据平台渲染组件
import React from 'react'
import { View, Text, Platform } from 'react-native'
const ComponentIOS = () => (
<Text>Hello from IOS</Text>
)
const ComponentAndroid = () => (
<Text>Hello from Android</Text>
)
const Component = Platform.select({
ios: () => ComponentIOS,
android: () => ComponentAndroid,
})();
const PlatformExample = () => (
<View style={{ marginTop: 100 }}>
<Text>Hello from my App</Text>
<Component />
</View>
)
你还可以使用 ES2015 扩展运算符语法来返回对象,并使用这些对象来应用样式。你可能记得在第四章的几个示例中使用了 Platform.select 函数。
列表 10.6 使用 Platform.select 根据平台应用样式
import React from 'react'
import { View, Text, Platform } from 'react-native'
let styles = {}
const PlatformExample = () => (
<View style={styles.container}>
<Text>
Hello { Platform.OS }
</Text>
</View>
)
styles = {
container: {
marginTop: 100,
...Platform.select({
ios: {
backgroundColor: 'red'
}
})
}
}
10.2 DatePickerIOS
DatePickerIOS 提供了一种在 iOS 上实现原生日期选择器组件的简单方法。它 有三种模式,在处理日期和时间时非常有用:date、time 和 dateTime,如图 10.1 所示。

图 10.1 DatePickerIOS 的 date 模式、time 模式和 datetime 模式
DatePickerIOS 有列在 表 10.1 中的属性。需要传递的最小属性是 date(作为开始或当前日期选择的日期)和一个 onDateChange 方法。当任何日期值改变时,onDateChange 被调用,传递函数新的日期值。
表 10.1 DatePickerIOS 属性与方法
| 属性 | 类型 | 描述 |
|---|---|---|
date |
日期 | 当前选择的日期 |
maximumDate |
日期 | 允许的最大日期 |
minimumDate |
日期 | 允许的最小日期 |
minuteInterval |
枚举 | 可选择的分钟间隔 |
mode |
字符串:date、time 或 datetime |
日期选择器模式 |
onDateChange |
函数:onDateChange(date) { } |
当日期改变时调用的函数 |
timeZoneOffsetInMinutes |
数字 | 时区偏移量(分钟);覆盖默认值(设备时区) |
10.2.1 使用 DatePickerIOS 的示例
在以下示例中,你将设置一个 DatePickerIOS 组件并在视图中显示时间。你不会传递模式属性,因为模式默认为 datetime。图 10.2 显示了结果。

图 10.2 DatePickerIOS 渲染选择的日期和时间
列表 10.7 使用 DatePicker 显示和更新时间值
import React, { Component } from 'react'
import { Text, View, DatePickerIOS } from 'react-native' ①
class App extends Component {
constructor() {
super()
this.state = { ②
date: new Date(), ②
}
this.onDateChange = this.onDateChange.bind(this)
}
onDateChange(date) { ③
this.setState({date: date}); ③
};
render() {
return (
<View style={{ marginTop: 50 }}>
<DatePickerIOS ④
date={this.state.date} ④
onDateChange={this.onDateChange} ④
/>
<Text style={{ marginTop: 40, textAlign: 'center' }}>
{ this.state.date.toLocaleDateString() } { this.state.date.toLocaleTimeString() } ⑤
</Text>
</View>)
}
}
10.3 使用 PickerIOS 处理值列表
使用 PickerIOS,你可以访问原生的 iOS Picker 组件。此组件基本上允许你使用原生 UI 滚动并通过列表选择值(见 图 10.3)。PickerIOS 有列在 表 10.2 中的方法和属性。
表 10.2 PickerIOS 方法与属性
| 属性 | 类型 | 描述 |
|---|---|---|
itemStyle |
对象(样式) | 容器内项目的文本样式 |
onValueChange |
函数(值) | 当 PickerIOS 的值改变时调用 |
selectedValue |
数字或字符串 | 当前选择的 PickerIOS 值 |

图 10.3 PickerIOS 渲染人员列表
PickerIOS 包裹要作为子元素渲染的项目列表。每个子元素必须是 PickerIOS.Item:
import { PickerIOS } from 'react-native'
const PickerItem = PickerIOS.Item
<PickerIOS>
<PickerItem />
<PickerItem />
<PickerItem />
</PickerIOS>
可以像这里一样单独声明每个 PickerIOS.Item,但大多数情况下,你将遍历数组中的元素,并为数组中的每个项目返回一个 PickerIOS.Item。以下列表显示了一个示例。
列表 10.8 使用 PickerIOS 与 PickerIOS.Item 数组
const people = [ #an array of people ];
render() {
<PickerIOS>
{
people.map((p, i) =>(
<PickerItem key={i} value={p} label={p}/>
))
}
<PickerIOS>
}
PickerIOS 和 PickerIOS.Item 接收它们自己的属性。对于 PickerIOS,主要的属性是 onValueChange 和 selectedValue。onValueChange 方法在每次选择器改变时被调用。selectedValue 是选择器在 UI 中显示为已选的值。
对于 PickerIOS.Item,主要的属性是 key、value 和 label。key 是一个唯一标识符,value 是将传递给 PickerIOS 组件的 onValueChange 方法的值,而 label 是在 UI 中作为 PickerIOS.Item 标签显示的内容。
10.3.1 使用 PickerIOS 的示例
在这个例子中,你将在 PickerIOS 中渲染人员数组。当值改变时,你将更新 UI 以显示新值。
列表 10.9 使用 PickerIOS 渲染人员数组
import React, { Component } from 'react'
import { Text, View, PickerIOS } from 'react-native' ①
const people = [ ②
{
name: 'Nader Dabit',
age: 36
},
{
name: 'Christina Jones',
age: 39
},
{
name: 'Amanda Nelson',
age: 22
}
];
const PickerItem = PickerIOS.Item
class App extends Component {
constructor() {
super()
this.state = { ③
value: 'Christina Jones' ③
} ③
this.onValueChange = this.onValueChange.bind(this) ③
}
onValueChange(value) { ④
this.setState({ value }); ④
};
render() {
return (
<View style={{ marginTop: 50 }}>
<PickerIOS ⑤
onValueChange={this.onValueChange} ⑤
selectedValue={this.state.value} ⑤
> ⑤
{
people.map((p, i) => { ⑥
return (
<PickerItem
key={i}
value={p.name}
label={p.name}
/>
)
})
}
</PickerIOS>
<Text style={{ marginTop: 40, textAlign: 'center' }}>
{this.state.value} ⑦
</Text>
</View>)
}
}
10.4 使用 ProgressViewIOS 显示加载指示器
ProgressViewIOS 允许你在 UI 中渲染原生的 UIProgressView。基本上,这是一种显示加载百分比指示、下载百分比指示或任何表示正在完成的任务的指示的原生方式(参见图 10.4)。它具有表 10.3 中显示的属性。

图 10.4 在 UI 中渲染 ProgressViewIOS
表 10.3 ProgressViewIOS 方法和属性
| 属性 | 类型 | 描述 |
|---|---|---|
progress |
数字 | 进度值(介于 0 和 1 之间) |
progressImage |
图片源 | 用于显示的进度条的可拉伸图片 |
progressTintColor |
字符串(颜色) | 进度条着色 |
progressViewStyle |
枚举(默认或条形) | 进度条样式 |
trackImage |
图片源 | 可拉伸的图片,用于显示在进度条后面 |
trackTintColor |
字符串 | 进度条轨道着色 |
10.4.1 ProgressViewIOS 的使用案例
ProgressViewIOS 最常见的用例是与外部 API 一起工作,该 API 告诉你当你在获取或提交数据或与本地 API 一起工作时,已经通过线缆传递了多少信息。例如,如果你正在将视频保存到用户的相机胶卷,你可以使用 ProgressViewIOS 来显示用户下载还需要多长时间以及已经完成了多少。
10.4.2 使用 ProgressViewIOS 的示例
创建此功能所需了解的主要属性是 progress。progress 接受介于 0 和 1 之间的数字,并将 ProgressViewIOS 填充到 0%到 100%的百分比填充。
在这个例子中,你将通过在 componentDidMount 中调用 setInterval 方法来模拟一些数据加载。你将每 0.01 秒增加状态值 0.01,直到达到 1,初始值为 0。
列表 10.10 使用 ProgressViewIOS 从 0%增加到 100%的进度条
import React, { Component } from 'react'
import { Text, View, ProgressViewIOS } from 'react-native' ①
class App extends Component {
constructor() {
super()
this.state = { ②
progress: 0, ②
}
}
componentDidMount() {
this.interval = setInterval(() => { ③
if (this.state.progress >= 1) { ③
return clearInterval(this.interval) ③
} ③
this.setState({ ③
progress: this.state.progress + .01 ③
}) ③
}, 10) ③
}
render() {
return (
<View style={{ marginTop: 50 }}>
<ProgressViewIOS ④
progress={this.state.progress} ④
/>
<Text style={{ marginTop: 10, textAlign: 'center' }}>
{Math.floor(this.state.progress * 100)}% complete ⑤
</Text>
</View>)
}
}
10.5 使用 SegmentedControlIOS 创建水平标签栏

图 10.5 基本的两个值(一个和两个)的 SegmentedControlIOS 实现
SegmentedControlIOS 允许你访问原生的 iOS UISegmentedControl 组件。它由单个按钮组成的水平标签栏,如图图 10.5 所示。
SegmentedControlIOS 的方法和属性在 表 10.4 中列出。至少,它需要一个值数组来渲染控制值,一个 selectedIndex 作为已选控制的索引,以及一个当控制被按下时将被调用的 onChange 方法。
表 10.4 SegmentedControlIOS 方法与属性
| 属性 | 类型 | 描述 |
|---|---|---|
enabled |
布尔值 | 如果为 false,则用户无法与控件交互。默认值为 true。 |
momentary |
布尔值 | 如果为 true,选择一个段不会在视觉上持续。onValueChange 仍将按预期工作。 |
onChange |
函数(event) | 当用户点击一个段时调用的回调;将事件作为参数传递。 |
onValueChange |
函数(value) | 当用户点击一个段时调用的回调;将段的值作为参数传递。 |
selectedIndex |
数字 | 要(预)选择的段的 props.values 中的索引。 |
tintColor |
字符串(颜色) | 控件的强调颜色。 |
values |
字符串数组 | 控制按钮的标签,按顺序排列。 |
10.5.1 SegmentedControlIOS 的用例
SegmentedControlIOS 是在 UI 中分离和显示某些可筛选/可排序数据的良好位置。例如,如果一个应用有按周列出和可查看的信息,你可以使用 SegmentedControlIOS 通过星期几进一步分离这些数据,为每一天提供一个单独的视图。
10.5.2 使用 SegmentedControlIOS 的示例
在此示例中,你将渲染一个包含三个项目的数组作为 SegmentedControlIOS。你还将根据所选项目在 UI 中显示一个值。
列表 10.11 SegmentedControlIOS 渲染三个值
import React, { Component } from 'react'
import { Text, View, SegmentedControlIOS } from 'react-native' ①
const values = ['One', 'Two', 'Three'] ②
class App extends Component {
constructor() {
super()
this.state = {
selectedIndex: 0, ③
}
}
render() {
const { selectedIndex } = this.state
let selectedItem = values[selectedIndex] ④
return (
<View style={{ marginTop: 40, padding: 20 }}>
<SegmentedControlIOS ⑤
values={values} ⑤
selectedIndex={this.state.selectedIndex} ⑤
onChange={(event) => { ⑤
this.setState({selectedIndex:
event.nativeEvent.selectedSegmentIndex}); ⑤
}}
/>
<Text>{selectedItem}</Text> ⑥
</View>)
}
}
10.6 使用 TabBarIOS 在 UI 底部渲染标签
TabBarIOS 允许你访问原生 iOS 标签栏。它将在 UI 底部渲染标签,如图 10.6 所示,为你提供一种优雅、简单的方式将应用程序分成几个部分。其方法和属性列在 表 10.5 中。
表 10.5 TabBarIOS 属性
| 属性 | 类型 | 描述 |
|---|---|---|
barTintColor |
字符串(颜色) | 标签栏的背景颜色。 |
itemPositioning |
枚举("fill", "center", "auto") |
标签栏项目定位。fill 将项目分布在整个标签栏宽度上。center 在可用的标签栏空间中居中项目。auto(默认)根据 UI 习惯动态分布项目;在水平紧凑环境中默认为 fill;否则默认为 center。 |
style |
对象(样式) | TabBarIOS 的样式。 |
tintColor |
字符串(颜色) | 当前选中标签图标的颜色。 |
translucent |
布尔值 | 表示标签栏是否为半透明。 |
unselectedItemTintColor |
字符串(颜色) | 未选择标签图标的颜色(自 iOS 10 以来可用)。 |
unselectedTintColor |
字符串(颜色) | 未选择标签上文本的颜色。 |
TabBarIOS 接受一个 TabBarIOS.Item 组件列表作为子组件:
const Item = TabBarIOS.Item
<TabBarIOS>
<Item>
<View> #some content here </View>
</Item>
<Item>
<View> #some other content here </View>
</Item>
</TabBarIOS>
要显示 TabBarIOS.Item 中的内容,TabBarIOS.Item 的 selected 属性必须是 true:

图 10.6 TabBarIOS 包含两个标签页:历史和收藏
<Item
selected={this.state.selectedComponent === 'home'}
>
#your content here
</Item>
10.6.1 TabBarIOS 的使用场景
TabBarIOS 的主要使用场景是导航。在移动设备上,很多时候,最好的导航类型是标签栏。将 UI 分离并在标签分隔的各个部分中显示内容是一种常见的模式,并且被鼓励,因为它提供了良好的用户体验。
10.6.2 使用 TabBarIOS 的示例
在此示例中,您将创建一个包含两个视图的应用程序:历史和收藏。当按下 TabBarIOS.Item 时,您将通过调用 onPress 方法来更新状态,在视图之间切换。
列表 10.12 使用 TabBarIOS 渲染标签
import React, { Component } from 'react'
import { Text, View, TabBarIOS } from 'react-native' ①
const Item = TabBarIOS.Item ②
class App extends Component {
constructor() {
super()
this.state = {
selectedTab: 'history', ③
}
this.renderView = this.renderView.bind(this)
}
renderView(tab) { ④
return (
<View style={{ flex: 1, justifyContent: 'center',
alignItems: 'center' }}>
<Text>Hello from {tab}</Text>
</View>
)
}
render() {
return (
<TabBarIOS> ⑤
<Item
systemIcon="history" ⑥
onPress={() => this.setState({ selectedTab: 'history' })} ⑦
selected={this.state.selectedTab === 'history'} ⑦
>
{this.renderView('History')} ⑧
</Item>
<Item
systemIcon='favorites'
onPress={() => this.setState({ selectedTab: 'favorites' })}
selected={this.state.selectedTab === 'favorites'}
>
{this.renderView('Favorites')}
</Item>
</TabBarIOS>
)
}
您可以使用系统图标或通过传递图标属性并要求本地图像来设置图标。有关所有系统图标的列表,请参阅 http://mng.bz/rYNJ。
10.7 使用 ActionSheetIOS 显示操作或分享表单
ActionSheetIOS 允许您访问原生 iOS UIAlertController 来显示原生 iOS 操作表或分享表单(见 图 10.7)。

图 10.7 ActionSheetIOS 渲染操作表(左侧)和分享表单(右侧)
您可以在 ActionSheetIOS 上调用的两个主要方法是 showActionSheetWithOptions 和 showShareActionSheetWithOptions;这些方法分别列在表 10.6 和 10.7 中。showActionSheetWithOptions 允许您传递一个按钮数组并将方法附加到每个按钮上。它使用两个参数调用:一个 options 对象和一个回调函数。showShareActionSheetWithOptions 显示原生 iOS 分享表单,传递要分享的 URL、消息和主题。它使用三个参数调用:一个 options 对象、一个失败回调函数和一个成功回调函数。
表 10.6 ActionSheetIOS 的 showActionSheetWithOptions 选项
| 选项 | 类型 | 描述 |
|---|---|---|
options |
字符串数组 | 按钮标题列表(必需) |
cancelButtonIndex |
整数 | options 中取消按钮的索引 |
destructiveButtonIndex |
整数 | options 中破坏性按钮的索引 |
title |
字符串 | 在操作表上方显示的标题 |
message |
字符串 | 在标题下方显示的消息 |
表 10.7 ActionSheetIOS 的 showShareActionSheetWithOptions 选项
| 选项 | 类型 | 描述 |
|---|---|---|
url |
字符串 | 要分享的 URL |
message |
字符串 | 要分享的消息 |
subject |
字符串 | 消息的主题 |
excludedActivityTypes |
数组 | 要排除在操作表中的活动 |
10.7.1 ActionSheetIOS 的使用场景
ActionSheetIOS 的主要用途是为用户提供一组选项进行选择,并根据他们的选择调用一个函数。例如,在 Twitter 应用中,当按下重发按钮时,操作表被用来提供用户几个选项,包括重发、引用重发和取消。这是一个常见的用例,在用户按下按钮后显示操作表,并给用户一组选项进行选择。
10.7.2 使用 ActionSheetIOS 的示例
在此示例中,你将创建一个包含两个按钮的视图。一个按钮将调用 showActionSheetWithOptions,另一个将调用 showShareActionSheetWithOptions。
列表 10.13 使用 ActionSheetIOS 创建操作表和分享表
import React, { Component } from 'react'
import { Text, View, ActionSheetIOS,
TouchableHighlight } from 'react-native' ①
const BUTTONS = ['Cancel', 'Button One', 'Button Two', 'Button Three'] ②
class App extends Component {
constructor() {
super()
this.state = { ③
clicked: null ③
}
this.showActionSheet = this.showActionSheet.bind(this)
this.showShareActionSheetWithOptions =
this.showShareActionSheetWithOptions.bind(this)
}
showActionSheet() { ④
ActionSheetIOS.showActionSheetWithOptions({
options: BUTTONS,
cancelButtonIndex: 0,
},
(buttonIndex) => {
if (buttonIndex > 0) {
this.setState({ clicked: BUTTONS[buttonIndex] });
}
});
}
showShareActionSheetWithOptions() { ⑤
ActionSheetIOS.showShareActionSheetWithOptions({
url: 'http://www.reactnative.training',
message: 'React Native Training',
},
(error) => console.log('error:', error),
(success, method) => { ⑥
if (success) {
console.log('successfully shared!', success)
}
});
};
render() {
return (
<View style={styles.container}> ⑦
<TouchableHighlight onPress={this.showActionSheet}
style={styles.button}> ⑦
<Text style={styles.buttonText}> ⑦
Show ActionSheet ⑦
</Text> ⑦
</TouchableHighlight> ⑦
<TouchableHighlight onPress={this.showShareActionSheetWithOptions}
style={styles.button}> ⑦
<Text style={styles.buttonText}> ⑦
Show ActionSheet With Options ⑦
</Text> ⑦
</TouchableHighlight>
<Text>
{this.state.clicked}
</Text>
</View>
)
}
}
styles = {
container: {
flex: 1,
justifyContent: 'center',
padding: 20,
},
button: {
height: 50,
marginBottom: 20,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'blue'
},
buttonText: {
color: 'white'
}
}
在 showActionSheet 方法中,你将按钮作为选项传入。将 cancelButtonIndex 设置为零将取消按钮放置在操作表的底部。回调方法接受按钮索引作为参数;如果按钮索引大于 0,则 clicked 状态值设置为新的按钮值。当你创建 showShareActionSheetWithOptions 方法时,你将 url 和要分享的 message 传入。第一个回调函数检查是否有错误,第二个检查成功是否为 true。
概述
-
要导入跨平台文件,请使用特定平台的 android.js 和 ios.js 文件扩展名。
-
要渲染特定平台的代码,请使用 Platform API。
-
使用
DatePickerIOS在你的应用中选择和保存日期。 -
使用
PickerIOS渲染和保存列表中的值。 -
使用
ProgressViewIOS显示加载进度。 -
使用
SegmentedControlIOS从选项数组中选择。 -
使用
TabBarIOS在你的应用中创建和切换标签页。 -
使用
ActionSheetIOS,你可以在应用中调用原生 iOS 的操作表或分享表。
11
实现 Android 特定组件和 API
本章涵盖
-
使用
DrawerLayoutAndroid创建侧边菜单 -
使用
ToolbarAndroid创建原生工具栏 -
使用
ViewPagerAndroid创建分页视图。 -
使用
DatePickerAndroid和TimePickerAndroid创建日期/时间选择器。 -
使用
ToastAndroid创建托盘通知
在本章中,我们将实现最常用的 Android 特定 API 和组件,讨论它们的属性和方法,并创建示例,这些示例将模仿功能性和逻辑,帮助你快速上手。为了了解这些功能是如何工作的,你将创建一个包含菜单、工具栏、可滚动分页、日期选择器和时间选择器的演示应用。该应用还将实现 Android 的托盘通知。在实现每个这些功能时,你将学习最常用的 Android 特定 API 和组件的能力。
11.1 使用 DrawerLayoutAndroid 创建菜单
要开始,你首先将创建一个滑动菜单(见图 11.1)。此菜单将链接到应用的每个功能部分。它基本上将作为在组件之间导航的方式。你将使用 DrawerLayoutAndroid 组件创建此菜单。

图 11.1 使用 DrawerLayoutAndroid 的应用程序初始布局。在第一个屏幕顶部的按钮“打开抽屉”将调用一个打开抽屉的方法。第二个屏幕是打开的抽屉。
首先要做的事情是创建一个新的 Android 应用程序。从你将要工作的文件夹中的命令行,创建一个新的应用程序,将以下命令中的 YourApplication 替换为你选择的任何应用程序名称:
react-native init YourApplication
接下来,创建你将用于创建所有这些功能的文件。在应用程序的根目录下,添加一个名为 app 的文件夹和四个文件:App.js、Home.js、Menu.js 和 Toolbar.js。
现在,你需要更新 index.android.js 以使用你的第一个特定于 Android 的组件,DrawerLayoutAndroid,这是一个从屏幕左侧滑动的工具栏。编辑 index.android.js 以包含并实现此组件。
列表 11.1 实现 DrawerLayoutAndroid 组件
import React from 'react'
import {
AppRegistry,
DrawerLayoutAndroid, ①
Button,
View
} from 'react-native'
import Menu from './app/Menu' ②
import App from './app/App' ③
class mycomponent extends React.Component {
constructor () {
super()
this.state = {
scene: 'Home' ④
}
this.jump = this.jump.bind(this)
this.openDrawer = this.openDrawer.bind(this)
}
openDrawer () {
this.drawer.openDrawer() ⑤
}
jump (scene) { ⑥
this.setState({ ⑥
scene ⑥
}) ⑥
this.drawer.closeDrawer() ⑥
}
render () {
return (
<DrawerLayoutAndroid ⑦
ref={drawer => this.drawer = drawer} ⑧
drawerWidth={300} ⑨
drawerPosition={DrawerLayoutAndroid.positions.Left} ⑩
renderNavigationView={() => <Menu onPress={this.jump} />}> ⑪
<View style={{ margin: 15 }}> ⑫
<Button onPress={() => this.openDrawer()} title='Open Drawer' />
</View>
<App ⑬
openDrawer={this.openDrawer}
jump={this.jump}
scene={this.state.scene} />
</DrawerLayoutAndroid>
)
}
}
AppRegistry.registerComponent('mycomponent', () => mycomponent)
接下来,在 app/Menu.js 中创建你将在抽屉中使用的菜单。
列表 11.2 创建 DrawerLayoutAndroid 菜单组件
import React from 'react'
import { View, StyleSheet, Button } from 'react-native'
let styles
const Menu = ({onPress }) => {
const {
button
} = styles
return (
<View style={{ flex: 1 }}>
<View style={button} >
<Button onPress={() => onPress('Home')} title='Home' />
</View>
<View style={button} >
<Button onPress={() => onPress('Toolbar')} title='Toolbar Android' />
</View>
</View>
)
}
styles = StyleSheet.create({
button: {
margin: 10,
marginBottom: 0
}
})
export default Menu
现在,在 app/App.js 中,创建以下组件,它基本上接受一个 scene 作为属性并根据属性返回一个组件。
列表 11.3 创建 DrawerLayoutAndroid 应用组件
import React from 'react'
import Home from './Home' ①
import Toolbar from './Toolbar' ②
function getScene (scene) { ③
switch (scene) {
case 'Home':
return Home
case 'Toolbar':
return Toolbar
default:
return Home
}
}
const App = (props) => {
const Scene = getScene(props.scene) ④
return (
<Scene openDrawer={props.openDrawer} jump={props.jump} /> ⑤
)
}
export default App
现在你可以开始创建与菜单交互的组件。为了使当前设置正常工作,你需要创建一个 Home 组件和一个 Toolbar 组件。尽管你已经看到了导入,但你还没有真正创建这些组件。在 app/Home.js 中,创建以下组件,这是一个基本的介绍页面。
列表 11.4 创建 DrawerLayoutAndroid 主组件
import React, { Component } from 'react'
import {
View,
Text,
StyleSheet
} from 'react-native'
let styles
class Home extends Component {
render () {
return (
<View style={styles.container}>
<Text style={styles.text}>
Hello, this is an example application showing off some
android-specific APIs and Components!
</Text>
</View>
)
}
}
styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center'
},
text: {
margin: 20,
textAlign: 'center',
fontSize: 18
}
})
export default Home
在 app/Toolbar.js 中,创建以下组件,这将通过显示“来自工具栏的问候”消息来表明你处于工具栏中。
列表 11.5 创建 DrawerLayoutAndroid 工具栏组件
import React from 'react'
import {
View,
Text
} from 'react-native'
class ToolBar extends React.Component {
render () {
return (
<View style={{ flex: 1 }}>
<Text>Hello from Toolbar</Text>
</View>
)
}
}
export default ToolBar
启动应用程序,你应该看到图 11.1 所示的菜单。
11.2 使用 ToolbarAndroid 创建工具栏
一切设置完毕后,让我们添加一个新的组件,ToolbarAndroid。ToolbarAndroid 是一个 React Native 组件,它包装了原生的 Android 工具栏。此组件可以显示各种内容,包括标题、副标题、日志、导航图标和操作按钮。
在此示例中,你将使用标题、副标题和两个操作(选项和菜单;参见图 11.2)来实现 ToolbarAndroid。当点击菜单时,你将触发 openDrawer 方法,这将打开菜单。
在 app/Toolbar.js 中,按照以下代码更新以实现工具栏。
列表 11.6 实现 ToolbarAndroid
import React from 'react'
import {
ToolbarAndroid, ①
View
} from 'react-native'
class Toolbar extends React.Component {
render () {
const onActionSelected = (index) => { ②
if (index === 1) { ②
this.props.openDrawer() ②
}
}
return (
<View style={{ flex: 1 }}>
<ToolbarAndroid ③
subtitleColor='white'
titleColor='white'
style={{ height: 56, backgroundColor: '#52998c' }}
title='React Native in Action'
subtitle='ToolbarAndroid'
actions={[ { title: 'Options', show: 'always' },
{ title: 'Menu', show: 'always' } ]} ④
onActionSelected={onActionSelected} ⑤
/>
</View>
)
}
}
export default Toolbar
当你刷新你的设备时,你应该不仅看到 ToolbarAndroid,还应该能够通过按标有菜单的按钮来打开 DrawerLayoutAndroid 菜单。

图 11.2 带有标题、副标题和两个操作的 ToolbarAndroid。此菜单可配置,但在此示例中你只使用默认设置。
11.3 使用 ViewPagerAndroid 实现可滚动分页
接下来,你将创建一个新的示例页面和组件,使用 ViewPagerAndroid。此组件允许你轻松地在视图之间左右滑动。ViewPagerAndroid 的每个子项都被视为一个独立的、可滑动的视图(见 图 11.3)。

图 11.3 带有两个子视图的 ViewPagerAndroid。当你滑动页面时,它们左右滚动以显示下一页。
要开始,创建一个 app/ViewPager.js 文件并将 列表 11.7 中的代码添加进去以实现 ViewPagerAndroid 组件。
列表 11.7 使用 ViewPagerAndroid 实现可滚动分页视图
import React, { Component } from 'react'
import {
ViewPagerAndroid, ①
View,
Text
} from 'react-native'
let styles
class ViewPager extends Component {
render () {
const {
pageStyle,
page1Style,
page2Style,
textStyle
} = styles
return (
<ViewPagerAndroid ②
style={{ flex: 1 }}
initialPage={0}>
<View style={[ pageStyle, page1Style ]}>
<Text style={textStyle}>First page</Text>
</View>
<View style={[ pageStyle, page2Style ]}>
<Text style={textStyle}>Second page</Text>
</View>
</ViewPagerAndroid>
)
}
}
styles = {
pageStyle: {
justifyContent: 'center',
alignItems: 'center',
padding: 20,
flex: 1,
},
page1Style: {
backgroundColor: 'orange'
},
page2Style: {
backgroundColor: 'red'
},
textStyle: {
fontSize: 18,
color: 'white'
}
}
export default ViewPager
接下来,更新 Menu.js 以添加按钮以查看新组件。在 Menu.js 中,在 Toolbar Android 按钮下方添加此按钮:
<View style={button} >
<Button onPress={() => onPress('ViewPager')} title='ViewPager Android' />
</View>
最后,导入新组件并更新 App.js 中的 switch 语句以渲染组件。
列表 11.8 包含新 ViewPager 组件的 App.js
import React from 'react'
import Home from './Home'
import Toolbar from './Toolbar'
import ViewPager from './ViewPager'
function getScene (scene) {
switch (scene) {
case 'Home':
return Home
case 'Toolbar':
return Toolbar
case 'ViewPager':
return ViewPager
default:
return Home
}
}
const App = (props) => {
const Scene = getScene(props.scene)
return (
<Scene openDrawer={props.openDrawer} jump={props.jump} />
)
}
export default App
运行应用程序。你应该在侧菜单中看到新的 ViewPager Android 按钮,并且你可以查看和与之交互的新组件。
11.4 使用 DatePickerAndroid API 显示原生日期选择器
DatePickerAndroid 允许你打开并交互使用原生 Android 日期选择器对话框,如图 11.4 所示。要打开和使用 DatePickerAndroid 组件,导入 DatePickerAndroid 并调用 DatePickerAndroid.open()。要开始,创建 app/DatePicker.js 并在其中创建 DatePicker 组件(列表 11.9)。

图 11.4 带有打开日期选择器并显示选中日期的视图的按钮的 DatePickerAndroid
列表 11.9 实现 DatePicker 组件
import React, { Component } from 'react'
import { DatePickerAndroid, View, Text } from 'react-native' ①
let styles
class DatePicker extends Component {
constructor() {
super()
this.state = { ②
date: new Date()
}
this.openDatePicker = this.openDatePicker.bind(this)
}
openDatePicker () { ③
DatePickerAndroid.open({
date: this.state.date
})
.then((date) => {
const { year, month, day, action } = date ④
if (action === 'dateSetAction') { ⑤
this.setState({ date: new Date(year, month, day) })
}
}) }
render() {
const {
container,
text
} = styles
return (
<View style={container}> ⑥
<Text onPress={this.openDatePicker} style={text}>
Open Datepicker
</Text> ⑥
<Text style={text}>{this.state.date.toString()}</Text> ⑥
</View> ⑥
)
}
}
styles = {
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center'
},
text: {
marginBottom: 15,
fontSize: 20
}
}
export default DatePicker
现在你有了这个组件,更新 app/App.js 以包含它。
列表 11.10 包含新 DatePicker 组件的 app/App.js
import React from 'react'
import Home from './Home'
import Toolbar from './Toolbar'
import ViewPager from './ViewPager'
import DatePicker from './DatePicker'
function getScene (scene) {
switch (scene) {
case 'Home':
return Home
case 'Toolbar':
return Toolbar
case 'ViewPager':
return ViewPager
case 'DatePicker':
return DatePicker
default:
return Home
}
}
const App = (props) => {
const Scene = getScene(props.scene)
return (
<Scene openDrawer={props.openDrawer} jump={props.jump} />
)
}
export default App
最后,更新菜单以添加将打开 DatePicker 组件的新按钮。在 app/Menu.js 中,在 ViewPager Android 按钮下方添加以下按钮:
<View style={button} >
<Button onPress={() => onPress('DatePicker')} title='DatePicker Android' />
</View>
11.5 使用 TimePickerAndroid 创建时间选择器
接下来是 TimePickerAndroid。它与 DatePickerAndroid 类似,即你导入它并调用 open 方法来与之交互。此组件弹出一个时间选择对话框,允许你选择时间并在你的应用程序中使用它(图 11.5)。
为了标准化时间格式,你将使用一个名为 moment.js 的第三方库。要开始使用此库,你必须首先安装它。在项目的根目录中,使用 npm 或 yarn(根据你的喜好——npm 和 yarn 在这里都将完全相同)安装 moment:
npm install moment –save
或者
yarn add moment

图 11.5 TimePickerAndroid 同时显示小时和分钟视图
在 app/TimePicker.js 中,创建以下 TimePicker 组件。
列表 11.11 使用 moment.js 的 TimePickerAndroid
import React, { Component } from 'react'
import { TimePickerAndroid, View, Text } from 'react-native' ①
import moment from 'moment' ②
let styles
class TimePicker extends Component {
constructor () {
super()
this.state = {
time: moment().format('h:mm a') ③
}
this.openTimePicker = this.openTimePicker.bind(this)
}
openTimePicker () { ④
TimePickerAndroid.open({ ⑤
time: this.state.time
})
.then((time) => {
const { hour, minute, action } = time ⑥
if (action === 'timeSetAction') {
const time = moment().minute(minute).hour(hour).format('h:mm a')
this.setState({ time })
}
})
}
render () {
const {
container,
text
} = styles
return (
<View style={container}> ⑦
<Text onPress={this.openTimePicker} style={text}>Open Time Picker</Text>
<Text style={text}>{this.state.time.toString()}</Text>
</View>
)
}
}
styles = {
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center'
},
text: {
marginBottom: 15,
fontSize: 20
}
}
export default TimePicker
接下来,更新 app/App.js 以包含新组件。
列表 11.12 添加 TimePicker 组件到 app/App.js
import React from 'react'
import Home from './Home'
import Toolbar from './Toolbar'
import ViewPager from './ViewPager'
import DatePicker from './DatePicker'
import TimePicker from './TimePicker'
function getScene (scene) {
switch (scene) {
case 'Home':
return Home
case 'Toolbar':
return Toolbar
case 'ViewPager':
return ViewPager
case 'DatePicker':
return DatePicker
case 'TimePicker':
return TimePicker
default:
return Home
}
}
const App = (props) => {
const Scene = getScene(props.scene)
return (
<Scene openDrawer={props.openDrawer} jump={props.jump} />
)
}
export default App
最后,更新菜单以添加将打开新 TimePicker 组件的按钮。在 app/Menu.js 中,在 DatePicker Android 按钮下方添加以下按钮:
<View style={button} >
<Button onPress={() => onPress('TimePicker')} title='TimePicker Android' />
</View>
11.6 使用 ToastAndroid 实现 Android toasts
ToastAndroid 允许你从 React Native 应用程序中轻松调用原生 Android toasts。Android toast 是一个带有消息的弹出窗口,在给定的时间后消失(见图 11.6)。要开始构建此组件,创建 app/Toast.js,如下所示。
列表 11.13 实现 ToastAndroid
import React from 'react'
import { View, Text, ToastAndroid } from 'react-native' ①
let styles
const Toast = () => {
let {
container,
button
} = styles
const basicToast = () => { ②
ToastAndroid.show('Hello World!', ToastAndroid.LONG) ②
}
const gravityToast = () => { ③
ToastAndroid.showWithGravity('Toast with Gravity!',
ToastAndroid.LONG, ToastAndroid.CENTER)
}
return (
<View style={container}> ④
<Text style={button} onPress={basicToast}> ⑤
Open basic toast
</Text>
<Text style={button} onPress={gravityToast}> ⑥
Open gravity toast
</Text>
</View>
)
}
styles = {
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center'
},
button: {
marginBottom: 10,
color: 'blue'
}
}
export default Toast

图 11.6 ToastAndroid 在默认和中间位置显示 toasts
ToastAndroid.show() 方法接受两个参数:一个消息和一个显示 toast 的时间长度。时间可以是 SHORT(大约 2 秒)或 LONG(大约 4 秒);此示例使用 LONG。ToastAndroid.showWithGravity() 方法类似于 ToastAndroid.show(),但你可以传递第三个参数来将 toast 定位到视图的顶部、底部或中心。在这种情况下,你使用 ToastAndroid.CENTER 作为第三个参数将 toast 定位到屏幕中间。
现在,更新 app/App.js 以包含新组件。
列表 11.14 向应用程序添加 toast 组件
import React from 'react'
import Home from './Home'
import Toolbar from './Toolbar'
import ViewPager from './ViewPager'
import DatePicker from './DatePicker'
import TimePicker from './TimePicker'
import Toast from './Toast'
function getScene (scene) {
switch (scene) {
case 'Home':
return Home
case 'Toolbar':
return Toolbar
case 'ViewPager':
return ViewPager
case 'DatePicker':
return DatePicker
case 'TimePicker':
return TimePicker
case 'Toast':
return Toast
default:
return Home
}
}
const App = (props) => {
const Scene = getScene(props.scene)
return (
<Scene openDrawer={props.openDrawer} jump={props.jump} />
)
}
export default App
最后,更新菜单以添加将打开 toast 组件的新按钮。在 app/Menu.js 中,在 TimePicker Android 按钮下方添加以下按钮:
<View style={button} >
<Button onPress={() => onPress('Toast')} title='Toast Android' />
</View>
摘要
-
你可以使用
DrawerLayoutAndroid创建应用程序的主菜单。 -
你可以使用
ToolbarAndroid创建一个交互式应用程序工具栏。 -
你可以使用
ViewPagerAndroid创建可滑动视图。 -
使用
DatePickerAndroid,你可以访问原生日期选择器,允许你在应用程序中创建和操作日期。 -
TimePickerAndroid允许你访问原生时间选择器,使得在应用程序中创建和操作时间成为可能。 -
你可以使用
ToastAndroid轻松创建原生 Android toast 通知。
第四部分
将所有内容整合在一起
本书这一部分将前几章中涵盖的所有内容——样式、导航、动画以及一些跨平台组件——整合到一个应用程序中。我们将首先查看最终设计,并简要概述应用程序将执行的操作。
你将创建一个新的 React Native 应用程序并安装 React Navigation 库,深入探讨组件以及导航 UI 的样式,通过使用 Fetch API 处理外部网络资源的数据,并最终构建一个允许用户查看他们最喜欢的星球大战角色信息的应用程序。
12
使用跨平台组件构建星球大战应用
本章涵盖
-
使用 Fetch API 获取数据的基本方法
-
使用
Modal组件显示和隐藏视图 -
使用
FlatList组件创建列表 -
使用
ActivityIndicator显示加载状态 -
在实际项目中使用 React Navigation 处理导航
React Native 附带了许多可在应用程序中使用的组件。其中一些组件是跨平台的:也就是说,无论你在 iOS 还是 Android 上运行应用程序,它们都能工作。其他组件是平台特定的:例如,ActionSheetIOS仅在 iOS 上运行,而ToolbarAndroid仅在 Android 平台上运行(跨平台组件在第十章和第十一章中已介绍)。
本章涵盖了构建演示应用程序时使用的一些最常用的跨平台组件及其实现方法。为此,你将通过构建一个跨平台的星球大战信息应用来实现以下跨平台组件和 API:
-
Fetch API
-
Modal -
ActivityIndicator -
FlatList -
Picker -
React-Navigation
此应用将访问 SWAPI,即《星球大战》API(swapi.co),并返回有关星球大战角色、飞船、家园星球等信息,如图 12.1 所示。当用户点击“人物”时,应用从swapi.co/api/people获取电影的主要演员阵容并显示其信息。在这个过程中,应用使用了几个 React Native 跨平台组件。在本章中,你将学习如何在以下操作中使用这些组件:
-
设置一个新的 React Native 应用程序并安装依赖项
-
导入
People组件并创建Container组件 -
创建
Navigation组件并注册路由 -
创建视图的主要类
-
创建
People组件 -
使用跨平台组件
FlatList、Modal和Picker创建状态并设置获取数据的 fetch 调用

图 12.1 你将使用 React Native 跨平台组件构建的完成的星球大战应用程序。你将关注第一个链接:人物。
12.1 创建应用程序并安装依赖项
你需要做的第一件事是设置一个新的 React Native 应用程序并安装构建此应用程序所需的任何依赖项。转到命令行,并输入以下内容创建一个 React Native 应用程序:
react-native init StarWarsApp
接下来,切换到新创建的 StarWarsApp 目录:
cd StarWarsApp
你需要为这个应用程序安装的唯一东西是 react-navigation,所以你可以使用 npm 或 yarn 安装它:
-
使用 npm:
npm i react-navigation -
使用 yarn:
yarn add react-navigation
现在项目已创建,打开 App.js 并创建 图 12.2 中显示的屏幕所需的组件。在文件顶部,导入以下组件。
列表 12.1 导入初始组件
import React, { Component } from 'react';
import {
StyleSheet,
Text,
FlatList,
TouchableHighlight
} from 'react-native';
import { createStackNavigator } from 'react-navigation';
在这个列表中,你导入了所需的 React Native 组件,以及来自 react-navigation 的 createStackNavigator。FlatList 是一个组件,它允许你使用任何数据数组在应用程序中渲染性能列表。createStackNavigator 是来自 react-navigation 的一个导航器,它提供了一个在场景之间导航的简单方法;每个场景都推送到路由堆栈的顶部。所有动画都已为你配置好,并提供了默认的 iOS 和 Android 感觉和过渡效果。
12.1.1 导入 People 组件并创建 Container 组件

图 12.2 应用程序的初始视图
接下来,你需要导入你将在应用程序中使用的两个视图。再次查看 图 12.2 中的第一个屏幕。正如你所见,有 People、Films 等链接。当用户点击 People 时,应用程序应该导航到一个列出 星球大战 电影中人物(主要角色)的组件。为此,你将在 12.2 节中创建一个 People 组件——你现在将导入该组件,稍后创建它。在 列表 12.1 的最后一个 import 下方,导入尚未创建的 People 组件:
import People from './People'
由于设计使用黑色背景,并且你不想在组件之间重复样式代码,让我们创建一个 Container 组件,你将使用它作为视图的包装器。这个 Container 组件将仅用于样式。在应用程序的根目录下创建一个名为 Container.js 的新文件,并输入以下代码。
列表 12.2 创建可重用的 Container 组件
import React from 'react'
import { StyleSheet, View } from 'react-native'
const styles = StyleSheet.create({ ①
container: {
flex: 1,
backgroundColor: 'black',
},
})
const Container = ({ children }) => ( ②
<View style={styles.container}> ③
{children} ③
</View> ③
)
export default Container
将 Container 导入到 App.js 文件中,在 People 组件的最后一个 import 下方:
import Container from './Container'
在 Container 导入下方,创建一个数组,你将使用它来创建链接。数组中的项将被传递给 FlatList 组件以创建链接列表。此数组应包含对象,并且每个对象都应该包含一个 title 键。你需要 title 键来显示链接的名称:
const links = [
{ title: 'People' },
{ title: 'Films' },
{ title: 'StarShips' },
{ title: 'Vehicles' },
{ title: 'Species' },
{ title: 'Planets' }
]
12.1.2 创建导航组件并注册路由
在 App.js 文件的底部,您接下来将创建主导航组件并将其传递给 AppRegistry。您正在使用 createStackNavigator 作为导航组件,并且需要注册您将在应用程序中使用的路由。
初始化 createStackNavigator 并将导航器传递给 AppRegistry 方法,用导航组件替换默认的 StarWars 组件,如下所示。createStackNavigator 为应用提供在屏幕之间切换的方式:每个新的屏幕都放置在堆栈的顶部,并且是一个跨平台组件。
清单 12.3 使用 createStackNavigator
const App = `createStackNavigator`({ ①
StarWars: {
screen: StarWars ②
},
People: {
screen: People ③
}
})
export default App
12.1.3 创建初始视图的主类
在 App.js 文件中,在您在 12.1.1 节中创建的链接数组下方,添加视图的主类(清单 12.4)。这个类返回一个列表,将渲染从 API 返回的所有电影角色。您还将使用 navigationOptions 静态属性设置标题,并在头部设置标志。您将使用 React Native 的 FlatList 渲染这个列表。它是用于在 React Native 应用中渲染简单列表的内置界面。
清单 12.4 创建主 StarWars 组件
class StarWars extends Component {
static navigationOptions = { ①
①
headerTitle: <Text ①
style={{ ①
fontSize: 34, color: 'rgb(255,232,31)' ①
}} ①
>Star Wars</Text>, ①
headerStyle: { backgroundColor: "black", height: 110 } ①
}
navigate = (link) => { ②
const { navigate } = this.props.navigation
navigate(link)
}
renderItem = ({ item, index }) => { ③
return (
<TouchableHighlight
onPress={() => this.navigate(item.title)}
style={[ styles.item, { borderTopWidth: index === 0 ? 1 : null} ]}>
<Text style={styles.text}>{item.title}</Text>
</TouchableHighlight>
)
}
render() { ④
return ( ④
<Container> ④
<FlatList ④
data={links} ④
keyExtractor={(item) => item.title} ④
renderItem={this.renderItem} ④
/> ④
</Container> ④
)
}
}
const styles = StyleSheet.create({
item: {
padding: 20,
justifyContent: 'center',
borderColor: 'rgba(255,232,31, .2)',
borderBottomWidth: 1
},
text: {
color: '#ffe81f',
fontSize: 18
}
});

图 12.3 StarWars 组件的组件和标题
因为您正在使用来自 react-navigation 的 createStackNavigator,您可以传递每个路由的配置。在这个路由中,您想要更改默认的头部配置和样式。为此,您创建一个静态的 navigationOptions 对象,并在其中传递一个包含标题的 headerTitle 组件和一个包含一些特定样式的 headerStyle 对象。headerTitle 是您将用作标志的文本,而 headerStyle 将背景颜色设置为黑色,并设置一个固定高度以适应文本。
navigate 方法接收一个链接作为参数。由 StackNavigation 渲染的任何组件都接收导航对象作为属性。您使用此属性解构 navigate 方法,然后导航到传入的链接。在这种情况下,链接是链接数组中的 title 属性,与传递给 createStackNavigator 的键相关联。
FlatList 接收一个 renderItem 方法,该方法遍历作为 data 属性传入的数据数组,并为数组中的每个项目返回一个包含 item 和 index 的对象。item 是具有所有属性的真正项目,而 index 是项目的索引。您将这些作为参数解构,将 item 作为参数传递给 navigate 以显示标题,并使用 index 在它是第一个数组项时应用 borderTop 样式。
render()返回Container,在其中你包装FlatList,传入作为数据链接和之前创建的renderItem方法。你还传入一个keyExtractor方法。如果数组中没有标记为key的项目,你必须告诉FlatList使用哪个项目作为其键;否则它将抛出错误。图 12.3 显示了应用程序的初始视图及其组件。App.js 的最终代码在www.manning.com/books/react-native-in-action和 GitHub 上github.com/dabit3/react-native-in-action/blob/chapter12/StarWars/App.js。
12.2 使用 FlatList、Modal 和 Picker 创建 People 组件
接下来,你将创建一个People组件来获取并显示从星球大战 API (图 12.4) 获取的星球大战演员信息。作为此组件的一部分,你将使用 React Native 跨平台组件Modal和Picker。Modal允许你在当前正在工作的视图之上显示一个元素。Picker显示一个可滚动的选项或值列表;此组件提供了一个方便的方法来捕获用户的输入并使他们的选择对应用程序的其余部分可用。

图 12.4 此组件将显示 People.js 屏幕的Loading(左侧)和Loaded(中间)状态。它还将允许你查看每个角色的家乡信息(右侧)。
当People组件加载时,它将从一个空的数据数组开始,一个loading状态为true,以及一些其他的状态:
state = {
data: [],
loading: true,
modalVisible: false,
gender: 'all',
pickerVisible: false
}
当组件挂载时,你将从星球大战 API swapi.co/api/people 获取所需的数据;当这些数据返回时,你将使用返回的数据填充数据数组,并将loading布尔值设置为false。
你将使用modalVisible布尔值来显示和隐藏用于获取角色家乡信息的Modal组件。你将使用pickerVisible来显示和隐藏一个Picker组件,该组件将允许你选择你想要查看的人的性别,并将结果传递给一个过滤器,以便相应地过滤结果。
创建一个新的文件,People.js,并开始编码。
列表 12.5 People.js 导入
import React, { Component } from 'react'
import { ①
StyleSheet,
Text,
View,
Image,
TouchableHighlight,
ActivityIndicator,
FlatList,
Modal,
Picker
} from 'react-native'
import _ from 'lodash' ②
import Container from './Container' ③
import HomeWorld from './HomeWorld' ④
Lodash 实用库提供了许多便利函数。在导入之前,你需要通过 npm 或 yarn 安装它。
下一步是创建组件的主要类并设置navigationOptions以给标题添加标题以及一些样式。在 People.js 中的最后一个导入下面,创建以下People类。
列表 12.6 创建People类并设置页面标题
export default class People extends Component {
static navigationOptions = { ①
headerTitle: 'People',
headerStyle: { ②
borderBottomWidth: 1,
borderBottomColor: '#ffe81f',
backgroundColor: 'black'
},
headerTintColor: '#ffe81f',
pressColorAndroid: 'white' ③
}
}
在这里创建了静态的 navigationOptions 属性,就像在 App.js 文件中一样,但不是传递一个组件作为 headerTitle,而是传递字符串“People”。你还在其中添加了一些样式。
12.2.1 创建状态并设置一个用于检索数据的 fetch 调用
现在你将创建状态并在 componentDidMount 中设置一个 fetch 调用。Fetch 是一个跨平台的 API,用于获取网络资源,它正在取代 XMLHttpRequest。Fetch 还未与所有互联网浏览器完全兼容,但 React Native 提供了一个 polyfill(一个模拟原始 API 行为的 API,在这种情况下是 Fetch)。Fetch API 是一种易于使用的现成方式来处理网络请求,包括 GET、POST、PUT 和 DELETE。fetch 返回一个承诺,这使得异步处理变得容易。``
A `fetch` request usually looks something like this: ``` fetch('https://swapi.co/api/people/') .then(response => response.json()) .then(json => { #do something with the returned data / json }) .catch(err => { #handle error here }) ``` In the example, the `fetch` call will hit the Star Wars API at [`swapi.co/api/people`](https://swapi.co/api/people) and return an object containing a results array. This results array will contain the characters to display on this page. To view this dataset, open the URL in a browser to check out the data structure. The data set looks like the following, with results being the array of movie characters you’re interested in using: ``` { "count": 87, "next": "http://swapi.co/api/people/?page=2", "previous": null, "results": [ { "name": "Luke Skywalker", "height": "172", "mass": "77", ... }, ... } ``` Once the data is returned from the API, you update the data array in the state with the results. Below the `navigationOptions` object in People.js, create the state and the `componentDidMount` `fetch` call. Listing 12.7 Setting up the initial state and fetching data ``` state = { data: [], loading: true, modalVisible: false, gender: 'all', pickerVisible: false } componentDidMount() { fetch('https://swapi.co/api/people/') .then(res => res.json()) .then(json => this.setState({ data: json.results, loading: false })) .catch((err) => console.log('err:', err)) } ``` In `componentDidMount`, you fetch the data from the API using `fetch()`. `fetch` returns a promise. You then take the returned data and call the `.json()` method to read the response and transform the data. `.json()` returns a promise containing the JSON data. Finally, you set the state again, updating the data and loading variables. ### 12.2.2 Adding the remaining class methods At this point in the app, if you load this page, the data should be loaded into the state and ready to use. Next up, you need to create the rest of the functionality to display this data, as well as a `render` method to display the data. To create the rest of the methods used in this component, add the following code after `componentDidMount` in People.js. Listing 12.8 Remaining methods for component functionality ``` renderItem = ({ item }) => { ① return ( <View style={styles.itemContainer}> <Text style={styles.name}>{item.name}</Text> <Text style={styles.info}>Height: {item.height}</Text> <Text style={styles.info}>Birth Year: {item.birth_year}</Text> <Text style={styles.info}>Gender: {item.gender}</Text> <TouchableHighlight style={styles.button} onPress={() => this.openHomeWorld(item.homeworld)} > <Text style={styles.info}>View Homeworld</Text> </TouchableHighlight> </View> ) } openHomeWorld = (url) => { ② this.setState({ url, modalVisible: true }) } closeModal = () => { ③ this.setState({ modalVisible: false }) } togglePicker = () => { ④ this.setState({ pickerVisible: !this.state.pickerVisible }) } filter = (gender) => { ⑤ this.setState({ gender }) } ``` The `renderItem` method is what you’ll pass to `FlatList` to render the data in the state. Every time an item is passed through this method, you get an object with two keys: `item` and `key`. You destructure the item when the method is called and use the item properties to display the data for the user (`item.name`, `item.height`, and so on). Note the ``onPress method passed to the `TouchableHighlight` component: this method passes the `item.homeworld` property to the `openHomeWorld` method. `item.homeworld` is a URL you’ll use to fetch the movie character’s home planet information.`` ````The `togglePicker` method toggles the `pickerVisible` Boolean. This Boolean shows and hides a picker from which you can choose a filter to view characters by gender: all, female, male, or other (robots and so on). ### 12.2.3 Implementing the render method With all the methods set up, the last thing to do is implement the UI in the `render` method. In People.js, you’ll introduce a new component called the `ActivityIndicator`: a cross-platform circular loading indicator that will indicate the loading state (you can see a list of properties in table 12.1). After the `filter` method, add the render method as shown next. Listing 12.9 `render` method ``` render() { let { data } = this.state ① if (this.state.gender !== 'all') { ② data = data.filter(f => f.gender === this.state.gender) ② } return ( <Container> <TouchableHighlight style={styles.pickerToggleContainer} onPress={this.togglePicker}> ③ <Text style={styles.pickerToggle}> {this.state.pickerVisible ? 'Close Filter' : 'Open Filter'} </Text> ③ </TouchableHighlight> { ④ this.state.loading ? <ActivityIndicator color='#ffe81f' /> : ( ④ <FlatList ④ data={data} ④ keyExtractor={(item) => item.name} ④ renderItem={this.renderItem} ④ /> ④ ) ④ } <Modal ⑤ onRequestClose={() => console.log('onrequest close called')} ⑥ animationType="slide" ⑦ visible={this.state.modalVisible}> ⑤ <HomeWorld closeModal={this.closeModal} url={this.state.url} /> </Modal> ⑤ { this.state.pickerVisible && ( ⑧ <View style={styles.pickerContainer}> <Picker ⑨ style={{ backgroundColor: '#ffe81f' }} ⑨ selectedValue={this.state.gender} ⑨ onValueChange={(item) => this.filter(item)}> ⑨ <Picker.Item itemStyle={{ color: 'yellow' }} label="All" value="all" /> <Picker.Item label="Males" value="male" /> <Picker.Item label="Females" value="female" /> <Picker.Item label="Other" value="n/a" /> </Picker> </View> ) } </Container> ); } ``` When the Close Filter / Open Filter button is clicked, the `togglePicker` method is called and the picker is shown or hidden. The `onValueChange` method fires every time the picker value is updated, which then updates the state, triggering a rerender of the component, and updating the filtered list of items in the view. Table 12.1 `ActivityIndicator` properties | **Property** | **Type** | **Description (some from docs)** | | --- | --- | --- | | `animating` | Boolean | Animates the `ActivityIndicator` icon | | `color` | Color | Color of the `ActivityIndicator` | | `size` | String (small or large) | Size of the `ActivityIndicator` | The last thing you need is the styling for this component. This code goes below the class definition in People.js. Listing 12.10 `People` component styling ``` const styles = StyleSheet.create({ pickerToggleContainer: { padding: 25, justifyContent: 'center', alignItems: 'center' }, pickerToggle: { color: '#ffe81f' }, pickerContainer: { position: 'absolute', bottom: 0, right: 0, left: 0 }, itemContainer: { padding: 15, borderBottomWidth: 1, borderBottomColor: '#ffe81f' }, name: { color: '#ffe81f', fontSize: 18 }, info: { color: '#ffe81f', fontSize: 14, marginTop: 5 } }); ``` You can find the final code for this component at [www.manning.com/books/react-native-in-action](http://www.manning.com/books/react-native-in-action) and also on GitHub at [`github.com/dabit3/react-native-in-action/blob/chapter12/StarWars/People.js`](https://github.com/dabit3/react-native-in-action/blob/chapter12/StarWars/People.js). ## 12.3 Creating the HomeWorld component To finish the app, you’ll create the final component: `HomeWorld`. In People.js, you created a `Modal`, and this `HomeWorld` component was the `Modal`’s content: ``` <Modal onRequestClose={() => console.log('onrequest close called')} animationType="slide" visible={this.state.modalVisible}> <HomeWorld closeModal={this.closeModal} url={this.state.url} /> </Modal> ``` You’ll use the `HomeWorld` component to fetch data about a character’s home planet and display this information to the user in the modal, as shown in figure 12.5.  Figure 12.5 `HomeWorld` component displaying data after fetching from the API. The Close Modal button calls the `closeModal` function passed in as a prop. This component will fetch the `url` prop that’s passed in when the modal opens in a `fetch` call placed in `componentDidMount`. This happens because `componentDidMount` is called every time the `visible` property of the modal is set to `true`: it’s basically reloading the component when the modal is shown. ### 12.3.1 Creating the HomeWorld class and initializing state Create a new file: HomeWorld.js. Then, import the components you’ll need, create the class definition, and create the initial state, as shown next. Listing 12.11 `HomeWorld` component class, imports, and initial state ``` import React from 'react' import { View, Text, ActivityIndicator, StyleSheet, } from 'react-native' export default class HomeWorld extends React.Component { state = { ① data: {}, loading: true } } ``` The initial state holds only two things: an empty `data` object and a `loading` Boolean set to `true`. When the component loads, you’ll show a loading indicator while you wait for the data to come back from the API. Once the data loads, you’ll update the `loading` Boolean to `false` and render the data that came back from the API. ### 12.3.2 Fetching data from the API using the url prop You’ll call the API using the `url` property in `componentDidMount`, which will be called once the component loads. Below the state declaration in HomeWorld.js, create the following `componentDidMount` method. Listing 12.12 Fetching data and uploading state in `componentDidMount` ``` componentDidMount() { if (!this.props.url) return ① const url = this.props.url.replace(/^http:\/\//i, 'https://') ② fetch(url) ③ .then(res => res.json()) .then(json => { this.setState({ data: json, loading: false }) }) .catch((err) => console.log('err:', err)) } ``` You update the API URL to use HTTPS because React Native doesn’t allow unsecure HTTP requests out of the box (although it can be configured to work if necessary). You call `fetch` on the URL and, when the response comes back, transform the data into JSON, update the state to set `loading` to `false`, and add the data to the state by updating the `data` value of `state` with the returned JSON. Finally, you need to create the `render` method and the styling. In the `render` method, you’ll display some properties relating to the character’s home world, such as its name, population, climate, and so on. These styles will be repetitive. In React and React Native, it’s best to create and reuse a component rather than creating and reusing styling, if it’s something you’ll be doing more than a handful of times. In this case, it makes sense to create a custom `TextContainer` component to use in the `render` method to display data. Above the class declaration in HomeWorld.js, create the following `TextContainer` component. Listing 12.13 Creating a reusable `TextContainer` component ``` const TextContainer = ({ label, info }) => ( <Text style={styles.text}>{label}: {info}</Text> ) ``` In this component, you return a basic `Text` component and receive two props that you’ll use: `label` and `info`. The static `label` is the description of the field, and `info` is the information you get when the API returns the home world data. ### 12.3.3 Wrapping up the HomeWorld component Now that the `TextContainer` is ready to go, finish the component by creating the `render` method and the styling in HomeWorld.js. Listing 12.14 `render` method and styling ``` export default class HomeWorld extends React.Component { ... render() { const { data } = this.state ① return ( <View style={styles.container}> { this.state.loading ? ( ② <ActivityIndicator color='#ffe81f' /> ) : ( <View style={styles.HomeworldInfoContainer}> ③ <TextContainer label="Name" info={data.name} /> <TextContainer label="Population" info={data.population} /> <TextContainer label="Climate" info={data.climate} /> <TextContainer label="Gravity" info={data.gravity} /> <TextContainer label="Terrain" info={data.terrain} /> <TextContainer label="Diameter" info={data.diameter} /> <Text ④ style={styles.closeButton} ④ onPress={this.props.closeModal}> ④ Close Modal ④ </Text> ④ </View> ) } </View> ) } } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#000000', paddingTop: 20 }, HomeworldInfoContainer: { padding: 20 }, text: { color: '#ffe81f', }, closeButton: { paddingTop: 20, color: 'white', fontSize: 14 } }) ``` ## Summary * React Native ships with cross-platform components: components that work on both the iOS and Android platforms. * Use the `Modal` component to show overlays by setting the `visible` prop to `true` or `false`. * Use the `Picker` component to easily allow user selections. The `selectedValue` prop defines which value is selected. * Use the Fetch API to work with network requests and use the response data. `fetch` will return a promise with data you can use in the app. * The `FlatList` component lets you easily and efficiently render lists of data by passing in a `renderItem` method as well as a data array as props. * `ActivityIndicator` is a great, easy way to indicate a loading state in your app. An indicator is shown or hidden based on the loading state. * Create reusable containers by wrapping the `children` prop in two React Native `View` components.````
附录
安装和运行 React Native
为 iOS 设备开发
在撰写本文时,如果您想为 iOS 开发,您必须有一台 Mac,因为 Linux 和 Windows 不支持为 iOS 平台开发。
入门
要开始,您必须有一台 Mac,并且需要在上面安装以下内容:
-
Xcode
-
Node.js
-
Watchman
-
React Native 命令行界面
按照以下步骤操作:
-
安装 Xcode,它可通过 App Store 获取。
-
React Native 文档以及我都推荐通过 Homebrew 安装 Node 和 Watchman。如果您还没有安装 Homebrew,请访问
brew.sh并在您的机器上安装它。 -
打开命令行,使用 Homebrew 安装 Node 和 Watchman:
brew install node`brew install watchman` -
一旦安装了 Node.js,请通过命令行运行以下命令来安装 React Native 命令行工具:
`npm install –g react-native-cli`
如果您遇到权限错误,请尝试使用 sudo 再次运行:
sudo npm install -g react-native-cli
在 iOS 上测试安装
通过创建新项目来检查 React Native 是否已正确安装。在终端或您选择的命令行中,运行以下命令,将 MyProjectName 替换为项目名称:
react-native init *MyProjectName*
cd *MyProjectName*
现在您已经创建了项目并切换到了新目录,您可以通过几种不同的方式运行项目:
-
在 MyProjectName 目录内,运行命令
react-native run-ios``.。 -
通过打开位于 MyProjectName/ios/MyProjectName.xcodeproj 的 MyProjectName.xcodeproj 文件在 Xcode 中打开项目。
为 Android 设备开发
您可以使用 Mac、Linux 或 Windows 环境开发 React Native for Android。
Mac 和 Android
要在 Mac 上开始,您需要在您的机器上安装以下内容:
-
Node.js
-
React Native 命令行工具
-
Watchman
-
Android Studio
按照以下步骤操作:
-
React Native 文档以及我都推荐通过 Homebrew 安装 Node 和 Watchman。如果您还没有安装 Homebrew,请访问
brew.sh并在您的机器上安装它。 -
打开命令行,使用 Homebrew 安装 Node 和 Watchman:
brew install node brew install watchman -
一旦安装了 Node.js,请通过命令行运行以下命令来安装 React Native 命令行工具:
npm install -g react-native-cli -
在
developer.android.com/studio/install.html安装 Android Studio。
当所有内容都安装完毕后,转到 A.2.4 节以创建您的第一个项目。
Windows 和 Android
以下内容必须在您的机器上安装:
-
Node.js
-
Python2
-
React Native 命令行工具
-
Watchman
-
Android Studio
按照以下步骤操作:
-
Watchman 在 Windows 上处于 alpha 阶段,但根据我的经验,到目前为止运行良好。要安装 Watchman,请访问
github.com/facebook/watchman/issues/19并通过第一个评论中的链接下载 alpha 版本。 -
React Native 推荐通过 Chocolatey(Windows 的包管理器)安装 Node.js 和 Python2。为此,请安装 Chocolatey (
chocolatey.org),以管理员身份打开命令行,然后运行以下命令:choco install nodejs.install choco install python2 Install the React Native command-line interface: npm install –g react-native-cli -
从.下载并安装 Android Studio。
-
当所有安装完成后,前往 A.2.4 节创建您的第一个项目。
Linux 和 Android
以下必须在您的机器上安装:
-
Node.js
-
React Native 命令行工具
-
Watchman
-
Android Studio
按照以下步骤操作:
-
如果您尚未安装 Node.js,请访问
nodejs.org/en/download/package-manager并按照您 Linux 发行版的说明进行操作。 -
运行以下命令来安装 React Native 命令行工具:
npm install -g react-native-cli -
从
developer.android.com/studio/install.html下载并安装 Android Studio。 -
从.下载并安装 Watchman。
一切安装完成后,继续到下一节创建您的第一个项目。
创建新项目(Mac/Windows/Linux)
一旦您的开发环境设置完成并且安装了 react-native-cli,您就可以从命令行创建新的 React Native 项目。导航到您想要创建项目的文件夹,并输入以下命令,将MyProjectName替换为项目名称:
react-native init `*MyProjectName*`
运行项目(Mac/Windows/Linux)
要运行 React Native 项目,请在命令行中切换到项目目录,并运行以下 Android 命令:
react-native run-android


浙公网安备 33010602011771号