ReactNative 秘籍第二版(三)
原文:
zh.annas-archive.org/md5/12592741083b1cbc7e657e9f51045dce译者:飞龙
第七章:为您的应用程序添加高级动画
在本章中,我们将涵盖以下配方:
-
从列表组件中删除项目
-
创建 Facebook 反应小部件
-
在全屏显示图像
介绍
在上一章中,我们介绍了在 React Native 中使用两个主要动画助手Animated和LayoutAnimation的基础知识。在本章中,我们将通过构建更复杂的配方来进一步了解这些概念,展示常见的本地 UX 模式。
从列表组件中删除项目
在这个配方中,我们将学习如何在ListView中创建带有动画侧向滑动的列表项。如果用户将项目滑动超过阈值,项目将被移除。这是许多具有可编辑列表的移动应用程序中的常见模式。我们还将看到如何使用PanResponder来处理拖动事件。
准备就绪
我们需要创建一个空的应用程序。对于这个配方,我们将其命名为removing-list-items。
我们还需要创建一个新的ContactList文件夹,并在其中创建两个文件:index.js和ContactItem.js。
如何做...
- 让我们从导入主
App类的依赖项开始,如下所示:
import React from 'react';
import {
Text,
StyleSheet,
SafeAreaView,
} from 'react-native';
import ContactList from './ContactList';
- 这个组件将很简单。我们只需要渲染一个
toolbar和我们在上一步中导入的ContactList组件,如下所示:
const App = () => (
<SafeAreaView style={styles.main}>
<Text style={styles.toolbar}>Contacts</Text>
<ContactList style={styles.content} />
</SafeAreaView>
);
const styles = StyleSheet.create({
main: {
flex: 1,
},
toolbar: {
backgroundColor: '#2c3e50',
color: '#fff',
fontSize: 22,
padding: 20,
textAlign: 'center',
},
content: {
padding: 10,
flex: 1,
},
});
export default App;
- 这就是我们开始实际工作的全部内容。让我们打开
ContactList/index.js文件,并导入所有依赖项,如下所示:
import React, { Component } from 'react';
import {
ListView,
ScrollView,
} from 'react-native';
import ContactItem from './ContactItem';
- 然后我们需要定义一些数据。在真实的应用程序中,我们会从 API 中获取数据,但为了保持简单并且只关注拖动功能,让我们在这个相同的文件中定义数据:
const data = [
{ id: 1, name: 'Jon Snow' },
{ id: 2, name: 'Luke Skywalker' },
{ id: 3, name: 'Bilbo Baggins' },
{ id: 4, name: 'Bob Labla' },
{ id: 5, name: 'Mr. Magoo' },
];
- 这个组件的
state只包含两个属性:列表的数据和一个布尔值,在拖动开始或结束时将更新。如果您不熟悉ListView的工作原理,请查看第二章中的显示项目列表配方,创建一个简单的 React Native 应用程序。让我们定义数据如下:
export default class ContactList extends Component {
ds = new ListView.DataSource({
rowHasChanged: (r1, r2) => r1 !== r2
});
state = {
dataSource: this.ds.cloneWithRows(data),
swiping: false,
};
// Defined in later steps
}
render方法只需要显示列表。在renderScrollComponent属性中,我们将仅在用户不在列表上滑动项目时启用滚动。如果用户在滑动,我们希望禁用垂直滚动,如下所示:
render() {
const { dataSource, swiping } = this.state;
return (
<ListView
key={data}
enableEmptySections
dataSource={dataSource}
renderScrollComponent={
(props) => <ScrollView {...props} scrollEnabled={!swiping}/>
}
renderRow={this.renderItem}
/>
);
}
renderItem方法将返回列表中的每个项目。在这里,我们需要将联系信息作为属性发送,以及三个回调函数:
renderItem = (contact) => (
<ContactItem
contact={contact}
onRemove={this.handleRemoveContact}
onDragEnd={this.handleToggleSwipe}
onDragStart={this.handleToggleSwipe}
/>
);
- 我们需要切换
state对象上的 swiping 属性的值,这将切换列表上的垂直滚动是否被锁定:
handleToggleSwipe = () => {
this.setState({ swiping: !this.state.swiping });
}
- 在移除项目时,我们需要找到给定
contact的index,然后从原始列表中将其移除。之后,我们需要更新state上的dataSource,以使用生成的数据重新渲染列表:
handleRemoveContact = (contact) => {
const index = data.findIndex(
(item) => item.id === contact.id
);
data.splice(index, 1);
this.setState({
dataSource: this.ds.cloneWithRows(data),
});
}
- 列表已经完成,现在让我们专注于列表项。让我们打开
ContactList/ContactItem.js文件,并导入我们需要的依赖项:
import React, { Component } from 'react';
import {
Animated,
Easing,
PanResponder,
StyleSheet,
Text,
TouchableHighlight,
View,
} from 'react-native';
- 我们需要为这个组件定义
defaultProps。defaultProps对象将需要为从父级ListView元素传递给它的四个 props 中的每一个都提供一个空函数。当项目被按下时,onPress函数将被执行,当联系人被移除时,onRemove函数将被执行,而两个拖动函数将监听拖动事件。在state上,我们只需要定义一个动画值来保存拖动的 x 和 y 坐标,如下所示:
export default class ContactItem extends Component {
static defaultProps = {
onPress: () => {},
onRemove: () => {},
onDragEnd: () => {},
onDragStart: () => {},
};
state = {
pan: new Animated.ValueXY(),
};
- 当组件被创建时,我们需要配置
PanResponder。我们将在componentWillMount生命周期钩子中进行这个操作。PanResponder负责处理手势。它提供了一个简单的 API 来捕获用户手指生成的事件,如下所示:
componentWillMount() {
this.panResponder = PanResponder.create({
onMoveShouldSetPanResponderCapture: this.handleShouldDrag,
onPanResponderMove: Animated.event(
[null, { dx: this.state.pan.x }]
),
onPanResponderRelease: this.handleReleaseItem,
onPanResponderTerminate: this.handleReleaseItem,
});
}
- 现在让我们定义实际的函数,这些函数将在前一步中定义的每个回调中执行。我们可以从
handleShouldDrag方法开始,如下所示:
handleShouldDrag = (e, gesture) => {
const { dx } = gesture;
return Math.abs(dx) > 2;
}
handleReleaseItem有点复杂。我们将把这个方法分成两步。首先,我们需要弄清楚当前项目是否需要被移除。为了做到这一点,我们需要设置一个阈值。如果用户将元素滑动超出我们的阈值,我们将移除该项目,如下所示:
handleReleaseItem = (e, gesture) => {
const { onRemove, contact,onDragEnd } = this.props;
const move = this.rowWidth - Math.abs(gesture.dx);
let remove = false;
let config = { // Animation to origin position
toValue: { x: 0, y: 0 },
duration: 500,
};
if (move < this.threshold) {
remove = true;
if (gesture.dx > 0) {
config = { // Animation to the right
toValue: { x: this.rowWidth, y: 0 },
duration: 100,
};
} else {
config = { // Animation to the left
toValue: { x: -this.rowWidth, y: 0 },
duration: 100,
};
}
}
// Remainder in next step
}
- 一旦我们对动画进行了配置,我们就准备好移动项目了!首先,我们将执行
onDragEnd回调,如果项目应该被移除,我们将运行onRemove函数,如下所示:
handleReleaseItem = (e, gesture) => {
// Code from previous step
onDragEnd();
Animated.spring(
this.state.pan,
config,
).start(() => {
if (remove) {
onRemove(contact);
}
});
}
- 拖动系统已经完全就绪。现在我们需要定义
render方法。我们只需要在TouchableHighlight元素内显示联系人姓名,包裹在Animated.View中,如下所示:
render() {
const { contact, onPress } = this.props;
return (
<View style={styles.row} onLayout={this.setThreshold}>
<Animated.View
style={[styles.pan, this.state.pan.getLayout()]}
{...this.panResponder.panHandlers}
>
<TouchableHighlight
style={styles.info}
onPress={() => onPress(contact)}
underlayColor="#ecf0f1"
>
<Text>{contact.name}</Text>
</TouchableHighlight>
</Animated.View>
</View>
);
}
- 我们需要在这个类上再添加一个方法,这个方法是通过
View元素的onLayout属性在布局改变时触发的。setThreshold将获取row的当前width并设置threshold。在这种情况下,我们将其设置为屏幕宽度的三分之一。这些值是必需的,以决定是否移除该项,如下所示:
setThreshold = (event) => {
const { layout: { width } } = event.nativeEvent;
this.threshold = width / 3;
this.rowWidth = width;
}
- 最后,我们将为行添加一些样式,如下所示:
const styles = StyleSheet.create({
row: {
backgroundColor: '#ecf0f1',
borderBottomWidth: 1,
borderColor: '#ecf0f1',
flexDirection: 'row',
},
pan: {
flex: 1,
},
info: {
backgroundColor: '#fff',
paddingBottom: 20,
paddingLeft: 10,
paddingTop: 20,
},
});
- 最终的应用程序应该看起来像这个屏幕截图:
它是如何工作的...
在步骤 5中,我们在state上定义了swiping属性。这个属性只是一个布尔值,当拖动开始时设置为true,当完成时设置为false。我们需要这个信息来锁定列表在拖动项目时的垂直滚动。
在步骤 7中,我们定义了列表中每行的内容。onDragStart属性接收handleToggleSwipe方法,当拖动开始时将执行该方法。当拖动完成时,我们也将执行相同的方法。
在同一步骤中,我们还将handleRemoveContact方法发送给每个项目。顾名思义,当用户将其滑出时,我们将从列表中移除当前项目。
在步骤 11中,我们为项目组件定义了defaultProps和state。在过去的示例中,我们一直使用单个值来创建动画,但是在这种情况下,我们需要处理x和y坐标,所以我们需要一个Animated.ValueXY的实例。在内部,这个类处理两个Animated.Value实例,因此 API 几乎与我们之前看到的那些相同。
在步骤 12中,创建了PanResponder。React Native 中的手势系统,就像浏览器中的事件系统一样,在触摸事件时处理手势分为两个阶段:捕获和冒泡。在我们的情况下,我们需要使用捕获阶段来确定当前事件是按压项目还是尝试拖动它。onMoveShouldSetPanResponderCapture将捕获事件。然后,我们需要通过返回true或false来决定是否拖动该项。
onPanResponderMove属性将在每一帧从动画中获取值,这些值将被应用于state中的pan对象。我们需要使用Animated.event来访问每一帧的动画值。在这种情况下,我们只需要x值。稍后,我们将使用这个值来运行不同的动画,将元素返回到其原始位置或将其从屏幕上移除。
当用户释放物品时,onPanResponderRelease函数将被执行。如果由于任何其他原因,拖动被中断,将执行onPanResponderTerminate。
在步骤 13中,我们需要检查当前事件是简单的按压还是拖动。我们可以通过检查x轴上的增量来做到这一点。如果触摸事件移动了超过两个像素,那么用户正在尝试拖动物品,否则,他们正在尝试按下按钮。我们将差异评估为绝对数,因为移动可能是从左到右或从右到左,我们希望适应这两种移动。
在步骤 14中,我们需要获取物品相对于设备宽度移动的距离。如果这个距离低于我们在setThreshold中定义的阈值,那么我们需要移除这些物品。我们为每个动画定义了config对象,否则将返回物品到原始位置。但是,如果我们需要移除物品,我们会检查方向并相应地设置配置。
在步骤 16中,我们定义了 JSX。我们在Animated.View上设置我们想要动画的样式。在这种情况下,它是left属性,但是我们可以从我们在state.pan中存储的Animated.ValueXY实例中调用getLayout方法,而不是手动创建对象,该方法返回具有其现有值的 top 和 left 属性。
在同一步骤中,我们还通过展开this.panResponder.panHandlers来为Animated.View设置事件处理程序,使用展开运算符将我们在前面步骤中定义的拖动配置绑定到Animated.View。
我们还定义了对props中的onPress回调的调用,传入当前的contact信息。
另请参阅
您可以在以下网址找到PanResponder API 文档:
facebook.github.io/react-native/docs/panresponder.html
创建一个 Facebook 反应小部件
在这个食谱中,我们将创建一个模拟 Facebook 反应小部件的组件。我们将有一个喜欢按钮图像,当按下时,将显示五个图标。图标行将使用交错的滑入动画,同时从0增加到1的不透明度。
准备工作
让我们创建一个名为facebook-widget的空应用程序。
我们需要一些图片来显示一个假时间线。一些你的猫的照片就可以了,或者你可以使用 GitHub 上相应存储库中包含的猫的图片(github.com/warlyware/react-native-cookbook/tree/master/chapter-7/facebook-widget)。我们还需要五个图标来显示五种反应,比如,生气、笑、心、惊讶,这些也可以在相应的存储库中找到。
首先,我们将在空应用程序中创建两个 JavaScript 文件:Reactions/index.js和Reactions/Icon.js。我们需要将猫的图片复制到应用程序根目录下的images/文件夹中,反应图标应放置在Reactions/images中。
如何做...
- 我们将在
App类上创建一个假的 Facebook 时间线。让我们首先导入依赖项,如下所示:
import React from 'react';
import {
Dimensions,
Image,
Text,
ScrollView,
StyleSheet,
SafeAreaView,
} from 'react-native';
import Reactions from './Reactions';
- 我们需要导入一些图片来在我们的时间线中渲染。这一步中的 JSX 非常简单:只是一个
toolbar,一个带有两个Image和两个Reaction组件的ScrollView,如下所示:
const image1 = require('./images/01.jpg');
const image2 = require('./images/02.jpg');
const { width } = Dimensions.get('window');
const App = () => (
<SafeAreaView style={styles.main}>
<Text style={styles.toolbar}>Reactions</Text>
<ScrollView style={styles.content}>
<Image source={image1} style={styles.image} resizeMode="cover" />
<Reactions />
<Image source={image2} style={styles.image} resizeMode="cover" />
<Reactions />
</ScrollView>
</SafeAreaView>
);
export default App;
- 我们需要为这个组件添加一些基本的样式,如下所示:
const styles = StyleSheet.create({
main: {
flex: 1,
},
toolbar: {
backgroundColor: '#3498db',
color: '#fff',
fontSize: 22,
padding: 20,
textAlign: 'center',
},
content: {
flex: 1,
},
image: {
width,
height: 300,
},
});
- 我们准备开始工作在这个食谱的
Reactions组件。让我们首先导入依赖项,如下所示。我们将在后续步骤中构建导入的Icon组件:
import React, { Component } from 'react';
import {
Image,
Text,
TouchableOpacity,
StyleSheet,
View,
} from 'react-native';
import Icon from './Icon';
- 让我们定义
defaultProps和初始state。我们还需要要求like图标图片以在屏幕上显示它,如下所示:
const image = require('./images/like.png');
export default class Reactions extends Component {
static defaultProps = {
icons: [
'like', 'heart', 'angry', 'laughing', 'surprised',
],
};
state = {
show: false,
selected: '',
};
// Defined at later steps
}
- 让我们定义两种方法:一种是将
state的选定值设置为选定的reaction,另一种是切换state的show值以相应地显示或隐藏反应行,如下所示:
onSelectReaction = (reaction) => {
this.setState({
selected: reaction,
});
this.toggleReactions();
}
toggleReactions = () => {
this.setState({
show: !this.state.show,
});
};
- 我们将为此组件定义
render方法。我们将显示一张图片,当按下时,将调用我们之前定义的toggleReactions方法,如下所示:
render() {
const { style } = this.props;
const { selected } = this.state;
return (
<View style={[style, styles.container]}>
<TouchableOpacity onPress={this.toggleReactions}>
<Image source={image} style={styles.icon} />
</TouchableOpacity>
<Text>{selected}</Text>
{this.renderReactions()}
</View>
);
}
- 在这一步中,您会注意到我们正在调用
renderReactions方法。接下来,我们将渲染用户按下主反应按钮时要显示的所有图标,如下所示:
renderReactions() {
const { icons } = this.props;
if (this.state.show) {
return (
<View style={styles.reactions}>
{ icons.map((name, index) => (
<Icon
key={index}
name={name}
delay={index * 100}
index={index}
onPress={this.onSelectReaction}
/>
))
}
</View>
);
}
}
- 我们需要为这个组件设置
styles。我们将为反应图标图像设置大小并定义一些填充。reactions容器的高度将为0,因为图标将浮动,我们不希望添加任何额外的空间:
const styles = StyleSheet.create({
container: {
padding: 10,
},
icon: {
width: 30,
height: 30,
},
reactions: {
flexDirection: 'row',
height: 0,
},
});
Icon组件目前缺失,所以如果我们尝试在这一点上运行我们的应用程序,它将失败。让我们通过打开Reactions/Icon.js文件并添加组件的导入来构建这个组件,如下所示:
import React, { Component } from 'react';
import {
Animated,
Dimensions,
Easing,
Image,
StyleSheet,
TouchableOpacity,
View,
} from 'react-native';
- 让我们定义我们将要使用的图标。我们将使用一个对象来存储图标,这样我们可以通过键名轻松检索到每个图像,如下所示:
const icons = {
angry: require('./images/angry.png'),
heart: require('./images/heart.png'),
laughing: require('./images/laughing.png'),
like: require('./images/like.png'),
surprised: require('./images/surprised.png'),
};
- 现在我们应该为这个组件定义
defaultProps。我们不需要定义初始状态:
export default class Icon extends Component {
static defaultProps = {
delay: 0,
onPress: () => {},
};
}
- 图标应该通过动画出现在屏幕上,所以当组件挂载时,我们需要创建并运行动画,如下所示:
componentWillMount() {
this.animatedValue = new Animated.Value(0);
}
componentDidMount() {
const { delay } = this.props;
Animated.timing(
this.animatedValue,
{
toValue: 1,
duration: 200,
easing: Easing.elastic(1),
delay,
}
).start();
}
- 当图标被按下时,我们需要执行
onPress回调来通知父组件已选择了一个反应。我们将反应的名称作为参数发送,如下所示:
onPressIcon = () => {
const { onPress, name } = this.props;
onPress(name);
}
- 拼图的最后一块是
render方法,我们将在这个组件中定义 JSX,如下所示:
render() {
const { name, index, onPress } = this.props;
const left = index * 50;
const top = this.animatedValue.interpolate({
inputRange: [0, 1],
outputRange: [10, -95],
});
const opacity = this.animatedValue;
return (
<Animated.View
style={[
styles.icon,
{ top, left, opacity },
]}
>
<TouchableOpacity onPress={this.onPressIcon}>
<Image source={icons[name]} style={styles.image} />
</TouchableOpacity>
</Animated.View>
);
}
- 作为最后一步,我们将为每个
icon添加样式。我们需要图标浮动,所以我们将position设置为absolute,width和height设置为40像素。在这个改变之后,我们应该能够运行我们的应用程序:
icon: {
position: 'absolute',
},
image: {
width: 40,
height: 40,
},
});
- 最终的应用程序应该看起来像这个屏幕截图:
它是如何工作的...
在 步骤 2 中,我们在时间线中定义了 Reactions 组件。现在,我们不专注于处理数据,而是专注于显示用户界面。因此,我们不会通过 Reactions 属性发送任何回调来获取所选值。
在 步骤 5 中,我们定义了 defaultProps 和初始 state。
我们的状态中有两个属性:
-
show属性是一个布尔值。我们用它来在用户按下主按钮时切换反应图标。当为false时,我们隐藏反应,当为true时,我们运行动画来显示每个图标。 -
selected包含当前的选择。每当选择新的反应时,我们将更新这个属性。
在 步骤 8 中,我们渲染图标。在这里,我们需要将图标的名称发送到每个创建的实例。我们还为每个图标发送了 100 毫秒的 delay,这将创建一个漂亮的交错动画。onPress 属性接收了 步骤 6 中定义的 onSelectReaction 方法,该方法在 state 上设置了所选的反应。
在步骤 13中,我们创建了动画。首先,我们使用Animated.Value助手定义了animatedValue变量,正如在之前的配方中提到的那样,这是负责在动画中每一帧中保存值的类。组件一旦挂载,我们就运行动画。动画的进度从0到1,持续时间为 200 毫秒,使用弹性缓动函数,并根据接收到的delay属性延迟动画。
在步骤 15中,我们为Icon组件定义了 JSX。在这里,我们对top和opacity属性进行动画处理。对于top属性,我们需要从animatedValue中插值出值,以便图标从其原始位置向上移动 95 像素。opacity属性所需的值从0到1,由于我们不需要插值任何内容来完成这一点,因此我们可以直接使用animatedValue。
left值是根据index计算的:我们只是将图标向前一个图标的左侧移动 50 像素,这样可以避免将图标全部渲染在同一个位置。
在全屏显示图像
在这个配方中,我们将创建一个图像时间轴。当用户按下任何图像时,它将在黑色背景下全屏显示图像。
我们将为背景使用不透明度动画,并将图像从其原始位置滑入。
准备工作
让我们创建一个名为photo-viewer的空白应用程序。
此外,我们还将创建PostContainer/index.js来显示时间轴中的每个图像,以及PhotoViewer/index.js来在全屏显示所选图像。
您可以使用此处配方存储库中托管在 GitHub 上的图像(github.com/warlyware/react-native-cookbook/tree/master/chapter-7/photo-viewer)中包含的图像,也可以使用自己的一些照片。将它们放在项目根目录中的images文件夹中。
如何做...
- 我们将在
App类中显示一个带有图像的时间轴。让我们导入所有依赖项,包括我们稍后将构建的另外两个组件,如下所示:
import React, { Component } from 'react';
import {
Dimensions,
Image,
Text,
ScrollView,
StyleSheet,
SafeAreaView,
} from 'react-native';
import PostContainer from './PostContainer';
import PhotoViewer from './PhotoViewer';
- 在这一步中,我们将定义要渲染的数据。这只是一个包含
title和image的对象数组。
const image1 = require('./images/01.jpg');
const image2 = require('./images/02.jpg');
const image3 = require('./images/03.jpg');
const image4 = require('./images/04.jpg');
const timeline = [
{ title: 'Enjoying the fireworks', image: image1 },
{ title: 'Climbing the Mount Fuji', image: image2 },
{ title: 'Check my last picture', image: image3 },
{ title: 'Sakuras are beautiful!', image: image4 },
];
- 现在我们需要声明此组件的初始
state。当按下任何图像时,我们将更新selected和position属性,如下所示:
export default class App extends Component {
state = {
selected: null,
position: null,
};
// Defined in following steps
}
- 为了更新
state,我们将声明两个方法:一个用于设置被按下的图像的值,另一个用于在查看器关闭时删除这些值:
showImage = (selected, position) => {
this.setState({
selected,
position,
});
}
closeViewer = () => {
this.setState({
selected: null,
position: null,
});
}
- 现在我们准备开始处理
render方法。在这里,我们需要在ScrollView中渲染每个图像,以便列表可以滚动,如下所示:
render() {
return (
<SafeAreaView style={styles.main}>
<Text style={styles.toolbar}>Timeline</Text>
<ScrollView style={styles.content}>
{
timeline.map((post, index) =>
<PostContainer key={index} post={post}
onPress={this.showImage} />
)
}
</ScrollView>
{this.renderViewer()}
</SafeAreaView>
);
}
- 在上一步中,我们调用了
renderViewer方法。在这里,我们只会在状态中有一个帖子selected时显示查看器组件。我们还会发送初始位置以开始动画和一个关闭查看器的回调,如下所示:
renderViewer() {
const { selected, position } = this.state;
if (selected) {
return (
<PhotoViewer
post={selected}
position={position}
onClose={this.closeViewer}
/>
);
}
}
- 这个组件的样式非常简单,只有一些颜色和填充,如下所示:
const styles = StyleSheet.create({
main: {
backgroundColor: '#ecf0f1',
flex: 1,
},
toolbar: {
backgroundColor: '#2c3e50',
color: '#fff',
fontSize: 22,
padding: 20,
textAlign: 'center',
},
content: {
flex: 1,
},
});
- 时间轴已经完成,但是如果我们尝试运行我们的应用程序,它将失败。让我们开始处理
PostContainer组件。我们将首先导入依赖项,如下所示:
import React, { Component } from 'react';
import {
Dimensions,
Image,
Text,
TouchableOpacity,
StyleSheet,
View,
} from 'react-native';
- 我们只需要两个
props来定义这个组件。post属性将接收图像数据,title和image,onPress属性是一个回调,当图像被按下时我们将执行它,如下所示:
const { width } = Dimensions.get('window');
export default class PostContainer extends Component {
static defaultProps = {
onPress: ()=> {},
};
// Defined on following steps
}
- 这个组件将在
ScrollView中。这意味着当用户开始滚动内容时,它的位置将会改变。当按下图像时,我们需要获取屏幕上的当前位置并将这些信息发送给父组件,如下所示:
onPressImage = (event) => {
const { onPress, post } = this.props;
this.refs.main.measure((fx, fy, width, height, pageX, pageY) => {
onPress(post, {
width,
height,
pageX,
pageY,
});
});
}
- 现在是时候为这个组件定义 JSX 了。为了保持简单,我们只会渲染
image和title:
render() {
const { post: { image, title } } = this.props;
return (
<View style={styles.main} ref="main">
<TouchableOpacity
onPress={this.onPressImage}
activeOpacity={0.9}
>
<Image
source={image}
style={styles.image}
resizeMode="cover"
/>
</TouchableOpacity>
<Text style={styles.title}>{title}</Text>
</View>
);
}
- 和往常一样,我们需要为这个组件定义一些样式。我们将添加一些颜色和填充,如下所示:
const styles = StyleSheet.create({
main: {
backgroundColor: '#fff',
marginBottom: 30,
paddingBottom: 10,
},
content: {
flex: 1,
},
image: {
width,
height: 300,
},
title: {
margin: 10,
color: '#ccc',
}
});
- 如果现在运行应用程序,我们应该能够看到时间轴,但是如果我们按下任何图像,将会抛出错误。我们需要定义查看器,所以让我们打开
PhotoViewer/index.js文件并导入依赖项:
import React, { Component } from 'react';
import {
Animated,
Dimensions,
Easing,
Text,
TouchableOpacity,
StyleSheet,
} from 'react-native';
- 让我们为这个组件定义
props。为了将图像居中显示在屏幕上,我们需要知道当前设备的height:
const { width, height } = Dimensions.get('window');
export default class PhotoViewer extends Component {
static defaultProps = {
onClose: () => {},
};
// Defined on following steps
}
- 当显示这个组件时,我们希望运行两个动画,因此我们需要在组件挂载后初始化并运行动画。动画很简单:它只是在
400毫秒内从0到1进行一些缓动,如下所示:
componentWillMount() {
this.animatedValue = new Animated.Value(0);
}
componentDidMount() {
Animated.timing(
this.animatedValue,
{
toValue: 1,
duration: 400,
easing: Easing.in,
}
).start();
}
- 当用户按下关闭按钮时,我们需要执行
onClose回调来通知父组件需要移除这个组件,如下所示:
onPressBtn = () => {
this.props.onClose();
}
- 我们将把
render方法分为两步。首先,我们需要插入动画的值,如下所示:
render() {
const { post: { image, title }, position } = this.props;
const top = this.animatedValue.interpolate({
inputRange: [0, 1],
outputRange: [position.pageY, height/2 - position.height/2],
});
const opacity = this.animatedValue;
// Defined on next step
}
- 我们只需要定义三个元素:
Animated.View来动画显示背景,Animated.Image来显示图像,以及一个关闭按钮。我们将opacity样式设置为主视图,这将使图像背景从透明变为黑色。图像将同时滑入,产生一个很好的效果:
// Defined on previous step
render() {
return (
<Animated.View
style={[
styles.main,
{ opacity },
]}
>
<Animated.Image
source={image}
style={[
styles.image,
{ top, opacity }
]}
/>
<TouchableOpacity style={styles.closeBtn}
onPress={this.onPressBtn}
>
<Text style={styles.closeBtnText}>X</Text>
</TouchableOpacity>
</Animated.View>
);
}
- 我们几乎完成了!这个食谱中的最后一步是定义样式。我们需要将主容器的位置设置为绝对位置,以便图像位于其他所有内容的顶部。我们还将关闭按钮移动到屏幕的右上角,如下所示:
const styles = StyleSheet.create({
main: {
backgroundColor: '#000',
bottom: 0,
left: 0,
position: 'absolute',
right: 0,
top: 0,
},
image: {
width,
height: 300,
},
closeBtn: {
position: 'absolute',
top: 50,
right: 20,
},
closeBtnText: {
fontSize: 20,
color: '#fff',
fontWeight: 'bold',
},
});
- 最终的应用程序应该类似于以下截图:
它是如何工作的...
在步骤 4中,我们在state中定义了两个属性:selected和position。selected属性保存了按下图像的图像数据,可以是步骤 3中定义的timeline对象中的任何一个。position属性将保存屏幕上的当前y坐标,稍后用于将图像从其原始位置动画到屏幕中心。
在步骤 5中,我们对timeline数组进行map操作,以渲染每个post。我们为每个 post 使用PostContainer元素,发送post信息,并使用onPress回调来设置按下的图像。
在步骤 10中,我们需要图像的当前位置。为了实现这一点,我们使用所需信息的组件的measure方法。该方法接收一个回调函数,并检索,除其他属性外,width、height和屏幕上的当前位置。
我们正在使用引用来访问在下一步的 JSX 中声明的组件。
在步骤 11中,我们声明了组件的 JSX。在主包装容器中,我们设置了ref属性,用于获取图像的当前位置。每当我们想要在当前类的任何方法中访问组件时,我们都使用引用。我们可以通过简单地设置ref属性并为任何组件分配一个名称来创建引用。
在步骤 18中,我们插值动画值以获得每一帧的正确顶部值。插值的输出将从图像的当前位置开始,并向屏幕中间进展。这样,根据值是负数还是正数,动画将从底部向顶部运行,或者反之。
我们不需要插值 opacity,因为当前的动画值已经从 0 到 1。
另请参阅
Refs 和 DOM 的深入解释可以在以下链接找到:
reactjs.org/docs/refs-and-the-dom.html。
第八章:处理应用逻辑和数据
在本章中,我们将涵盖以下内容:
-
本地存储和检索数据
-
从远程 API 检索数据
-
向远程 API 发送数据
-
与 WebSockets 建立实时通信
-
将持久数据库功能与 Realm 集成
-
在网络连接丢失时掩盖应用程序
-
将本地持久化数据与远程 API 同步
介绍
开发任何应用程序最重要的一个方面是处理数据。这些数据可能来自用户本地,可能由公开 API 的远程服务器提供,或者,与大多数业务应用程序一样,可能是两者的组合。您可能想知道处理数据的最佳策略是什么,或者如何甚至完成简单的任务,比如发出 HTTP 请求。幸运的是,React Native 通过提供易于处理来自各种不同来源的数据的机制,使您的生活变得更加简单。
开源社区已经进一步提供了一些可以与 React Native 一起使用的优秀模块。在本章中,我们将讨论如何处理各个方面的数据,以及如何将其整合到我们的 React Native 应用程序中。
本地存储和检索数据
在开发移动应用程序时,我们需要考虑需要克服的网络挑战。一个设计良好的应用程序应该允许用户在没有互联网连接时继续使用应用程序。这需要应用程序在没有互联网连接时在设备上本地保存数据,并在网络再次可用时将数据与服务器同步。
另一个需要克服的挑战是网络连接,可能会很慢或有限。为了提高我们应用的性能,我们应该将关键数据保存在本地设备上,以避免对服务器 API 造成压力。
在这个示例中,我们将学习一种从设备本地保存和检索数据的基本有效策略。我们将创建一个简单的应用程序,其中包含一个文本输入和两个按钮,一个用于保存字段内容,另一个用于加载现有内容。我们将使用AsyncStorage类来实现我们的目标。
准备工作
我们需要创建一个名为local-data-storage的空应用程序。
如何做...
- 我们将从
App组件开始。让我们首先导入所有的依赖项:
import React, { Component } from 'react';
import {
Alert,
AsyncStorage,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
- 现在,让我们创建
App类。我们将创建一个key常量,以便我们可以设置要用于保存内容的键的名称。在state中,我们将有两个属性:一个用于保存来自文本输入组件的值,另一个用于加载和显示当前存储的值:
const key = '@MyApp:key';
export default class App extends Component {
state = {
text: '',
storedValue: '',
};
//Defined in later steps
}
- 当组件挂载时,如果存在,我们希望加载已存储的值。一旦应用程序加载,我们将显示内容,因此我们需要在
componentWillMount生命周期方法中读取本地值:
componentWillMount() {
this.onLoad();
}
onLoad函数从本地存储加载当前内容。就像浏览器中的localStorage一样,只需使用保存数据时定义的键即可:
onLoad = async () => {
try {
const storedValue = await AsyncStorage.getItem(key);
this.setState({ storedValue });
} catch (error) {
Alert.alert('Error', 'There was an error while loading the
data');
}
}
- 保存数据也很简单。我们将声明一个键,通过
AsyncStorage的setItem方法保存我们想要与该键关联的任何数据:
onSave = async () => {
const { text } = this.state;
try {
await AsyncStorage.setItem(key, text);
Alert.alert('Saved', 'Successfully saved on device');
} catch (error) {
Alert.alert('Error', 'There was an error while saving the
data');
}
}
- 接下来,我们需要一个函数,将输入文本中的值保存到
state中。当输入的值发生变化时,我们将获取新的值并保存到state中:
onChange = (text) => {
this.setState({ text });
}
- 我们的 UI 将很简单:只有一个
Text元素来呈现保存的内容,一个TextInput组件允许用户输入新值,以及两个按钮。一个按钮将调用onLoad函数来加载当前保存的值,另一个将保存文本输入的值:
render() {
const { storedValue, text } = this.state;
return (
<View style={styles.container}>
<Text style={styles.preview}>{storedValue}</Text>
<View>
<TextInput
style={styles.input}
onChangeText={this.onChange}
value={text}
placeholder="Type something here..."
/>
<TouchableOpacity onPress={this.onSave} style=
{styles.button}>
<Text>Save locally</Text>
</TouchableOpacity>
<TouchableOpacity onPress={this.onLoad} style=
{styles.button}>
<Text>Load data</Text>
</TouchableOpacity>
</View>
</View>
);
}
- 最后,让我们添加一些样式。这将是简单的颜色、填充、边距和布局,如第二章中所述,创建一个简单的 React Native 应用:
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#fff',
},
preview: {
backgroundColor: '#bdc3c7',
width: 300,
height: 80,
padding: 10,
borderRadius: 5,
color: '#333',
marginBottom: 50,
},
input: {
backgroundColor: '#ecf0f1',
borderRadius: 3,
width: 300,
height: 40,
padding: 5,
},
button: {
backgroundColor: '#f39c12',
padding: 10,
borderRadius: 3,
marginTop: 10,
},
});
- 最终的应用程序应该类似于以下截图:
它是如何工作的...
AsyncStorage类允许我们轻松地在本地设备上保存数据。在 iOS 上,这是通过在文本文件上使用字典来实现的。在 Android 上,它将使用 RocksDB 或 SQLite,具体取决于可用的内容。
不建议使用此方法保存敏感信息,因为数据未加密。
在步骤 4中,我们加载了当前保存的数据。AsyncStorage API 包含一个getItem方法。该方法接收我们要检索的键作为参数。我们在这里使用await/async语法,因为这个调用是异步的。获取值后,我们只需将其设置为state;这样,我们就能在视图上呈现数据。
在步骤 7中,我们保存了state中的文本。使用setItem方法,我们可以设置一个新的key和任何我们想要的值。这个调用是异步的,因此我们使用了await/async语法。
另请参阅
关于 JavaScript 中async/await的工作原理的一篇很棒的文章,可在ponyfoo.com/articles/understanding-javascript-async-await上找到。
从远程 API 检索数据
在之前的章节中,我们使用了来自 JSON 文件或直接在源代码中定义的数据。虽然这对我们之前的示例很有用,但在现实世界的应用程序中很少有帮助。
在这个示例中,我们将学习如何从 API 请求数据。我们将从 API 中进行GET请求,以获得 JSON 响应。然而,现在,我们只会在文本元素中显示 JSON。我们将使用 Fake Online REST API for Testing and Prototyping,托管在jsonplaceholder.typicode.com,由优秀的开发测试 API 软件 JSON Server(github.com/typicode/json-server)提供支持。
我们将保持这个应用程序简单,以便我们可以专注于数据管理。我们将有一个文本组件,用于显示来自 API 的响应,并添加一个按钮,按下时请求数据。
准备工作
我们需要创建一个空的应用。让我们把这个命名为remote-api。
如何做...
- 让我们从
App.js文件中导入我们的依赖项开始:
import React, { Component } from 'react';
import {
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View
} from 'react-native';
- 我们将在
state上定义一个results属性。这个属性将保存来自 API 的响应。一旦我们得到响应,我们需要更新视图:
export default class App extends Component {
state = {
results: '',
};
// Defined later
}
const styles = StyleSheet.create({
// Defined later
});
- 当按钮被按下时,我们将发送请求。接下来,让我们创建一个处理该请求的方法:
onLoad = async () => {
this.setState({ results: 'Loading, please wait...' });
const response = await fetch('http://jsonplaceholder.typicode.com/users', {
method: 'GET',
});
const results = await response.text();
this.setState({ results });
}
- 在
render方法中,我们将显示来自state的响应。我们将使用TextInput来显示 API 数据。通过属性,我们将声明编辑为禁用,并支持多行功能。按钮将调用我们在上一步中创建的onLoad函数:
render() {
const { results } = this.state;
return (
<View style={styles.container}>
<View>
<TextInput
style={styles.preview}
value={results}
placeholder="Results..."
editable={false}
multiline
/>
<TouchableOpacity onPress={this.onLoad} style=
{styles.btn}>
<Text>Load data</Text>
</TouchableOpacity>
</View>
</View>
);
}
- 最后,我们将添加一些样式。同样,这只是布局、颜色、边距和填充:
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#fff',
},
preview: {
backgroundColor: '#bdc3c7',
width: 300,
height: 400,
padding: 10,
borderRadius: 5,
color: '#333',
marginBottom: 50,
},
btn: {
backgroundColor: '#3498db',
padding: 10,
borderRadius: 3,
marginTop: 10,
},
});
- 最终的应用程序应该类似于以下截图:
它是如何工作的...
在步骤 4中,我们向 API 发送了请求。我们使用fetch方法发出请求。第一个参数是一个包含端点 URL 的字符串,而第二个参数是一个配置对象。对于这个请求,我们需要定义的唯一选项是request方法为GET,但我们也可以使用这个对象来定义头部、cookie、参数和许多其他内容。
我们还使用async/await语法来等待响应,并最终将其设置在state上。如果您愿意,当然也可以使用承诺来实现这个目的。
还要注意,我们在这里使用箭头函数来正确处理作用域。当将此方法分配给onPress回调时,这将自动设置正确的作用域。
向远程 API 发送数据
在上一个教程中,我们介绍了如何使用fetch从 API 获取数据。在这个教程中,我们将学习如何向同一个 API 发送数据。这个应用程序将模拟创建一个论坛帖子,帖子的请求将具有title、body和user参数。
如何做到这一点...准备好
在进行本教程之前,我们需要创建一个名为remote-api-post的新空应用程序。
在这个教程中,我们还将使用非常流行的axios包来处理我们的 API 请求。您可以通过终端使用yarn安装它:
yarn add axios
或者,您也可以使用npm:
npm install axios --save
首先,我们需要从state中获取值。我们还可以在这里运行一些验证,以确保title和body不为空。在POST请求中,我们需要定义请求的内容类型,这种情况下将是 JSON。我们将userId属性硬编码为1。在真实的应用程序中,我们可能会从之前的 API 请求中获取这个值。请求完成后,我们获取 JSON 响应,如果成功,将触发我们之前定义的onLoad方法:
- 首先,我们需要打开
App.js文件并导入我们将要使用的依赖项:
import React, { Component } from 'react';
import axios from 'axios';
import {
Alert,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
SafeAreaView,
} from 'react-native';
- 我们将定义
App类,其中包含一个具有三个属性的state对象。title和body属性将用于发出请求,results将保存 API 的响应:
const endpoint = 'http://jsonplaceholder.typicode.com/posts';
export default class App extends Component {
state = {
results: '',
title: '',
body: '',
};
const styles = StyleSheet.create({
// Defined later
});
}
- 保存新帖子后,我们将从 API 请求所有帖子。我们将定义一个
onLoad方法来获取新数据。这段代码与上一个教程中的onLoad方法完全相同,但这次,我们将使用axios包来创建请求:
onLoad = async () => {
this.setState({ results: 'Loading, please wait...' });
const response = await axios.get(endpoint);
const results = JSON.stringify(response);
this.setState({ results });
}
- 让我们开始保存新数据。
onSave = async () => {
const { title, body } = this.state;
try {
const response = await axios.post(endpoint, {
headers: {
'Content-Type': 'application/json;charset=UTF-8',
},
params: {
userId: 1,
title,
body
}
});
const results = JSON.stringify(response);
Alert.alert('Success', 'Post successfully saved');
this.onLoad();
} catch (error) {
Alert.alert('Error', `There was an error while saving the
post: ${error}`);
}
}
- 保存功能已经完成。接下来,我们需要方法来保存
title和body到state。这些方法将在用户在输入文本中输入时执行,跟踪state对象上的值:
onTitleChange = (title) => this.setState({ title });
onPostChange = (body) => this.setState({ body });
- 我们已经拥有了功能所需的一切,所以让我们添加 UI。
render方法将显示一个工具栏、两个输入文本和一个保存按钮,用于调用我们在步骤 4中定义的onSave方法:
render() {
const { results, title, body } = this.state;
return (
<SafeAreaView style={styles.container}>
<Text style={styles.toolbar}>Add a new post</Text>
<ScrollView style={styles.content}>
<TextInput
style={styles.input}
onChangeText={this.onTitleChange}
value={title}
placeholder="Title"
/>
<TextInput
style={styles.input}
onChangeText={this.onPostChange}
value={body}
placeholder="Post body..."
/>
<TouchableOpacity onPress={this.onSave} style=
{styles.button}>
<Text>Save</Text>
</TouchableOpacity>
<TextInput
style={styles.preview}
value={results}
placeholder="Results..."
editable={false}
multiline
/>
</ScrollView>
</SafeAreaView>
);
}
- 最后,让我们添加样式来定义布局、颜色、填充和边距:
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
toolbar: {
backgroundColor: '#3498db',
color: '#fff',
textAlign: 'center',
padding: 25,
fontSize: 20,
},
content: {
flex: 1,
padding: 10,
},
preview: {
backgroundColor: '#bdc3c7',
flex: 1,
height: 500,
},
input: {
backgroundColor: '#ecf0f1',
borderRadius: 3,
height: 40,
padding: 5,
marginBottom: 10,
flex: 1,
},
button: {
backgroundColor: '#3498db',
padding: 10,
borderRadius: 3,
marginBottom: 30,
},
});
- 最终的应用程序应该类似于以下截图:
工作原理...
在步骤 2中,我们在state上定义了三个属性。results属性将包含来自服务器 API 的响应,我们稍后将用它来在 UI 中显示值。
我们使用title和body属性来保存输入文本组件中的值,以便用户可以创建新的帖子。然后,这些值将在按下保存按钮时发送到 API。
在步骤 6中,我们在 UI 上声明了元素。我们使用了两个输入来输入数据和一个保存按钮,当按下时调用onSave方法。最后,我们使用输入文本来显示结果。
使用 WebSockets 建立实时通信
在本教程中,我们将在 React Native 应用程序中集成 WebSockets。我们将使用 WebSockets 应用程序的Hello World,也就是一个简单的聊天应用程序。这个应用程序将允许用户发送和接收消息。
准备工作
为了在 React Native 上支持 WebSockets,我们需要运行一个服务器来处理所有连接的客户端。服务器应该能够在收到任何连接的客户端的消息时广播消息。
我们将从一个新的空白 React Native 应用程序开始。我们将其命名为web-sockets。在项目的根目录下,让我们添加一个带有index.js文件的server文件夹。如果您还没有它,您将需要 Node 来运行服务器。您可以从nodejs.org/获取 Node.js,或者使用 Node 版本管理器(github.com/creationix/nvm)。
我们将使用优秀的 WebSocket 包ws。您可以通过终端使用yarn添加该包:
yarn add ws
或者,您可以使用npm:
npm install --save ws
安装完包后,将以下代码添加到/server/index.js文件中。一旦此服务器运行,它将通过server.on('connection')监听传入的连接,并通过socket.on('message')监听传入的消息。有关ws的更多信息,请查看github.com/websockets/ws上的文档:
const port = 3001;
const WebSocketServer = require('ws').Server;
const server = new WebSocketServer({ port });
server.on('connection', (socket) => {
socket.on('message', (message) => {
console.log('received: %s', message);
server.clients.forEach(client => {
if (client !== socket) {
client.send(message);
}
});
});
});
console.log(`Web Socket Server running on port ${port}`);
一旦服务器代码就位,您可以通过在项目根目录的终端中运行以下命令来启动服务器:
node server/index.js
让服务器保持运行,这样一旦我们构建了 React Native 应用程序,我们就可以使用服务器在客户端之间进行通信。
如何做到这一点...
- 首先,让我们创建
App.js文件并导入我们将使用的所有依赖项:
import React, { Component } from 'react';
import {
Dimensions,
ScrollView,
StyleSheet,
Text,
TextInput,
SafeAreaView,
View,
Platform
} from 'react-native';
- 在
state对象上,我们将声明一个history属性。该属性将是一个数组,用于保存用户之间来回发送的所有消息:
export default class App extends Component {
state = {
history: [],
};
// Defined in later steps
}
const styles = StyleSheet.create({
// Defined in later steps
});
- 现在,我们需要通过连接到服务器并设置用于接收消息、错误以及连接打开或关闭时的回调函数来将 WebSockets 集成到我们的应用中。我们将在组件创建后执行此操作,使用
componentWillMount生命周期钩子:
componentWillMount() {
const localhost = Platform.OS === 'android' ? '10.0.3.2' :
'localhost';
this.ws = new WebSocket(`ws://${localhost}:3001`);
this.ws.onopen = this.onOpenConnection;
this.ws.onmessage = this.onMessageReceived;
this.ws.onerror = this.onError;
this.ws.onclose = this.onCloseConnection;
}
- 让我们定义打开/关闭连接和处理接收到的错误的回调。我们只是要记录这些操作,但这是我们可以在连接关闭时显示警报消息,或者在服务器抛出错误时显示错误消息的地方:
onOpenConnection = () => {
console.log('Open!');
}
onError = (event) => {
console.log('onerror', event.message);
}
onCloseConnection = (event) => {
console.log('onclose', event.code, event.reason);
}
- 当从服务器接收到新消息时,我们需要将其添加到
state上的history属性中,以便在消息到达时能够立即呈现新内容:
onMessageReceived = (event) => {
this.setState({
history: [
...this.state.history,
{ isSentByMe: false, messageText: event.data },
],
});
}
- 现在,让我们发送消息。我们需要定义一个方法,当用户在键盘上按下Return键时将执行该方法。此时我们需要做两件事:将新消息添加到
history中,然后通过 socket 发送消息:
onSendMessage = () => {
const { text } = this.state;
this.setState({
text: '',
history: [
...this.state.history,
{ isSentByMe: true, messageText: text },
],
});
this.ws.send(text);
}
- 在上一步中,我们从
state中获取了text属性。每当用户在输入框中输入内容时,我们需要跟踪该值,因此我们需要一个用于监听按键并将值保存到state的函数:
onChangeText = (text) => {
this.setState({ text });
}
- 我们已经就位了所有功能,现在让我们来处理 UI。在
render方法中,我们将添加一个工具栏,一个滚动视图来呈现history中的所有消息,以及一个文本输入框,允许用户发送新消息:
render() {
const { history, text } = this.state;
return (
<SafeAreaView style={[styles.container, android]}>
<Text style={styles.toolbar}>Simple Chat</Text>
<ScrollView style={styles.content}>
{ history.map(this.renderMessage) }
</ScrollView>
<View style={styles.inputContainer}>
<TextInput
style={styles.input}
value={text}
onChangeText={this.onChangeText}
onSubmitEditing={this.onSendMessage}
/>
</View>
</SafeAreaView>
);
}
- 为了渲染来自
history的消息,我们将遍历history数组,并通过renderMessage方法渲染每条消息。我们需要检查当前消息是否属于此设备上的用户,以便我们可以应用适当的样式:
renderMessage(item, index){
const sender = item.isSentByMe ? styles.me : styles.friend;
return (
<View style={[styles.msg, sender]} key={index}>
<Text>{item.msg}</Text>
</View>
);
}
- 最后,让我们来处理样式!让我们为工具栏、
history组件和文本输入添加样式。我们需要将history容器设置为灵活,因为我们希望它占用所有可用的垂直空间:
const styles = StyleSheet.create({
container: {
backgroundColor: '#ecf0f1',
flex: 1,
},
toolbar: {
backgroundColor: '#34495e',
color: '#fff',
fontSize: 20,
padding: 25,
textAlign: 'center',
},
content: {
flex: 1,
},
inputContainer: {
backgroundColor: '#bdc3c7',
padding: 5,
},
input: {
height: 40,
backgroundColor: '#fff',
},
// Defined in next step
});
- 现在,让我们来处理每条消息的样式。我们将为所有消息创建一个名为
msg的公共样式对象,然后为来自设备上用户的消息创建样式,最后为来自其他人的消息创建样式,相应地更改颜色和对齐方式:
msg: {
margin: 5,
padding: 10,
borderRadius: 10,
},
me: {
alignSelf: 'flex-start',
backgroundColor: '#1abc9c',
marginRight: 100,
},
friend: {
alignSelf: 'flex-end',
backgroundColor: '#fff',
marginLeft: 100,
}
- 最终的应用程序应该类似于以下屏幕截图:
它是如何工作的...
在步骤 2中,我们声明了state对象,其中包含一个用于跟踪消息的history数组。history属性将保存表示客户端之间交换的所有消息的对象。每个对象都将有两个属性:包含消息文本的字符串,以及用于确定发送者的布尔标志。我们可以在这里添加更多数据,例如用户的名称、头像图像的 URL,或者我们可能需要的其他任何内容。
在步骤 3中,我们连接到 WebSocket 服务器提供的套接字,并设置了处理套接字事件的回调。我们指定了服务器地址以及端口。
在步骤 5中,我们定义了当从服务器接收到新消息时要执行的回调。在这里,每次接收到新消息时,我们都会向state的history数组添加一个新对象。每个消息对象都有isSentByMe的属性
messageText。
在步骤 6中,我们将消息发送到服务器。我们需要将消息添加到历史记录中,因为服务器将向所有其他客户端广播消息,但不包括消息的作者。为了跟踪这条消息,我们需要手动将其添加到历史记录中。
将持久数据库功能与 Realm 集成
随着您的应用程序变得更加复杂,您可能会达到需要在设备上存储数据的地步。这可能是业务数据,比如用户列表,以避免不得不进行昂贵的网络连接到远程 API。也许您根本没有 API,您的应用程序作为一个自给自足的实体运行。无论情况如何,您可能会受益于利用数据库来存储您的数据。对于 React Native 应用程序有多种选择。第一个选择是 AsyncStorage,我们在本章的 存储和检索本地数据 教程中介绍过。您还可以考虑 SQLite,或者您可以编写一个适配器来连接到特定于操作系统的数据提供程序,比如 Core Data。
另一个极好的选择是使用移动数据库,比如 Realm。Realm 是一个非常快速、线程安全、事务性、基于对象的数据库。它主要设计用于移动设备,具有直观的 JavaScript API。它支持其他功能,如加密、复杂查询、UI 绑定等。您可以在 realm.io/products/realm-mobile-database/ 上阅读有关它的所有信息。
在本教程中,我们将介绍在 React Native 中使用 Realm。我们将创建一个简单的数据库,并执行基本操作,如插入、更新和删除记录。然后我们将在 UI 中显示这些记录。
准备工作
让我们创建一个名为 realm-db 的新的空白 React Native 应用程序。
安装 Realm 需要运行以下命令:
react-native link
因此,我们将致力于一个从 Expo 弹出的应用程序。这意味着您可以使用以下命令创建此应用程序:
react-native init
或者,您可以使用以下命令创建一个新的 Expo 应用程序:
expo init
然后,您可以通过以下命令弹出使用 Expo 创建的应用程序:
expo eject
创建 React Native 应用程序后,请务必通过 cd 在新应用程序中使用 ios 目录安装 CocoaPods 依赖项,并运行以下命令:
pod install
有关 CocoaPods 工作原理的详细解释以及弹出(或纯粹的 React Native)应用程序与 Expo React Native 应用程序的区别,请参阅第十章 应用程序工作流程和第三方插件。
在将数据发送到远程 API的示例中,我们使用了axios包来处理我们的 AJAX 调用。在这个示例中,我们将使用原生 JavaScript 的 fetch 方法进行 AJAX 调用。两种方法都同样有效,对两种方法都有所了解,希望能让您决定您更喜欢哪种方法来进行您的项目。
一旦您处理了创建一个弹出式应用程序,就可以使用 yarn 安装 Realm:
yarn add realm
或者,您可以使用 npm:
npm install --save realm
安装了包之后,您可以使用以下代码链接本机包:
react-native link realm
如何做...
- 首先,让我们打开
App.js并导入我们将要使用的依赖项:
import React, { Component } from 'react';
import {
StyleSheet,
Text,
View,
TouchableOpacity
} from 'react-native';
import Realm from 'realm';
- 接下来,我们需要在
componentWillMount方法中实例化我们的 Realm 数据库。我们将使用realm类变量保留对它的引用:
export default class App extends Component {
realm;
componentWillMount() {
const realm = this.realm = new Realm({
schema: [
{
name: 'User',
properties: {
firstName: 'string',
lastName: 'string',
email: 'string'
}
}
]
});
}
// Defined in later steps.
}
- 为了创建
User条目,我们将使用randomuser.me提供的随机用户生成器 API。让我们创建一个带有getRandomUser函数的方法。这将fetch这些数据:
getRandomUser() {
return fetch('https://randomuser.me/api/')
.then(response => response.json());
}
- 我们还需要一个在我们的应用程序中创建用户的方法。
createUser方法将使用我们之前定义的函数来获取一个随机用户,然后使用realm.write方法和realm.create方法将其保存到我们的 realm 数据库中:
createUser = () => {
const realm = this.realm;
this.getRandomUser().then((response) => {
const user = response.results[0];
const userName = user.name;
realm.write(() => {
realm.create('User', {
firstName: userName.first,
lastName: userName.last,
email: user.email
});
this.setState({users:realm.objects('User')});
});
});
}
- 由于我们正在与数据库交互,我们还应该添加一个用于更新数据库中的
User的函数。updateUser将简单地获取集合中的第一条记录并更改其信息:
updateUser = () => {
const realm = this.realm;
const users = realm.objects('User');
realm.write(() => {
if(users.length) {
let firstUser = users.slice(0,1)[0];
firstUser.firstName = 'Bob';
firstUser.lastName = 'Cookbook';
firstUser.email = 'react.native@cookbook.com';
this.setState(users);
}
});
}
- 最后,让我们添加一种删除用户的方法。我们将添加一个
deleteUsers方法来删除所有用户。这是通过调用带有执行realm.deleteAll的回调函数的realm.write实现的:
deleteUsers = () => {
const realm = this.realm;
realm.write(() => {
realm.deleteAll();
this.setState({users:realm.objects('User')});
});
}
- 让我们构建我们的用户界面。我们将呈现一个
User对象列表和一个按钮,用于我们的create、update和delete方法:
render() {
const realm = this.realm;
return (
<View style={styles.container}>
<Text style={styles.welcome}>
Welcome to Realm DB Test!
</Text>
<View style={styles.buttonContainer}>
<TouchableOpacity style={styles.button}
onPress={this.createUser}>
<Text style={styles.buttontext}>Add User</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.button}
onPress={this.updateUser}>
<Text>Update First User</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.button}
onPress={this.deleteUsers}>
<Text>Remove All Users</Text>
</TouchableOpacity>
</View>
<View style={styles.container}>
<Text style={styles.welcome}>Users:</Text>
{this.state.users.map((user, idx) => {
return <Text key={idx}>{user.firstName} {user.lastName}
{user.email}</Text>;
})}
</View>
</View>
);
}
- 一旦我们在任何平台上运行应用程序,我们与数据库交互的三个按钮应该显示在我们的 Realm 数据库中保存的实时数据上:
它是如何工作的...
Realm 数据库是用 C++构建的,其核心被称为Realm 对象存储。有一些产品为每个主要平台(Java、Objective-C、Swift、Xamarin 和 React Native)封装了这个对象存储。React Native 的实现是 Realm 的 JavaScript 适配器。从 React Native 方面来看,我们不需要担心实现细节。相反,我们得到了一个用于持久化和检索数据的清晰 API。步骤 4到步骤 6演示了使用一些基本的 Realm 方法。如果您想了解 API 的更多用法,请查看此文档,网址为realm.io/docs/react-native/latest/api/。
在网络连接丢失时对应用进行屏蔽
互联网连接并不总是可用,特别是当人们在城市中移动、乘坐火车或在山上徒步时。良好的用户体验将在用户失去与互联网的连接时通知用户。
在这个示例中,我们将创建一个应用,当网络连接丢失时会显示一条消息。
准备工作
我们需要创建一个空的应用。让我们把它命名为network-loss。
如何做...
- 让我们从
App.js中导入必要的依赖项开始:
import React, { Component } from 'react';
import {
SafeAreaView,
NetInfo,
StyleSheet,
Text,
View,
Platform
} from 'react-native';
- 接下来,我们将定义
App类和一个用于存储连接状态的state对象。如果连接成功,online布尔值将为true,如果连接失败,offline布尔值将为true:
export default class App extends Component {
state = {
online: null,
offline: null,
};
// Defined in later steps
}
- 组件创建后,我们需要获取初始网络状态。我们将使用
NetInfo类的getConnectionInfo方法来获取当前状态,并且我们还将设置一个回调,当状态改变时将执行该回调:
componentWillMount() {
NetInfo.getConnectionInfo().then((connectionInfo) => {
this.onConnectivityChange(connectionInfo);
});
NetInfo.addEventListener('connectionChange',
this.onConnectivityChange);
}
- 当组件即将被销毁时,我们需要通过
componentWillUnmount生命周期来移除监听器:
componentWillUnmount() {
NetInfo.removeEventListener('connectionChange',
this.onConnectivityChange);
}
- 让我们添加一个在网络状态改变时执行的回调。它只是检查当前的网络类型是否为
none,并相应地设置state:
onConnectivityChange = connectionInfo => {
this.setState({
online: connectionInfo.type !== 'none',
offline: connectionInfo.type === 'none',
});
}
- 现在,我们知道网络是开启还是关闭,但我们仍然需要一个用于显示信息的用户界面。让我们渲染一个带有一些虚拟文本的工具栏作为内容:
render() {
return (
<SafeAreaView style={styles.container}>
<Text style={styles.toolbar}>My Awesome App</Text>
<Text style={styles.text}>Lorem...</Text>
<Text style={styles.text}>Lorem ipsum...</Text>
{this.renderMask()}
</SafeAreaView>
);
}
- 正如您从上一步中看到的,有一个
renderMask函数。当网络离线时,此函数将返回一个模态,如果在线则什么都不返回:
renderMask() {
if (this.state.offline) {
return (
<View style={styles.mask}>
<View style={styles.msg}>
<Text style={styles.alert}>Seems like you do not have
network connection anymore.</Text>
<Text style={styles.alert}>You can still continue
using the app, with limited content.</Text>
</View>
</View>
);
}
}
- 最后,让我们为我们的应用添加样式。我们将从工具栏和内容开始:
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F5FCFF',
},
toolbar: {
backgroundColor: '#3498db',
padding: 15,
fontSize: 20,
color: '#fff',
textAlign: 'center',
},
text: {
padding: 10,
},
// Defined in next step
}
- 对于断开连接的消息,我们将在所有内容的顶部呈现一个黑色蒙版,并在屏幕中央放置一个带有文本的容器。对于
mask,我们需要将位置设置为absolute,然后将top、bottom、right和left设置为0。我们还将为 mask 的背景颜色添加不透明度,并将内容调整和对齐到中心:
const styles = StyleSheet.create({
// Defined in previous step
mask: {
alignItems: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
bottom: 0,
justifyContent: 'center',
left: 0,
position: 'absolute',
top: 0,
right: 0,
},
msg: {
backgroundColor: '#ecf0f1',
borderRadius: 10,
height: 200,
justifyContent: 'center',
padding: 10,
width: 300,
},
alert: {
fontSize: 20,
textAlign: 'center',
margin: 5,
}
});
- 要在模拟器中看到蒙版显示,模拟设备必须断开与互联网的连接。对于 iOS 模拟器,只需断开 Mac 的 Wi-Fi 或拔掉以太网即可断开模拟器与互联网的连接。在 Android 模拟器上,您可以通过工具栏禁用手机的 Wi-Fi 连接:
- 一旦设备与互联网断开连接,蒙版应该相应地显示:
工作原理...
在步骤 2中,我们创建了初始的state对象,其中包含两个属性:online在网络连接可用时为true,offline在网络不可用时为true。
在步骤 3中,我们检索了初始的网络状态并设置了一个监听器来检查状态的变化。NetInfo返回的网络类型可以是wifi、cellular、unknown或none。Android 还有额外的选项,如bluetooth、ethernet和WiMAX(用于 WiMAX 连接)。您可以阅读文档以查看所有可用的值:facebook.github.io/react-native/docs/netinfo.html。
在步骤 5中,我们定义了每当网络状态发生变化时执行的方法,并相应地设置了state的值online和offline。更新状态会重新渲染 DOM,如果没有连接,则会显示蒙版。
将本地持久化数据与远程 API 同步
在使用移动应用程序时,网络连接通常是理所当然的。但是当您的应用程序需要调用 API 并且用户刚刚失去连接时会发生什么?幸运的是,对于我们来说,React Native 有一个模块可以对网络连接状态做出反应。我们可以以一种支持连接丢失的方式设计我们的应用程序,通过在网络连接恢复后自动同步我们的数据来实现。
这个步骤将展示使用NetInfo模块来控制我们的应用是否会进行 API 调用的简单实现。如果失去连接,我们将保留挂起请求的引用,并在网络访问恢复时完成它。我们将再次使用jsonplaceholder.typicode.com来向实时服务器发出POST请求。
准备工作
对于这个步骤,我们将使用一个名为syncing-data的空的 React Native 应用程序。
如何做...
- 我们将从在
App.js中导入我们的依赖项开始这个步骤:
import React from 'react';
import {
StyleSheet,
Text,
View,
NetInfo,
TouchableOpacity
} from 'react-native';
- 我们需要添加
pendingSync类变量,用于在没有可用网络连接时存储挂起的请求。我们还将创建state对象,其中包含用于跟踪应用程序是否连接(isConnected)、同步状态(syncStatus)以及发出POST请求后服务器的响应(serverResponse)的属性:
export default class App extends React.Component {
pendingSync;
state = {
isConnected: null,
syncStatus: null,
serverResponse: null
}
// Defined in later steps
}
- 在
componentWillMount生命周期钩子中,我们将通过NetInfo.isConnected.fetch方法获取网络连接的状态,并使用响应设置状态的isConnected属性。我们还将添加一个connectionChange事件的事件侦听器,以跟踪连接的变化:
componentWillMount() {
NetInfo.isConnected.fetch().then(isConnected => {
this.setState({isConnected});
});
NetInfo.isConnected.addEventListener('connectionChange',
this.onConnectionChange);
}
- 接下来,让我们实现在前一步中定义的事件侦听器将执行的回调。在这个方法中,我们将更新
state的isConnected属性。然后,如果定义了pendingSync类变量,这意味着我们有一个缓存的POST请求,因此我们将提交该请求并相应地更新状态:
onConnectionChange = (isConnected) => {
this.setState({isConnected});
if (this.pendingSync) {
this.setState({syncStatus : 'Syncing'});
this.submitData(this.pendingSync).then(() => {
this.setState({syncStatus : 'Sync Complete'});
});
}
}
- 接下来,我们需要实现一个函数,当有活动网络连接时,将实际进行 API 调用:
submitData(requestBody) {
return fetch('http://jsonplaceholder.typicode.com/posts', {
method : 'POST',
body : JSON.stringify(requestBody)
}).then((response) => {
return response.text();
}).then((responseText) => {
this.setState({
serverResponse : responseText
});
});
}
- 在我们可以开始处理用户界面之前,我们需要做的最后一件事是为“提交数据”按钮添加一个处理
onPress事件的函数,我们将要渲染这个按钮。如果没有网络连接,这将立即执行调用,否则将保存在this.pendingSync中:
onSubmitPress = () => {
const requestBody = {
title: 'foo',
body: 'bar',
userId: 1
};
if (this.state.isConnected) {
this.submitData(requestBody);
} else {
this.pendingSync = requestBody;
this.setState({syncStatus : 'Pending'});
}
}
- 现在,我们可以构建我们的用户界面,它将渲染“提交数据”按钮,并显示当前连接状态、同步状态以及来自 API 的最新响应:
render() {
const {
isConnected,
syncStatus,
serverResponse
} = this.state;
return (
<View style={styles.container}>
<TouchableOpacity onPress={this.onSubmitPress}>
<View style={styles.button}>
<Text style={styles.buttonText}>Submit Data</Text>
</View>
</TouchableOpacity>
<Text style={styles.status}>
Connection Status: {isConnected ? 'Connected' :
'Disconnected'}
</Text>
<Text style={styles.status}>
Sync Status: {syncStatus}
</Text>
<Text style={styles.status}>
Server Response: {serverResponse}
</Text>
</View>
);
}
- 您可以像在上一个步骤的步骤 10中描述的那样在模拟器中禁用网络连接:
它是如何工作的...
这个步骤利用了NetInfo模块来控制何时进行 AJAX 请求。
在步骤 6中,我们定义了当单击“提交数据”按钮时执行的方法。如果没有连接,我们将请求主体保存到pendingSync类变量中。
在步骤 3中,我们定义了componentWillMount生命周期钩子。在这里,两个NetInfo方法调用检索当前的网络连接状态,并附加一个事件监听器到变化事件。
在步骤 4中,我们定义了当网络连接发生变化时将执行的函数,该函数适当地通知状态的isConnected布尔属性。如果设备已连接,我们还会检查是否有挂起的 API 调用,并在存在时完成请求。
这个教程也可以扩展为支持挂起调用的队列系统,这将允许多个 AJAX 请求延迟,直到重新建立互联网连接。
使用 Facebook 登录
Facebook 是全球最大的社交媒体平台,拥有超过 10 亿用户。这意味着您的用户很可能拥有 Facebook 账户。您的应用程序可以注册并链接到他们的账户,从而允许您使用他们的 Facebook 凭据作为应用程序的登录。根据请求的权限,这还将允许您访问用户信息、图片,甚至让您能够访问共享内容。您可以从 Facebook 文档中了解更多关于可用权限选项的信息,网址为developers.facebook.com/docs/facebook-login/permissions#reference-public_profile。
在这个教程中,我们将介绍通过应用程序登录 Facebook 以获取会话令牌的基本方法。然后,我们将使用该令牌访问 Facebook 的 Graph API 提供的基本/me端点,这将给我们用户的姓名和 ID。要与 Facebook Graph API 进行更复杂的交互,您可以查看文档,该文档可以在developers.facebook.com/docs/graph-api/using-graph-api找到。
为了使这个示例简单,我们将构建一个使用Expo.Facebook.logInWithReadPermissionsAsync方法来完成 Facebook 登录的 Expo 应用程序,这也将允许我们绕过其他必要的设置。如果您希望在不使用 Expo 的情况下与 Facebook 交互,您可能希望使用 React Native Facebook SDK,这需要更多的步骤。您可以在github.com/facebook/react-native-fbsdk找到 SDK。
准备工作
对于这个示例,我们将创建一个名为facebook-login的新应用程序。您需要有一个活跃的 Facebook 账户来测试其功能。
这个示例还需要一个 Facebook 开发者账户。如果您还没有,请前往developers.facebook.com注册。一旦您登录,您可以使用仪表板创建一个新的应用程序。一旦创建完成,请记下应用程序 ID,因为我们将在示例中需要它。
如何做...
- 让我们从打开
App.js文件并添加我们的导入开始:
import React from 'react';
import {
StyleSheet,
Text,
View,
TouchableOpacity,
Alert
} from 'react-native';
import Expo from 'expo';
- 接下来,我们将声明
App类并添加state对象。state将跟踪用户是否使用loggedIn布尔值登录,并将在一个名为facebookUserInfo的对象中保存从 Facebook 检索到的用户数据:
export default class App extends React.Component {
state = {
loggedIn: false,
facebookUserInfo: {}
}
// Defined in later steps
}
- 接下来,让我们定义我们的类的
logIn方法。这将是在按下登录按钮时调用的方法。这个方法使用Facebook方法的logInWithReadPermissionsAsyncExpo 辅助类来提示用户显示 Facebook 登录屏幕。在下面的代码中,用你的应用 ID 替换第一个参数,标记为APP_ID:
logIn = async () => {
const { type, token } = await
Facebook.logInWithReadPermissionsAsync(APP_ID, {
permissions: ['public_profile'],
});
// Defined in next step
}
- 在
logIn方法的后半部分,如果请求成功,我们将使用从登录中收到的令牌调用 Facebook Graph API 来请求已登录用户的信息。一旦响应解析,我们就相应地设置状态:
logIn = async () => {
//Defined in step above
if (type === 'success') {
const response = await fetch(`https://graph.facebook.com/me?
access_token=${token}`);
const facebookUserInfo = await response.json();
this.setState({
facebookUserInfo,
loggedIn: true
});
}
}
- 我们还需要一个简单的
render函数。我们将显示一个登录按钮用于登录,以及一些Text元素,一旦登录成功完成,将显示用户信息:
render() {
return (
<View style={styles.container}>
<Text style={styles.headerText}>Login via Facebook</Text>
<TouchableOpacity
onPress={this.logIn}
style={styles.button}
>
<Text style={styles.buttonText}>Login</Text>
</TouchableOpacity>
{this.renderFacebookUserInfo()}
</View>
);
}
- 如您在前面的
render函数中所看到的,我们正在调用this.renderFacebookUserInfo来渲染用户信息。这个方法简单地检查用户是否通过this.state.loggedIn登录。如果是,我们将显示用户的信息。如果不是,我们将返回null来显示空白:
renderFacebookUserInfo = () => {
return this.state.loggedIn ? (
<View style={styles.facebookUserInfo}>
<Text style={styles.facebookUserInfoLabel}>Name:</Text>
<Text style={styles.facebookUserInfoText}>
{this.state.facebookUserInfo.name}</Text>
<Text style={styles.facebookUserInfoLabel}>User ID:</Text>
<Text style={styles.facebookUserInfoText}>
{this.state.facebookUserInfo.id}</Text>
</View>
) : null;
}
- 最后,我们将添加样式以完成布局,设置填充、边距、颜色和字体大小:
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
button: {
marginTop: 30,
padding: 10,
backgroundColor: '#3B5998'
},
buttonText: {
color: '#fff',
fontSize: 30
},
headerText: {
fontSize: 30
},
facebookUserInfo: {
paddingTop: 30
},
facebookUserInfoText: {
fontSize: 24
},
facebookUserInfoLabel: {
fontSize: 20,
marginTop: 10,
color: '#474747'
}
});
- 现在,如果我们运行应用程序,我们将看到我们的登录按钮,当按下登录按钮时会出现登录模态,以及用户的信息,一旦用户成功登录,将显示在屏幕上:
它是如何工作的...
在我们的 React Native 应用中与 Facebook 互动要比以往更容易,通过 Expo 的Facebook辅助库。
在步骤 5中,我们创建了logIn函数,它使用Facebook.logInWithReadPermissionsAsync来向 Facebook 发出登录请求。它接受两个参数:一个appID和一个选项对象。在我们的情况下,我们只设置了权限选项。权限选项接受一个字符串数组,用于请求每种类型的权限,但是对于我们的目的,我们只使用最基本的权限,'public_profile'。
在步骤 6中,我们完成了logIn函数。在成功登录后,它使用从logInWithReadPermissionsAsync返回的数据提供的令牌,向 Facebook 的 Graph API 端点/me发出调用。用户的信息和登录状态将保存到状态中,这将触发重新渲染并在屏幕上显示用户的数据。
这个示例故意只调用了一个简单的 API 端点。您可以使用此端点返回的数据来填充应用程序中的用户数据。或者,您可以使用从登录中收到的相同令牌来执行图形 API 提供的任何操作。要查看通过 API 可以获得哪些数据,您可以在developers.facebook.com/docs/graph-api/reference上查看参考文档。