ReactNative 秘籍第二版(二)
原文:
zh.annas-archive.org/md5/12592741083b1cbc7e657e9f51045dce译者:飞龙
第五章:实施复杂用户界面-第三部分
在本章中,我们将涵盖以下示例:
-
创建地图应用程序
-
创建音频播放器
-
创建图像轮播
-
将推送通知添加到您的应用程序
-
实施基于浏览器的身份验证
介绍
在本章中,我们将介绍您可能需要添加到应用程序的一些更高级功能。本章中我们将构建的应用程序包括构建完全功能的音频播放器,地图集成以及实施基于浏览器的身份验证,以便您的应用程序可以连接到开发人员的公共 API。
创建地图应用程序
使用移动设备是一种便携式体验,因此地图是许多 iOS 和 Android 应用程序的常见部分并不奇怪。您的应用程序可能需要告诉用户他们在哪里,他们要去哪里,或者其他用户实时在哪里。
在这个示例中,我们将制作一个简单的应用程序,该应用程序在 Android 上使用 Google Maps,在 iOS 上使用 Apple 的地图应用程序,以显示以用户位置为中心的地图。我们将使用 Expo 的Location辅助库来获取用户的纬度和经度,并将使用这些数据来使用 Expo 的MapView组件渲染地图。MapView是由 Airbnb 创建的 react-native-maps 包的 Expo 版本,因此您可以期望 react-native-maps 文档适用,该文档可以在github.com/react-community/react-native-maps找到。
准备工作
我们需要为这个示例创建一个新的应用程序。让我们称之为map-app。由于此示例中的用户图标将使用自定义图标,因此我们还需要一个图像。我使用了 Maico Amorim 的图标 You Are Here,您可以从thenounproject.com/term/you-are-here/12314/下载。随意使用任何您喜欢的图像来代表用户图标。将图像保存到项目根目录的assets文件夹中。
如何做...
- 我们将首先打开
App.js并添加我们的导入:
import React from 'react';
import {
Location,
Permissions,
MapView,
Marker
} from 'expo';
import {
StyleSheet,
Text,
View,
} from 'react-native';
- 接下来,让我们定义
App类和初始state。在这个示例中,state只需要跟踪用户的位置,我们将其初始化为null:
export default class App extends Component {
state = {
location: null
}
// Defined in following steps
}
- 接下来,我们将定义
componentDidMount生命周期钩子,它将要求用户授予访问设备地理位置的权限。如果用户授予应用程序使用其位置的权限,返回的对象将具有一个值为'granted'的status属性。如果授予了权限,我们将使用this.getLocation获取用户的位置,这是在下一步中定义的:
async componentDidMount() {
const permission = await Permissions.askAsync(Permissions.LOCATION);
if (permission.status === 'granted') {
this.getLocation();
}
}
getLocation函数很简单。它使用Location组件的getCurrentPositionAsync方法从设备的 GPS 中获取位置信息,然后将该位置信息保存到state中。该信息包含用户的纬度和经度,在渲染地图时我们将使用它:
async getLocation() {
let location = await Location.getCurrentPositionAsync({});
this.setState({
location
});
}
- 现在,让我们使用该位置信息来渲染我们的地图。首先,我们将检查
state上是否保存了一个location。如果是,我们将渲染MapView,否则渲染null。我们需要设置的唯一属性来渲染地图是initialRegion属性,它定义了地图在首次渲染时应该显示的位置。我们将在具有保存到state的纬度和经度的对象上传递这个属性,并使用latitudeDelta和longitudeDelta定义一个起始缩放级别:
renderMap() {
return this.state.location ?
<MapView
style={styles.map}
initialRegion={{
latitude: this.state.location.coords.latitude,
longitude: this.state.location.coords.longitude,
latitudeDelta: 0.09,
longitudeDelta: 0.04,
}}
>
// Map marker is defined in next step
</MapView> : null
}
- 在
MapView中,我们需要在用户当前位置添加一个标记。Marker组件是MapView的父组件的一部分,所以在 JSX 中,我们将定义MapView.Marker作为MapView元素的子元素。这个元素需要用户的位置、标题和描述以在图标被点击时显示,以及通过image属性定义一个自定义图像:
<MapView
style={styles.map}
initialRegion={{
latitude: this.state.location.coords.latitude,
longitude: this.state.location.coords.longitude,
latitudeDelta: 0.09,
longitudeDelta: 0.04,
}}
>
<MapView.Marker
coordinate={this.state.location.coords}
title={"User Location"}
description={"You are here!"}
image={require('./assets/you-are-here.png')}
/>
</MapView> : null
- 现在,让我们定义我们的
render函数。它简单地在一个包含的View元素中渲染地图:
render() {
return (
<View style={styles.container}>
{this.renderMap()}
</View>
);
}
- 最后,让我们添加我们的样式。我们将在容器和地图上都将
flex设置为1,以便两者都填满屏幕:
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
map: {
flex: 1
}
});
- 现在,如果我们打开应用程序,我们将看到一个地图在设备提供的位置上渲染了我们自定义的用户图标!不幸的是,Google 地图集成可能无法在 Android 模拟器中工作,因此可能需要一个真实的设备来测试应用程序的 Android 实现。查看本食谱末尾的*还有更多...*部分以获取更多信息。不要惊讶,iOS 应用程序在模拟器上运行时显示用户的位置在旧金山;这是由于 Xcode 位置默认设置的工作方式。在真实的 iOS 设备上运行它,以查看它是否渲染了你的位置:
工作原理...
通过利用 Expo 提供的MapView组件,在你的 React Native 应用中实现地图现在比以前简单直接得多。
在步骤 3中,我们利用了Permissions帮助库。Permissions有一个叫做askAsync的方法,它接受一个参数,定义了你的应用想要从用户那里请求什么类型的权限。Permissions还为你可以从用户那里请求的每种类型的权限提供了常量。这些权限类型包括LOCATION,NOTIFICATIONS(我们将在本章后面使用),CAMERA,AUDIO_RECORDING,CONTACTS,CAMERA_ROLL和CALENDAR。由于我们在这个示例中需要位置,我们传入了常量Permissions.LOCATION。一旦askAsync返回 promise 解析,返回对象将有一个status属性和一个expiration属性。如果用户已经允许了请求的权限,status将被设置为'granted'字符串。如果被授予,我们将触发我们的getLocation方法。
在步骤 4中,我们定义了从设备 GPS 获取位置的函数。我们调用Location组件的getCurrentPositionAsync方法。这个方法将返回一个带有coords属性和timestamp属性的对象。coords属性让我们可以访问latitude和longitude,以及altitude,accuracy(位置的不确定性半径,以米为单位测量),altitudeAccuracy(高度值的精度,以米为单位(仅限 iOS)),heading和speed。一旦接收到,我们将位置保存到state中,这样render函数将被调用,我们的地图将被渲染。
在步骤 5中,我们定义了renderMap方法来渲染地图。首先,我们检查是否有位置,如果有,我们渲染MapView元素。这个元素只需要我们定义一个属性的值:initialRegion。这个属性接受一个带有四个属性的对象:latitude,longitude,latitudeDelta和longitudeDelta。我们将latitude和longitude设置为state对象中的值,并为latitudeDelta和longitudeDelta提供初始值。这两个属性决定了地图应该以什么初始缩放级别进行渲染;这个数字越大,地图就会显示得越远。我建议尝试这两个值,看看它们如何影响渲染的地图。
在步骤 6中,我们通过将MapView.Marker元素作为MapView元素的子元素添加到地图上。我们通过将保存在state(state.location.coords)上的信息传递给coords属性来定义坐标,并在被点击时为标记的弹出窗口设置了title和description。我们还可以通过在image属性中使用require语句内联我们的自定义图像来轻松定义自定义图钉。
还有更多...
如前所述,您可以阅读 react-native-maps 项目的文档,了解这个优秀库的更多功能(github.com/react-community/react-native-maps)。例如,您可以使用 Google 地图样式向导(mapstyle.withgoogle.com/)轻松自定义 Google 地图的外观,生成mapStyle JSON 对象,然后将该对象传递给MapView组件的customMapStyle属性。或者,您可以使用Polygon和Circle组件向地图添加几何形状。
一旦您准备部署您的应用程序,您需要采取一些后续步骤来确保地图在 Android 上正常工作。您可以阅读 Expo 文档中有关如何使用MapView组件部署到独立 Android 应用程序的详细信息:docs.expo.io/versions/latest/sdk/map-view#deploying-to-a-standalone-app-on-android。
在 Android 模拟器中渲染 Google 地图可能会出现问题。您可以参考以下 GitHub 链接获取更多信息:github.com/react-native-community/react-native-maps/issues/942。
创建音频播放器
音频播放器是许多应用程序内置的常见界面。无论您的应用程序需要在设备上本地播放音频文件还是从远程位置流式传输音频,Expo 的Audio组件都可以帮助您。
在这个食谱中,我们将构建一个功能齐全的基本音频播放器,具有播放/暂停、下一曲和上一曲功能。为简单起见,我们将硬编码我们将使用的曲目信息,但在现实世界的情况下,您可能会使用类似我们定义的对象:一个带有曲目标题、专辑名称、艺术家名称和远程音频文件 URL 的对象。我从互联网档案馆的现场音乐档案中随机选择了三个现场曲目(archive.org/details/etree)。
准备工作
我们需要为这个食谱创建一个新的应用。让我们称之为audio-player。
如何做...
- 让我们从打开
App.js并添加我们需要的依赖开始:
import React, { Component } from 'react';
import { Audio } from 'expo';
import { Feather } from '@expo/vector-icons';
import {
StyleSheet,
Text,
TouchableOpacity,
View,
Dimensions
} from 'react-native';
- 音频播放器需要音频来播放。我们将创建一个
playlist数组来保存音频曲目。每个曲目由一个带有title、artist、album和uri的对象表示:
const playlist = [
{
title: 'People Watching',
artist: 'Keller Williams',
album: 'Keller Williams Live at The Westcott Theater on 2012-09-22',
uri: 'https://ia800308.us.archive.org/7/items/kwilliams2012-09-22.at853.flac16/kwilliams2012-09-22at853.t16.mp3'
},
{
title: 'Hunted By A Freak',
artist: 'Mogwai',
album: 'Mogwai Live at Ancienne Belgique on 2017-10-20',
uri: 'https://ia601509.us.archive.org/17/items/mogwai2017-10-20.brussels.fm/Mogwai2017-10-20Brussels-07.mp3'
},
{
title: 'Nervous Tic Motion of the Head to the Left',
artist: 'Andrew Bird',
album: 'Andrew Bird Live at Rio Theater on 2011-01-28',
uri: 'https://ia800503.us.archive.org/8/items/andrewbird2011-01-28.early.dr7.flac16/andrewbird2011-01-28.early.t07.mp3'
}
];
- 接下来,我们将定义我们的
App类和初始的state对象,其中包含四个属性:
-
isPlaying用于定义播放器是正在播放还是暂停 -
playbackInstance用于保存Audio实例 -
volume和currentTrackIndex用于当前播放的曲目 -
isBuffering用于在曲目在播放开始时缓冲时显示缓冲中...消息
如下所示的代码:
export default class App extends Component {
state = {
isPlaying: false,
playbackInstance: null,
volume: 1.0,
currentTrackIndex: 0,
isBuffering: false,
}
// Defined in following steps
}
- 让我们接下来定义
componentDidMount生命周期钩子。我们将使用这个方法通过setAudioModeAsync方法配置Audio组件,传入一个带有一些推荐设置的options对象。这些将在食谱末尾的*它是如何工作...*部分进行更多讨论。之后,我们将使用loadAudio加载音频,定义在下一步中:
async componentDidMount() {
await Audio.setAudioModeAsync({
allowsRecordingIOS: false,
playThroughEarpieceAndroid: true,
interruptionModeIOS: Audio.INTERRUPTION_MODE_IOS_DO_NOT_MIX,
playsInSilentModeIOS: true,
shouldDuckAndroid: true,
interruptionModeAndroid:
Audio.INTERRUPTION_MODE_ANDROID_DO_NOT_MIX,
});
this.loadAudio();
}
loadAudio函数将处理我们播放器的音频加载。首先,我们将创建一个新的Audio.Sound实例。然后,我们将在我们的新Audio实例上调用setOnPlaybackStatusUpdate方法,传入一个处理程序,每当实例内的播放状态发生变化时将被调用。最后,我们在实例上调用loadAsync,传入一个来自playlist数组的源,以及一个带有音量和state的isPlaying值的shouldPlay属性的状态对象。第三个参数决定我们是否希望在播放之前等待文件下载完成,因此我们传入false:
async loadAudio() {
const playbackInstance = new Audio.Sound();
const source = {
uri: playlist[this.state.currentTrackIndex].uri
}
const status = {
shouldPlay: this.state.isPlaying,
volume: this.state.volume,
};
playbackInstance
.setOnPlaybackStatusUpdate(
this.onPlaybackStatusUpdate
);
await playbackInstance.loadAsync(source, status, false);
this.setState({
playbackInstance
});
}
- 我们仍然需要定义处理状态更新的回调。在这个函数中,我们需要做的就是将从
setOnPlaybackStatusUpdate函数调用中传入的status参数上的isBuffering值设置到state上的isBuffering值:
onPlaybackStatusUpdate = (status) => {
this.setState({
isBuffering: status.isBuffering
});
}
- 我们的应用现在知道如何从
playlist数组中加载音频文件,并更新state中加载的音频文件的当前缓冲状态,我们稍后将在render函数中使用它向用户显示消息。现在剩下的就是为播放器本身添加行为。首先,我们将处理播放/暂停状态。handlePlayPause方法检查this.state.isPlaying的值,以确定是否应播放或暂停曲目,并相应地调用playbackInstance上的关联方法。最后,我们需要更新state中的isPlaying的值:
handlePlayPause = async () => {
const { isPlaying, playbackInstance } = this.state;
isPlaying ? await playbackInstance.pauseAsync() : await playbackInstance.playAsync();
this.setState({
isPlaying: !isPlaying
});
}
- 接下来,让我们定义处理跳转到上一首曲目的函数。首先,我们通过调用
unloadAsync从playbackInstance中清除当前曲目。然后,我们将state中的currentTrackIndex值更新为当前值减一,或者如果我们在playlist数组的开头,则更新为0。然后,我们将调用this.loadAudio来加载正确的曲目:
handlePreviousTrack = async () => {
let { playbackInstance, currentTrackIndex } = this.state;
if (playbackInstance) {
await playbackInstance.unloadAsync();
currentTrackIndex === 0 ? currentTrackIndex = playlist.length
- 1 : currentTrackIndex -= 1;
this.setState({
currentTrackIndex
});
this.loadAudio();
}
}
- 毫不奇怪,
handleNextTrack与前面的函数相同,但这次我们要么将当前索引加1,要么如果我们在playlist数组的末尾,则将索引设置为0:
handleNextTrack = async () => {
let { playbackInstance, currentTrackIndex } = this.state;
if (playbackInstance) {
await playbackInstance.unloadAsync();
currentTrackIndex < playlist.length - 1 ? currentTrackIndex +=
1 : currentTrackIndex = 0;
this.setState({
currentTrackIndex
});
this.loadAudio();
}
}
- 现在是时候定义我们的
render函数了。在我们的 UI 中,我们需要三个基本部分:当曲目正在播放但仍在缓冲时显示“缓冲中…”的消息,用于显示当前曲目信息的部分,以及用于保存播放器控件的部分。当且仅当this.state.isBuffering和this.state.isPlaying都为true时,“缓冲中…”消息才会显示。歌曲信息是通过renderSongInfo方法呈现的,我们将在步骤 12中定义:
render() {
return (
<View style={styles.container}>
<Text style={[styles.largeText, styles.buffer]}>
{this.state.isBuffering && this.state.isPlaying ?
'Buffering...' : null}
</Text>
{this.renderSongInfo()}
<View style={styles.controls}>
// Defined in next step.
</View>
</View>
);
}
- 播放器控件由三个
TouchableOpacity按钮元素组成,每个按钮都有来自 Feather 图标库的相应图标。您可以在第三章中找到有关使用图标的更多信息,实现复杂用户界面-第一部分。根据this.state.isPlaying的值,我们将确定是显示播放图标还是暂停图标:
<View style={styles.controls}>
<TouchableOpacity
style={styles.control}
onPress={this.handlePreviousTrack}
>
<Feather name="skip-back" size={32} color="#fff"/>
</TouchableOpacity>
<TouchableOpacity
style={styles.control}
onPress={this.handlePlayPause}
>
{this.state.isPlaying ?
<Feather name="pause" size={32} color="#fff"/> :
<Feather name="play" size={32} color="#fff"/>
}
</TouchableOpacity>
<TouchableOpacity
style={styles.control}
onPress={this.handleNextTrack}
>
<Feather name="skip-forward" size={32} color="#fff"/>
</TouchableOpacity>
</View>
renderSongInfo方法返回用于显示当前播放的曲目相关元数据的基本 JSX:
renderSongInfo() {
const { playbackInstance, currentTrackIndex } = this.state;
return playbackInstance ?
<View style={styles.trackInfo}>
<Text style={[styles.trackInfoText, styles.largeText]}>
{playlist[currentTrackIndex].title}
</Text>
<Text style={[styles.trackInfoText, styles.smallText]}>
{playlist[currentTrackIndex].artist}
</Text>
<Text style={[styles.trackInfoText, styles.smallText]}>
{playlist[currentTrackIndex].album}
</Text>
</View>
: null;
}
- 现在剩下的就是添加样式。这里定义的样式现在已经是老生常谈了,不超出居中、颜色、字体大小以及添加填充和边距的范围:
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#191A1A',
alignItems: 'center',
justifyContent: 'center',
},
trackInfo: {
padding: 40,
backgroundColor: '#191A1A',
},
buffer: {
color: '#fff'
},
trackInfoText: {
textAlign: 'center',
flexWrap: 'wrap',
color: '#fff'
},
largeText: {
fontSize: 22
},
smallText: {
fontSize: 16
},
control: {
margin: 20
},
controls: {
flexDirection: 'row'
}
});
- 您现在可以在模拟器中查看您的应用,应该有一个完全正常工作的音频播放器!请注意,Android 模拟器中的音频播放速度可能太慢,无法正常工作,并且可能听起来非常杂乱。在真实的 Android 设备上打开应用程序以听到音轨正常播放:
工作原理...
在步骤 4中,一旦应用程序完成加载,我们就在componentDidMount方法中对Audio组件进行了初始化。Audio组件的setAudioModeAsync方法将一个选项对象作为其唯一参数。
让我们回顾一些我们在这个配方中使用的选项:
-
interruptionModeIOS和interruptionModeAndroid设置了您的应用中的音频应该如何与设备上其他应用程序的音频进行交互。我们分别使用了Audio组件的INTERRUPTION_MODE_IOS_DO_NOT_MIX和INTERRUPTION_MODE_ANDROID_DO_NOT_MIX枚举来声明我们的应用音频应该中断任何其他正在播放音频的应用程序。 -
playsInSilentModeIOS是一个布尔值,用于确定当设备处于静音模式时,您的应用是否应该播放音频。 -
shouldDuckAndroid是一个布尔值,用于确定当另一个应用的音频中断您的应用时,您的应用的音频是否应该降低音量(减小)。虽然此设置默认为true,但我已将其添加到配方中,以便您知道这是一个选项。
在步骤 5中,我们定义了loadAudio方法,该方法在这个示例中承担了大部分工作。首先,我们创建了Audio.Sound类的新实例,并将其保存到playbackInstance变量中以供以后使用。接下来,我们设置将传递到playbackInstance的loadAsync函数的source和status变量,用于实际加载音频文件。在source对象中,我们将uri属性设置为playlist数组中对象中的相应uri属性的索引存储在this.state.currentTrackIndex中。在status对象中,我们将音量设置为state上保存的volume值,并将shouldPlay设置为一个布尔值,用于确定音频是否应该播放,最初设置为this.state.isPlaying。由于我们希望流式传输远程 MP3 文件而不是等待整个文件下载,因此我们将第三个参数downloadFirst设置为false。
在调用loadAsync方法之前,我们首先调用了playbackInstance的setOnPlaybackStatusUpdate,它接受一个回调函数,当playbackInstance的状态发生变化时应该被调用。我们在步骤 6中定义了该处理程序。该处理程序简单地将回调的status参数中的isBuffering值保存到state的isBuffering属性中,这将触发重新渲染,相应地更新 UI 中的'缓冲中...'消息。
在步骤 7中,我们定义了handlePlayPause函数,用于在应用程序中切换播放和暂停功能。如果有曲目正在播放,this.state.isPlaying将为true,因此我们将在playbackInstance上调用pauseAsync函数,否则,我们将调用playAsync来重新开始播放音频。一旦我们播放或暂停,我们就会更新state上的isPlaying的值。
在步骤 8和步骤 9中,我们创建了处理跳转到下一首和上一首曲目的函数。每个函数根据需要增加或减少this.state.currentTrackIndex的值,因此在每个函数底部调用this.loadAudio时,它将加载与playlist数组中对象相关联的曲目的新索引。
还有更多...
我们当前应用程序的功能比大多数音频播放器更基本,但您可以利用所有工具来构建功能丰富的音频播放器。例如,您可以通过在setOnPlaybackStatusUpdate回调中利用status参数上的positionMillis属性在 UI 中显示当前曲目时间。或者,您可以使用 React Native 的Slider组件允许用户调整音量或播放速率。Expo 的Audio组件提供了构建出色音频播放器应用程序的所有基本组件。
创建图像轮播
有各种应用程序使用图像轮播。每当有一组图像,您希望用户能够浏览时,轮播很可能是实现任务的最有效的 UI 模式之一。
在 React Native 社区中有许多软件包用于处理轮播的创建,但根据我的经验,没有一个比 react-native-snap-carousel (github.com/archriss/react-native-snap-carousel)更稳定或更多功能。该软件包为自定义轮播的外观和行为提供了出色的 API,并支持 Expo 应用程序开发,无需弹出。您可以通过 Carousel 组件的layout属性轻松更改幻灯片在轮播框架中滑入和滑出时的外观,截至 3.6 版本,您甚至可以创建自定义插值!
虽然您不仅限于使用此软件包显示图像,但我们将构建一个仅显示图像的轮播,以及一个标题,以保持配方简单。我们将使用优秀的免费许可照片网站unsplash.com通过托管在source.unsplash.com的 Unsplash Source 项目获取用于在我们的轮播中显示的随机图像。Unsplash Source 允许您轻松地从 Unsplash 请求随机图像,而无需访问官方 API。您可以访问 Unsplash Source 网站以获取有关其工作原理的更多信息。
准备工作
我们需要为这个配方创建一个新的应用程序。让我们把这个应用叫做“轮播”。
如何做...
- 我们将从打开
App.js并导入依赖项开始:
import React, { Component } from 'react';
import {
SafeAreaView,
StyleSheet,
Text,
View,
Image,
TouchableOpacity,
Picker,
Dimensions,
} from 'react-native';
import Carousel from 'react-native-snap-carousel';
- 接下来,让我们定义
App类和初始state对象。state有三个属性:一个布尔值,用于指示我们当前是否正在显示轮播图,一个layoutType属性,用于设置我们轮播图的布局样式,以及一个我们稍后将用于从 Unsplash Source 获取图像的imageSearchTerms数组。请随意更改imageSearchTerms数组:
export default class App extends React.Component {
state = {
showCarousel: false,
layoutType: 'default',
imageSearchTerms: [
'Books',
'Code',
'Nature',
'Cats',
]
}
// Defined in following steps
}
- 接下来,让我们定义
render方法。我们只需检查this.state.showCorousel的值,然后相应地显示轮播图或控件:
render() {
return (
<SafeAreaView style={styles.container}>
{this.state.showCarousel ?
this.renderCarousel() :
this.renderControls()
}
</SafeAreaView>
);
}
- 接下来,让我们创建
renderControls函数。这将是用户在首次打开应用程序时看到的布局,包括用于在轮播图中选择布局类型的 React NativePicker和用于打开轮播图的按钮。Picker有三个可用选项:默认、tinder 和 stack:
renderControls = () => {
return(
<View style={styles.container}>
<Picker
selectedValue={this.state.layoutType}
style={styles.picker}
onValueChange={this.updateLayoutType}
>
<Picker.Item label="Default" value="default" />
<Picker.Item label="Tinder" value="tinder" />
<Picker.Item label="Stack" value="stack" />
</Picker>
<TouchableOpacity
onPress={this.toggleCarousel}
style={styles.openButton}
>
<Text style={styles.openButtonText}>Open Carousel</Text>
</TouchableOpacity>
</View>
)
}
- 让我们定义
toggleCarousel函数。该函数只是将state上的showCarousel的值设置为其相反值。通过定义一个切换函数,我们可以使用相同的函数来打开和关闭轮播图:
toggleCarousel = () => {
this.setState({
showCarousel: !this.state.showCarousel
});
}
- 类似地,
updateLayoutType方法只是更新state上的layoutType到从Picker组件传入的layoutType值:
updateLayoutType = (layoutType) => {
this.setState({
layoutType
});
}
renderCarousel函数返回轮播图的标记。它由一个用于关闭轮播图的按钮和Carousel组件本身组成。该组件接受一个layout属性,由Picker设置。它还有一个data属性,用于接收应该循环播放每个轮播幻灯片的数据,以及一个renderItem回调函数,用于处理每个单独幻灯片的渲染:
renderCarousel = () => {
return(
<View style={styles.carouselContainer}>
<View style={styles.closeButtonContainer}>
<TouchableOpacity
onPress={this.toggleCarousel}
style={styles.button}
>
<Text style={styles.label}>x</Text>
</TouchableOpacity>
</View>
<Carousel
layout={this.state.layoutType}
data={this.state.imageSearchTerms}
renderItem={this.renderItem}
sliderWidth={350}
itemWidth={350}
>
</Carousel>
</View>
);
}
- 我们仍然需要处理每个幻灯片的渲染的函数。该函数接收一个对象参数,其中包含传递给
data属性的数组中的下一个项目。我们将返回一个使用item参数值从 Unsplash Source 获取350x350大小的随机项目的Image组件。我们还将添加一个Text元素来显示正在显示的图像类型:
renderItem = ({item}) => {
return (
<View style={styles.slide}>
<Image
style={styles.image}
source={{ uri: `https://source.unsplash.com/350x350/?
${item}`}}
/>
<Text style={styles.label}>{item}</Text>
</View>
);
}
- 我们需要的最后一件事是一些样式来布局我们的 UI。
container样式适用于主要包装SafeAreaView元素,因此我们将justifyContent设置为'space-evenly',以便Picker和TouchableOpacity组件填满屏幕。为了在屏幕右上角显示关闭按钮,我们将flexDirection: 'row和justifyContent: 'flex-end'应用于包装元素。其余的样式只是尺寸、颜色、填充、边距和字体大小:
const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'column',
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'space-evenly',
},
carouselContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#474747'
},
closeButtonContainer: {
width: 350,
flexDirection: 'row',
justifyContent: 'flex-end'
},
slide: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
image: {
width:350,
height: 350,
},
label: {
fontSize: 30,
padding: 40,
color: '#fff',
backgroundColor: '#474747'
},
openButton: {
padding: 10,
backgroundColor: '#000'
},
openButtonText: {
fontSize: 20,
padding: 20,
color: '#fff',
},
closeButton: {
padding: 10
},
picker: {
height: 150,
width: 100,
backgroundColor: '#fff'
}
});
- 我们已经完成了我们的轮播应用程序。它可能不会赢得任何设计奖,但它是一个具有流畅、本地感觉行为的工作轮播应用程序:
它是如何工作的...
在步骤4 中,我们定义了renderControls函数,该函数在应用程序首次启动时呈现 UI。这是我们第一次使用Picker组件的示例。它是 React Native 核心库的一部分,并提供下拉类型选择器,用于在许多应用程序中选择选项。selectedValue属性是与选择器中当前选定项目绑定的值。通过将其设置为this.state.layoutType,我们将默认选择为“默认”布局,并在选择不同的Picker项目时保持值同步。选择器中的每个项目都由Picker.Item组件表示。其label属性定义了项目的显示文本,value属性表示项目的字符串值。由于我们将onValueChange属性与updateLayoutType函数一起使用,每当选择新项目时都会调用它,从而相应地更新this.state.layoutType。
在步骤7 中,我们定义了轮播图的 JSX。轮播图的data和renderItem属性是必需的,并且一起工作以呈现轮播图中的每个幻灯片。当实例化轮播图时,传递到data属性的数组将被循环处理,并且renderItem回调函数将针对区域中的每个项目调用,该项目作为参数传递到renderItem中。我们还设置了sliderWidth和itemWidth属性,这些属性对于水平轮播图是必需的。
在步骤 8中,我们定义了renderItem函数,该函数对传递到data中的数组中的每个条目调用。我们将返回的Image组件的源设置为 Unsplash 源 URL,该 URL 将返回所请求类型的随机图像。
还有更多...
有一些事情我们可以做来改进这个配方。我们可以利用Image.prefetch()方法在打开轮播图之前下载第一张图片,这样图片就可以立即准备好,或者添加一个输入框,允许用户选择自己的图片搜索词。
react-native-snap-carousel 包为 React Native 应用程序提供了一个很好的构建多媒体轮播图的方式。我们在这里没有时间涵盖的一些功能包括视差图片和自定义分页。对于有冒险精神的开发人员,该包提供了一种创建自定义插值的方式,使您可以创建超出三种内置布局之外的自定义布局。
将推送通知添加到您的应用程序
推送通知是提供应用程序和用户之间持续反馈循环的一种很好的方式,不断提供与用户相关的应用程序特定数据。消息应用程序在有新消息到达时发送通知。提醒应用程序显示通知以提醒用户在特定时间或位置执行任务。播客应用程序可以使用通知通知用户新的一集已经发布。购物应用程序可以使用通知提醒用户查看限时优惠。
推送通知是增加用户互动和留存的一种有效方式。如果您的应用程序使用与时间敏感或基于事件的数据,推送通知可能是一项有价值的资产。在这个配方中,我们将使用 Expo 的推送通知实现,它简化了一些在原生 React Native 项目中所需的设置。如果您的应用程序需要非 Expo 项目,我建议考虑使用 react-native-push-notification 包 github.com/zo0r/react-native-push-notification。
在这个配方中,我们将制作一个非常简单的消息应用程序,并添加推送通知。我们将请求适当的权限,然后将推送通知令牌注册到我们将构建的 Express 服务器上。我们还将渲染一个TextInput,让用户输入消息。当用户按下发送按钮时,消息将被发送到我们的服务器,服务器将通过 Expo 的推送通知服务器向所有已在我们的 Express 服务器上注册令牌的设备发送来自应用程序的消息的推送通知。
由于 Expo 内置的推送通知服务,为每个本机设备创建通知的复杂工作被转移到了 Expo 托管的后端。我们在这个教程中构建的 Express 服务器只会将每个推送通知的 JSON 对象传递给 Expo 后端,其余工作都会被处理。Expo 文档中的以下图表(docs.expo.io/versions/latest/guides/push-notifications)说明了推送通知的生命周期:
图片来源:
docs.expo.io/versions/latest/guides/push-notifications/
虽然使用 Expo 实现推送通知比起其他方式少了一些设置工作,但技术的要求仍然意味着我们需要运行一个服务器来处理注册和发送通知,这意味着这个教程会比大多数教程长一些。让我们开始吧!
准备工作
在这个应用程序中,我们需要做的第一件事是请求设备允许使用推送通知。不幸的是,推送通知权限在模拟器中无法正常工作,因此需要一个真实设备来测试这个应用程序。
我们还需要能够从本地主机之外的地址访问推送通知服务器。在真实环境中,推送通知服务器已经有一个公共 URL,但在开发环境中,最简单的解决方案是创建一个隧道,将开发推送通知服务器暴露给互联网。为此目的,我们将使用 ngrok 工具,因为它是一个成熟、强大且非常易于使用的解决方案。您可以在ngrok.com了解更多关于该软件的信息。
首先,使用以下命令通过npm全局安装ngrok:
npm i -g ngrok
安装完成后,您可以通过使用ngrok和https参数在互联网和本地机器上的端口之间创建隧道:
ngrok https [port-to-expose]
我们将在本教程中稍后使用这个命令来暴露开发服务器。
让我们为这个教程创建一个新的应用程序。我们将其命名为push-notifications。我们将需要三个额外的 npm 包来完成这个教程:express用于推送通知服务器,esm用于在服务器上使用 ES6 语法支持,expo-server-sdk用于处理推送通知。使用yarn安装它们:
yarn add express esm expo-server-sdk
或者,使用npm安装它们:
npm install express esm expo-server-sdk --save
如何做...
- 让我们从构建
App开始。我们将通过向App.js添加我们需要的依赖项来开始:
import React from 'react';
import {
StyleSheet,
Text,
View,
TextInput,
TouchableOpacity
} from 'react-native';
import { Permissions, Notifications } from 'expo';
- 我们将在服务器上声明两个 API 端点的常量,但是
url将在教程后面运行服务器时由ngrok生成,因此我们将在那时更新这些常量的值:
const PUSH_REGISTRATION_ENDPOINT = 'http://generated-ngrok-url/token';
const MESSAGE_ENPOINT = 'http://generated-ngrok-url/message';
- 让我们创建
App组件并初始化state对象。我们需要一个notification属性来保存Notifications侦听器接收到的通知,我们将在后面的步骤中定义:
export default class App extends React.Component {
state = {
notification: null,
messageText: ''
}
// Defined in following steps
}
- 让我们定义一个方法来处理将推送通知令牌注册到服务器。我们将通过
Permissions组件上的askAsync方法向用户请求通知权限。如果获得了权限,就从Notifications组件的getExpoPushTokenAsync方法获取设备上的令牌:
registerForPushNotificationsAsync = async () => {
const { status } = await Permissions.askAsync(Permissions.NOTIFICATIONS);
if (status !== 'granted') {
return;
}
let token = await Notifications.getExpoPushTokenAsync();
// Defined in following steps
}
- 一旦我们获得了适当的令牌,我们将将其发送到推送通知服务器进行注册。然后,我们将向
PUSH_REGISTRATION_ENDPOINT发出POST请求,发送token对象和user对象到请求体中。我已经在用户对象中硬编码了值,但在真实应用中,这将是您为当前用户存储的元数据:
registerForPushNotificationsAsync = async () => {
// Defined in above step
fetch(PUSH_REGISTRATION_ENDPOINT, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
token: {
value: token,
},
user: {
username: 'warly',
name: 'Dan Ward'
},
}),
});
// Defined in next step
}
- 注册令牌后,我们将设置一个事件侦听器来监听应用程序在打开和前台运行时发生的任何通知。在某些情况下,我们需要手动处理来自传入推送通知的信息显示。查看本教程末尾的*工作原理...*部分,了解为什么需要这样做以及如何利用它。我们将在下一步中定义处理程序:
registerForPushNotificationsAsync = async () => {
// Defined in above steps
this.notificationSubscription =
Notifications.addListener(this.handleNotification);
}
- 每当收到新通知时,
handleNotification方法将被运行。我们将只是将传递给此回调的新通知存储在state对象中,以便稍后在render函数中使用:
handleNotification = (notification) => {
this.setState({ notification });
}
- 我们希望我们的应用程序在启动时请求使用推送通知的权限,并注册推送通知令牌。我们将利用
componentDidMount生命周期钩子来运行我们的registerForPushNotificationsAsync方法:
componentDidMount() {
this.registerForPushNotificationsAsync();
}
- UI 将非常简单,以保持教程简单。它由一个用于消息文本的
TextInput,一个用于发送消息的发送按钮,以及一个用于显示通知的View组成:
render() {
return (
<View style={styles.container}>
<TextInput
value={this.state.messageText}
onChangeText={this.handleChangeText}
style={styles.textInput}
/>
<TouchableOpacity
style={styles.button}
onPress={this.sendMessage}
>
<Text style={styles.buttonText}>Send</Text>
</TouchableOpacity>
{this.state.notification ?
this.renderNotification()
: null}
</View>
);
}
- 在上一步中定义的
TextInput组件缺少其onChangeText属性所需的方法。让我们接下来创建这个方法。它只是将用户输入的文本保存到this.state.messageText中,以便可以被value属性和其他地方使用。
handleChangeText = (text) => {
this.setState({ messageText: text });
}
TouchableOpacity组件的onPress属性调用sendMessage方法,在用户按下按钮时发送消息文本。在这个函数中,我们将获取消息文本并将其POST到我们推送通知服务器上的MESSAGE_ENDPOINT。服务器将处理后续操作。消息发送后,我们将清除state中的messageText属性。
sendMessage = async () => {
fetch(MESSAGE_ENPOINT, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: this.state.messageText,
}),
});
this.setState({ messageText: '' });
}
App所需的最后一部分是样式。这些样式很简单,现在应该都很熟悉了。
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#474747',
alignItems: 'center',
justifyContent: 'center',
},
textInput: {
height: 50,
width: 300,
borderColor: '#f6f6f6',
borderWidth: 1,
backgroundColor: '#fff',
padding: 10
},
button: {
padding: 10
},
buttonText: {
fontSize: 18,
color: '#fff'
},
label: {
fontSize: 18
}
});
- React Native 应用程序部分完成后,让我们继续进行服务器部分。首先,在项目的根目录中创建一个新的
server文件夹,并在其中创建一个index.js文件。让我们首先导入express来运行服务器,以及expo-server-sdk来处理注册和发送推送通知。我们将创建一个 Express 服务器应用并将其存储在app常量中,以及一个 Expo 服务器 SDK 的新实例存储在expo常量中。我们还将添加一个savedPushTokens数组来存储在 React Native 应用中注册的任何令牌,以及一个PORT_NUMBER常量来指定服务器要运行的端口号。
import express from 'express';
import Expo from 'expo-server-sdk';
const app = express();
const expo = new Expo();
let savedPushTokens = [];
const PORT_NUMBER = 3000;
- 我们的服务器需要公开两个端点(一个用于注册令牌,一个用于接受来自 React Native 应用的消息),因此我们将创建两个函数,当命中这些路由时将执行这些函数。首先我们将定义
saveToken函数。它只是获取一个令牌,检查它是否存储在savedPushTokens数组中,如果尚未存储,则将其推送到数组中。
const saveToken = (token) => {
if (savedPushTokens.indexOf(token === -1)) {
savedPushTokens.push(token);
}
}
- 我们服务器需要的另一个函数是在接收来自 React Native 应用的消息时发送推送通知的处理程序。我们将遍历所有保存在
savedPushTokens数组中的令牌,并为每个令牌创建一个消息对象。每个消息对象的标题为收到消息!,这将以粗体显示在推送通知中,消息文本作为通知的正文。
const handlePushTokens = (message) => {
let notifications = [];
for (let pushToken of savedPushTokens) {
if (!Expo.isExpoPushToken(pushToken)) {
console.error(`Push token ${pushToken} is not a valid Expo push token`);
continue;
}
notifications.push({
to: pushToken,
sound: 'default',
title: 'Message received!',
body: message,
data: { message }
})
}
// Defined in following step
}
- 一旦我们有了消息数组,我们可以将它们发送到 Expo 的服务器,然后 Expo 的服务器将把推送通知发送到所有注册设备。我们将通过 expo 服务器的
chunkPushNotifications和sendPushNotificationsAsync方法发送消息数组,并根据情况将成功收据或错误记录到服务器控制台上。关于这个工作原理的更多信息,请参阅本教程末尾的*工作原理...*部分:
const handlePushTokens = (message) => {
// Defined in previous step
let chunks = expo.chunkPushNotifications(notifications);
(async () => {
for (let chunk of chunks) {
try {
let receipts = await expo.sendPushNotificationsAsync(chunk);
console.log(receipts);
} catch (error) {
console.error(error);
}
}
})();
}
- 现在我们已经定义了处理推送通知和消息的函数,让我们通过创建 API 端点来公开这些函数。如果您对 Express 不熟悉,它是一个在 Node 中运行 Web 服务器的强大且易于使用的框架。您可以通过基本路由文档快速了解基本路由的基础知识:
expressjs.com/en/starter/basic-routing.html。
我们将使用 JSON 数据,因此第一步将是使用express.json()调用应用 JSON 解析器中间件:
app.use(express.json());
- 尽管我们实际上不会使用服务器的根路径(
/),但定义一个是个好习惯。我们将只回复一条消息,表示服务器正在运行:
app.get('/', (req, res) => {
res.send('Push Notification Server Running');
});
- 首先,让我们实现保存推送通知令牌的端点。当向
/token端点发送POST请求时,我们将把令牌值传递给saveToken函数,并返回一个声明已收到令牌的响应:
app.post('/token', (req, res) => {
saveToken(req.body.token.value);
console.log(`Received push token, ${req.body.token.value}`);
res.send(`Received push token, ${req.body.token.value}`);
});
- 同样,
/message端点将从请求体中获取message并将其传递给handlePushTokens函数进行处理。然后,我们将发送一个响应,表示已收到消息:
app.post('/message', (req, res) => {
handlePushTokens(req.body.message);
console.log(`Received message, ${req.body.message}`);
res.send(`Received message, ${req.body.message}`);
});
- 服务器的最后一部分是对服务器实例调用 Express 的
listen方法,这将启动服务器:
app.listen(PORT_NUMBER, () => {
console.log('Server Online on Port ${PORT_NUMBER}');
});
- 我们需要一种启动服务器的方法,因此我们将在
package.json文件中添加一个名为 serve 的自定义脚本。打开package.json文件并更新它,使其具有一个新的serve脚本的 scripts 对象。添加了这个之后,我们可以通过yarn run serve命令或npm run serve命令使用 yarn 运行服务器或使用 npm 运行服务器。package.json文件应该看起来像这样:
{
"main": "node_modules/expo/AppEntry.js",
"private": true,
"dependencies": {
"esm": "³.0.28",
"expo": "²⁷.0.1",
"expo-server-sdk": "².3.3",
"express": "⁴.16.3",
"react": "16.3.1",
"react-native": "https://github.com/expo/react-native/archive/sdk-27.0.0.tar.gz"
},
"scripts": {
"serve": "node -r esm server/index.js"
}
}
- 我们已经把所有的代码放在了一起,让我们来使用它吧!如前所述,推送通知权限在模拟器上无法正常工作,因此需要一个真实设备来测试推送通知功能。首先,我们将通过运行以下命令来启动我们新创建的服务器:
yarn run serve
npm run serve
应该会看到我们在步骤 21中定义的listen方法调用中定义的Server Online消息:
- 接下来,我们需要运行
ngrok来将我们的服务器暴露到互联网上。打开一个新的终端窗口,并使用以下命令创建一个ngrok隧道:
ngrok http 3000
您应该在终端中看到ngrok界面。这显示了ngrok生成的 URL。在这种情况下,ngrok正在将我的位于http://localhost:3000的服务器转发到 URLhttp://ddf558bd.ngrok.io。让我们复制该 URL:
- 您可以通过在浏览器中访问生成的 URL 来测试服务器是否正在运行并且可以从互联网访问。直接导航到此 URL 的行为与导航到
http://localhost:3000完全相同,这意味着我们在上一步中定义的GET端点应该运行。该函数返回Push Notification Server Running字符串,并应在浏览器中显示:
- 现在我们已经确认服务器正在运行,让我们更新 React Native 应用程序以使用正确的服务器 URL。在步骤 2中,我们添加了常量来保存我们的 API 端点,但是我们还没有正确的 URL。让我们更新这些 URL 以反映
ngrok生成的隧道 URL:
const PUSH_REGISTRATION_ENDPOINT = 'http://ddf558bd.ngrok.io/token';
const MESSAGE_ENPOINT = 'http://ddf558bd.ngrok.io/message';
- 如前所述,您需要在真实设备上运行此应用程序,以便权限请求能够正确工作。一旦您打开应用程序,设备应该会提示您是否要允许该应用程序发送通知:
- 一旦选择了“允许”,推送通知令牌将被发送到服务器的
/token端点以进行保存。这也应该在服务器终端中打印出相关的console.log语句与保存的令牌。在这种情况下,我的 iPhone 的推送令牌是字符串。
ExponentPushToken[g5sIEbOm2yFdzn5VdSSy9n]:
-
此时,如果您有第二个 Android 或 iOS 设备,请继续在该设备上打开 React Native 应用程序。如果没有,不用担心。还有另一种简单的方法可以测试我们的推送通知功能是否正常工作,而无需使用第二个设备。
-
您可以使用 React Native 应用程序的文本输入向其他注册设备发送消息。如果您有第二个已向服务器注册令牌的设备,它应该会收到与新发送的消息相对应的推送通知。您还应该在服务器上看到两个新的
console.log实例:一个显示接收到的消息,另一个显示从 Expo 服务器返回的receipts数组。数组中的每个 receipt 对象都将具有一个status属性,如果操作成功,则该属性的值为'ok':
- 如果您没有第二个设备进行测试,可以使用 Expo 的推送通知工具,托管在
expo.io/dashboard/notifications。只需从服务器终端复制push token并将其粘贴到标有 EXPO PUSH TOKEN(来自您的应用程序)的输入中。要模拟从我们的 React Native 应用程序发送的消息,请将 MESSAGE TITLE 设置为Message received!,将 MESSAGE BODY 设置为您想要发送的消息文本,并选中 Play Sound 复选框。如果愿意,还可以通过提供具有"message"键和您的消息文本值的 JSON 对象来模拟data对象,例如{ "message": "This is a test message." }。然后接收到的消息应该看起来像这个屏幕截图:
它是如何工作的...
我们在这里构建的配方有点牵强,但请求权限、注册令牌、接受应用程序数据以及响应应用程序数据发送推送通知所需的核心概念都在这里。
在步骤 4中,我们定义了registerForPushNotificationsAsync函数的第一部分。我们首先通过Permissions.askAsync方法询问用户是否允许我们通过Permissions.NOTIFICATIONS常量发送通知。然后我们保存了解析后的return对象的status属性,如果用户授予权限,则该属性的值将为'granted'。如果我们没有获得权限,我们将立即return;否则,我们通过调用getExpoPushTokenAsync从 Expo 的Notifications组件中获取令牌。此函数返回一个令牌字符串,格式如下:
ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]
在步骤 5中,我们定义了对服务器注册端点(/token)的POST调用。此函数将令牌发送到请求正文中,然后使用在步骤 14中定义的saveToken函数在服务器上保存。
在步骤 6中,我们创建了一个事件监听器,用于监听任何新的推送通知。这是通过调用Notifications.addListener并传入一个回调函数来实现的,每次接收到新通知时都会执行该函数。在 iOS 设备上,系统设计为仅在发送推送通知的应用程序未打开并处于前台时才产生推送通知。这意味着如果您尝试在用户当前使用您的应用程序时发送推送通知,他们将永远不会收到。
为了解决这个问题,Expo 建议手动在应用程序内显示推送通知数据。Notifications.addListener方法就是为了满足这个需求而创建的。当接收到推送通知时,传递给addListener的回调将被执行,并将新的通知对象作为参数接收。在步骤 7中,我们将此通知保存到state中,以便相应地重新渲染 UI。在本教程中,我们只在Text组件中显示了消息文本,但您也可以使用模态框进行更类似通知的呈现。
在步骤 11中,我们创建了sendMessage函数,该函数将存储在state中的消息文本发布到服务器的/message端点。这将执行在步骤 15中定义的handlePushToken服务器函数。
在步骤 13中,我们开始在服务器上使用 Express 和 Expo 服务器 SDK。通过直接调用express()创建一个新的服务器,通常按惯例将其命名为app。我们能够使用new Expo()创建一个新的 Expo 服务器 SDK 实例,并将其存储在expo常量中。稍后我们使用 Expo 服务器 SDK 使用expo发送推送通知,在步骤 17到步骤 20中使用app定义路由,并在步骤 22中通过调用app.listen()启动服务器。
在步骤 14中,我们定义了saveToken函数,当 React Native 应用程序使用/token端点注册令牌时将执行该函数。此函数将传入的令牌保存到savedPushTokens数组中,以便稍后在用户发送消息时使用。在真实的应用程序中,这通常是您希望将令牌保存到持久性数据库(如 SQL、MongoDB 或 Firebase 数据库)的地方。
在步骤 15中,我们开始定义handlePushTokens函数,当 React Native 应用程序使用/message端点时运行。该函数循环处理savedPushTokens数组。使用 Expo 服务器 SDK 的isExpoPushToken方法检查每个令牌的有效性,该方法接受一个令牌并返回true如果令牌有效。如果无效,我们将在服务器控制台上记录错误。如果有效,我们将在下一步的批处理中将新的通知对象推送到本地notifications数组中。每个通知对象都需要一个to属性,其值设置为有效的 Expo 推送令牌。所有其他属性都是可选的。我们设置的可选属性如下:
-
声音:可以默认播放默认通知声音,或者对于无声音为
null -
标题:推送通知的标题,通常以粗体显示
-
正文:推送通知的正文
-
数据:自定义数据 JSON 对象
在步骤 16中,我们使用 Expo 服务器 SDK 的chunkPushNotifications实例方法创建了一个优化发送到 Expo 推送通知服务器的数据块数组。然后我们循环遍历这些块,并通过expo.sendPushNotificationsAsync方法将每个块发送到 Expo 的推送通知服务器。它返回一个解析为每个推送通知的收据数组的 promise。如果过程成功,数组中将有一个{ status: 'ok' }对象。
这个端点的行为比真实服务器可能要简单,因为大多数消息应用程序处理消息的方式可能更复杂。至少,可能会有一个接收者列表,指定注册设备将接收特定推送通知。逻辑被故意保持简单,以描绘基本流程。
在步骤 18中,我们在服务器上定义了第一个可访问的路由,即根(/)路径。Express 提供了get和post辅助方法,用于轻松地创建GET和POST请求的 API 端点。回调函数接收请求对象和响应对象作为参数。所有服务器 URL 都需要响应请求;否则,请求将超时。响应通过响应对象上的send方法发送。这个路由不处理任何数据,所以我们只返回指示我们的服务器正在运行的字符串。
在步骤 19和步骤 20中,我们为/token和/message定义了POST端点,分别执行saveToken和handlePushTokens。我们还在每个端点中添加了console.log语句,以便在服务器终端上记录令牌和消息,便于开发。
在步骤 21中,我们在 Express 服务器上定义了listen方法,启动了服务器。第一个参数是要监听请求的端口号,第二个参数是回调函数,通常用于在服务器终端上console.log一条消息,表示服务器已启动。
在步骤 22中,我们在项目的package.json文件中添加了一个自定义脚本。可以通过在package.json文件中添加一个scripts键,将可以在终端中运行的任何命令设置为自定义 npm 脚本,其键是自定义脚本的名称,值是运行该自定义脚本时应执行的命令。在这个示例中,我们定义了一个名为serve的自定义脚本,运行node -r esm server/index.js命令。这个命令使用我们在本示例开始时安装的esm npm 包在 Node 中运行我们的服务器文件(server/index.js)。自定义脚本可以使用npm执行:
npm run [custom-script-name]
也可以使用yarn执行:
yarn run [custom-script-name]
还有更多...
推送通知可能会很复杂,但幸运的是,Expo 以多种方式简化了这个过程。Expo 的推送通知服务有很好的文档,涵盖了通知定时、其他语言中的 Expo 服务器 SDK 以及如何通过 HTTP/2 实现通知的具体内容。我鼓励你在docs.expo.io/versions/latest/guides/push-notifications上阅读更多。
实现基于浏览器的身份验证
在第八章的使用 Facebook 登录示例中,我们将介绍使用 Expo 的Facebook组件创建登录工作流程,以提供用户的基本 Facebook 账户信息给我们的应用程序。Expo 还提供了一个Google组件,用于获取用户的 Google 账户信息的类似功能。但是,如果我们想要创建一个使用来自不同网站的账户信息的登录工作流程,我们该怎么办呢?在这种情况下,Expo 提供了AuthSession组件。
AuthSession 是建立在 Expo 的 WebBrowser 组件之上的,我们在第四章 实现复杂用户界面 - 第二部分 中已经使用过。典型的登录流程包括四个步骤:
-
用户启动登录流程
-
网页浏览器打开到登录页面
-
认证提供程序在成功登录时提供重定向
-
React Native 应用程序处理重定向
在这个应用程序中,我们将使用 Spotify API 通过用户登录来获取我们应用程序的 Spotify 账户信息。前往 beta.developer.spotify.com/dashboard/applications 创建一个新的 Spotify 开发者账户(如果你还没有),以及一个新的应用。应用可以取任何你喜欢的名字。创建完应用后,你会在应用信息中看到一个客户端 ID 字符串。在构建 React Native 应用程序时,我们将需要这个 ID。
准备就绪
我们需要一个新的应用程序来完成这个教程。让我们将应用命名为 browser-based-auth。
重定向 URI 也需要在之前创建的 Spotify 应用中列入白名单。重定向应该是 https://auth.expo.io/@YOUR_EXPO_USERNAME/YOUR_APP_SLUG 的形式。由于我的 Expo 用户名是 warlyware,并且由于我们正在构建的这个 React Native 应用程序名为 browser-based-auth,我的重定向 URI 是 https://auth.expo.io/@warlyware/browser-based-auth。请确保将其添加到 Spotify 应用的设置中的重定向 URI 列表中。
如何做...
- 我们将从打开
App.js并导入我们将要使用的依赖项开始。
import React, { Component } from 'react';
import { TouchableOpacity, StyleSheet, Text, View } from 'react-native';
import { AuthSession } from 'expo';
import { FontAwesome } from '@expo/vector-icons';
- 让我们也声明
CLIENT_ID为一个常量,以便以后使用。复制之前创建的 Spotify 应用的客户端 ID,以便我们可以将其保存在CLIENT_ID常量中:
const CLIENT_ID = Your-Spotify-App-Client-ID;
- 让我们创建
App类和初始state。userInfo属性将保存我们从 Spotify API 收到的用户信息,didError是一个布尔值,用于跟踪登录过程中是否发生错误:
export default class App extends React.Component {
state = {
userInfo: null,
didError: false
};
// Defined in following steps
}
- 接下来,让我们定义将用户登录到 Spotify 的方法。
AuthSession组件的getRedirectUrl方法提供了在登录后返回到 React Native 应用程序所需的重定向 URL,这是我们在本示例的准备就绪部分中保存在 Spotify 应用程序中的相同重定向 URI。 然后,我们将在登录请求中使用重定向 URL,我们将使用AuthSession.startAsync方法启动登录请求,传入一个选项对象,其中authUrl属性设置为用于授权用户数据的 Spotify 端点。 有关此 URL 的更多信息,请参阅本示例末尾的*它是如何工作...*部分:
handleSpotifyLogin = async () => {
let redirectUrl = AuthSession.getRedirectUrl();
let results = await AuthSession.startAsync({
authUrl:
`https://accounts.spotify.com/authorize?client_id=${CLIENT_ID}
&redirect_uri=${encodeURIComponent(redirectUrl)}
&scope=user-read-email&response_type=token`
});
// Defined in next step
};
- 我们将点击 Spotify 端点以进行用户身份验证的结果保存在本地
results变量中。 如果结果对象上的type属性返回的不是'success',那么就会发生错误,因此我们将相应地更新state的didError属性。 否则,我们将使用从授权接收到的访问令牌点击/me端点以获取用户信息,然后将其保存到this.state.userInfo中:
handleSpotifyLogin = async () => {
if (results.type !== 'success') {
this.setState({ didError: true });
} else {
const userInfo = await axios.get(`https://api.spotify.com/v1/me`, {
headers: {
"Authorization": `Bearer ${results.params.access_token}`
}
});
this.setState({ userInfo: userInfo.data });
}
};
- 现在
auth相关的方法已经定义,让我们创建render函数。 我们将使用FontAwesomeExpo 图标库来显示 Spotify 标志,添加一个按钮允许用户登录,并添加渲染错误或用户信息的方法,具体取决于this.state.didError的值。 一旦在state的userInfo属性上保存了数据,我们还将禁用登录按钮:
render() {
return (
<View style={styles.container}>
<FontAwesome
name="spotify"
color="#2FD566"
size={128}
/>
<TouchableOpacity
style={styles.button}
onPress={this.handleSpotifyLogin}
disabled={this.state.userInfo ? true : false}
>
<Text style={styles.buttonText}>
Login with Spotify
</Text>
</TouchableOpacity>
{this.state.didError ?
this.displayError() :
this.displayResults()
}
</View>
);
}
- 接下来,让我们定义处理错误的 JSX。 模板只是显示一个通用的错误消息,表示用户应该再试一次:
displayError = () => {
return (
<View style={styles.userInfo}>
<Text style={styles.errorText}>
There was an error, please try again.
</Text>
</View>
);
}
displayResults函数将是一个View组件,如果state中保存了userInfo,则显示用户的图像,用户名和电子邮件地址,否则它将提示用户登录:
displayResults = () => {
{ return this.state.userInfo ? (
<View style={styles.userInfo}>
<Image
style={styles.profileImage}
source={ {'uri': this.state.userInfo.images[0].url} }
/>
<View>
<Text style={styles.userInfoText}>
Username:
</Text>
<Text style={styles.userInfoText}>
{this.state.userInfo.id}
</Text>
<Text style={styles.userInfoText}>
Email:
</Text>
<Text style={styles.userInfoText}>
{this.state.userInfo.email}
</Text>
</View>
</View>
) : (
<View style={styles.userInfo}>
<Text style={styles.userInfoText}>
Login to Spotify to see user data.
</Text>
</View>
)}
}
- 这个示例的样式非常简单。 它使用了列式弹性布局,应用了 Spotify 的黑色和绿色配色方案,并添加了字体大小和边距:
const styles = StyleSheet.create({
container: {
flexDirection: 'column',
backgroundColor: '#000',
flex: 1,
alignItems: 'center',
justifyContent: 'space-evenly',
},
button: {
backgroundColor: '#2FD566',
padding: 20
},
buttonText: {
color: '#000',
fontSize: 20
},
userInfo: {
height: 250,
width: 200,
alignItems: 'center',
},
userInfoText: {
color: '#fff',
fontSize: 18
},
errorText: {
color: '#fff',
fontSize: 18
},
profileImage: {
height: 64,
width: 64,
marginBottom: 32
}
});
- 现在,如果我们查看应用程序,我们应该能够登录到 Spotify,并看到与用于登录的帐户关联的图像,用户名和电子邮件地址:
它是如何工作的...
在步骤 4中,我们创建了处理 Spotify 登录过程的方法。AuthSession.startAsync方法只需要一个authUrl,这是由 Spotify 开发者文档提供的。所需的四个部分是Client-ID,用于处理来自 Spotify 的响应的重定向 URI,指示应用程序请求的用户信息范围的scope参数,以及response_type参数为token。我们只需要用户的基本信息,因此我们请求了user-read-email的范围类型。有关所有可用范围的信息,请查看beta.developer.spotify.com/documentation/general/guides/scopes/上的文档。
在步骤 5中,我们完成了 Spotify 登录处理程序。如果登录不成功,我们相应地更新了state上的didError。如果成功,我们使用该响应访问 Spotify API 端点以获取用户数据(api.spotify.com/v1/me)。我们根据 Spotify 的文档,使用Bearer ${results.params.access_token}定义了GET请求的Authorization标头来验证请求。在此请求成功后,我们将返回的用户数据存储在userInfo state对象中,这将重新呈现 UI 并显示用户信息。
深入了解 Spotify 的认证过程,您可以在beta.developer.spotify.com/documentation/general/guides/authorization-guide/找到指南。
另请参阅
-
Expo
MapView文档:docs.expo.io/versions/latest/sdk/map-view -
Airbnb 的 React Native Maps 包:
github.com/react-community/react-native-maps -
Expo 音频文档:
docs.expo.io/versions/latest/sdk/audio -
React Native Image Prefetch 文档:
facebook.github.io/react-native/docs/image.html#prefetch -
React Native Snap Carousel 自定义插值文档:
github.com/archriss/react-native-snap-carousel/blob/master/doc/CUSTOM_INTERPOLATIONS.md -
Expo 推送通知文档:
docs.expo.io/versions/latest/guides/push-notifications -
Express 基本路由指南:
expressjs.com/en/starter/basic-routing.html -
esm 软件包:
github.com/standard-things/esm -
用于 Node 的 Expo 服务器 SDK:
github.com/expo/exponent-server-sdk-node -
ngrok 软件包:
github.com/inconshreveable/ngrok
第六章:向您的应用程序添加基本动画
在本章中,我们将涵盖以下教程:
-
创建简单动画
-
运行多个动画
-
创建动画通知
-
展开和折叠容器
-
创建带有加载动画的按钮
介绍
为了提供良好的用户体验,我们可能希望添加一些动画来引导用户的注意力,突出特定的操作,或者只是为我们的应用程序增添独特的风格。
正在进行一个倡议,将所有处理从 JavaScript 移至本地端。在撰写本文时(React Native 版本 0.58),我们可以选择使用本地驱动程序在本地世界中运行所有这些计算。不幸的是,这不能用于所有动画,特别是与布局相关的动画,比如 flexbox 属性。在文档中阅读有关使用本地动画时的注意事项的更多信息facebook.github.io/react-native/docs/animations#caveats。
本章中的所有教程都使用 JavaScript 实现。React Native 团队承诺在将所有处理移至本地端时使用相同的 API,因此我们不需要担心现有 API 的变化。
创建简单动画
在这个教程中,我们将学习动画的基础知识。我们将使用一张图片来创建一个简单的线性移动,从屏幕的右侧移动到左侧。
准备工作
为了完成这个教程,我们需要创建一个空的应用程序。让我们称之为simple-animation。
我们将使用一个云的 PNG 图像来制作这个教程。您可以在 GitHub 上托管的教程存储库中找到该图像github.com/warlyware/react-native-cookbook/tree/master/chapter-6/simple-animation/assets/images。将图像放在/assets/images文件夹中以供应用程序使用。
如何做...
- 让我们从打开
App.js并导入App类的依赖项开始。Animated类将负责创建动画的值。它提供了一些准备好可以进行动画处理的组件,还提供了几种方法和辅助程序来运行平滑的动画。
Easing类提供了几种辅助方法,用于计算运动(如linear和quadratic)和预定义动画(如bounce、ease和elastic)。我们将使用Dimensions类来获取当前设备尺寸,以便在动画初始化时知道在哪里放置元素:
import React, { Component } from 'react';
import {
Animated,
Easing,
Dimensions,
StyleSheet,
View,
} from 'react-native';
- 我们还将初始化一些我们在应用程序中需要的常量。在这种情况下,我们将获取设备尺寸,设置图像的大小,并
require我们将要进行动画处理的图像:
const { width, height } = Dimensions.get('window');
const cloudImage = require('./assets/images/cloud.png');
const imageHeight = 200;
const imageWidth = 300;
- 现在,让我们创建
App组件。我们将使用组件生命周期系统中的两种方法。如果您对这个概念不熟悉,请查看相关的 React 文档(reactjs.cn/react/docs/component-specs.html)。这个页面还有一个关于生命周期钩子如何工作的非常好的教程:
export default class App extends Component {
componentWillMount() {
// Defined on step 4
}
componentDidMount() {
// Defined on step 7
}
startAnimation () {
// Defined on step 5
}
render() {
// Defined on step 6
}
}
const styles = StyleSheet.create({
// Defined on step 8
});
- 为了创建动画,我们需要定义一个标准值来驱动动画。
Animated.Value是一个处理每一帧动画值的类。我们需要在组件创建时创建这个类的实例。在这种情况下,我们使用componentWillMount方法,但我们也可以使用constructor或者属性的默认值:
componentWillMount() {
this.animatedValue = new Animated.Value();
}
- 一旦我们创建了动画值,我们就可以定义动画。我们还通过将
Animated.timing的start方法传递给一个箭头函数来创建一个循环,该箭头函数再次执行startAnimation函数。现在,当图像达到动画的末尾时,我们将再次开始相同的动画,以创建一个无限循环的动画:
startAnimation() {
this.animatedValue.setValue(width);
Animated.timing(
this.animatedValue,
{
toValue: -imageWidth,
duration: 6000,
easing: Easing.linear,
useNativeDriver: true,
}
).start(() => this.startAnimation());
}
- 我们已经完成了动画,但目前只是计算了每一帧的值,没有对这些值做任何操作。下一步是在屏幕上渲染图像,并设置我们想要动画的样式属性。在这种情况下,我们想要在x轴上移动元素;因此,我们应该更新
left属性:
render() {
return (
<View style={styles.background}>
<Animated.Image
style={[
styles.image,
{ left: this.animatedValue },
]}
source={cloudImage}
/>
</View>
);
}
- 如果我们刷新模拟器,我们将看到图像在屏幕上,但它还没有被动画处理。为了解决这个问题,我们需要调用
startAnimation方法。我们将在组件完全渲染后开始动画,使用componentDidMount生命周期钩子:
componentDidMount() {
this.startAnimation();
}
- 如果我们再次运行应用程序,我们将看到图像在屏幕顶部移动,就像我们想要的那样!作为最后一步,让我们为应用程序添加一些基本样式:
const styles = StyleSheet.create({
background: {
flex: 1,
backgroundColor: 'cyan',
},
image: {
height: imageHeight,
position: 'absolute',
top: height / 3,
width: imageWidth,
},
});
输出如下所示:
工作原理...
在步骤 5中,我们设置了动画数值。第一行每次调用此方法时都会重置初始值。在本例中,初始值将是设备的宽度,这将把图像移动到屏幕的右侧,这是我们想要开始动画的地方。
然后,我们使用Animated.timing函数基于时间创建动画,并传入两个参数。对于第一个参数,我们传入了在步骤 4中的componentWillMount生命周期钩子中创建的animatedValue。第二个参数是一个包含动画配置的对象。在这种情况下,我们将把结束值设置为图像宽度的负值,这将把图像放在屏幕的左侧。动画在那里完成。
配置完毕后,Animated类将计算所需的所有帧,以在分配的 6 秒内执行从右向左的线性动画(通过将duration属性设置为6000毫秒)。
React Native 还提供了另一个与Animated配对使用的辅助工具,称为Easing。在这种情况下,我们使用Easing辅助类的linear属性。Easing提供其他常见的缓动方法,如elastic和bounce。查看Easing类文档,并尝试为easing属性设置不同的值,看看每个值的效果。您可以在facebook.github.io/react-native/docs/easing.html找到文档。
动画配置正确后,我们需要运行它。我们通过调用start方法来实现这一点。此方法接收一个可选的callback函数参数,当动画完成时将执行该函数。在这种情况下,我们递归运行相同的startAnimation函数。这将创建一个无限循环,这正是我们想要实现的。
在步骤 6中,我们正在渲染图像。如果要对图像进行动画处理,应始终使用Animate.Image组件。在内部,此组件将处理动画的值,并将为本机组件上的每个帧设置每个值。这避免了在每个帧上在 JavaScript 层上运行渲染方法,从而实现更流畅的动画。
除了Image之外,我们还可以对View、Text和ScrollView组件进行动画处理。这四个组件都有内置的支持,但我们也可以创建一个新组件,并通过Animated.createAnimatedComponent()添加动画支持。这四个组件都能处理样式更改。我们所要做的就是将animatedValue传递给我们想要动画的属性,这种情况下是left属性,但我们也可以在每个组件上使用任何可用的样式。
运行多个动画
在这个配方中,我们将学习如何在几个元素中使用相同的动画值。这样,我们可以重复使用相同的值,以及插值,为其余的元素获得不同的值。
这个动画将类似于上一个配方。这次,我们将有两朵云:一朵较小,移动较慢,另一朵较大,移动较快。在屏幕中央,我们将有一架静止的飞机。我们不会给飞机添加任何动画,但移动的云会使它看起来像飞机在移动。
准备就绪
让我们通过创建一个名为multiple-animations的空应用程序来开始这个配方。
我们将使用三种不同的图像:两个云和一架飞机。您可以从 GitHub 上的配方存储库下载图像,地址为github.com/warlyware/react-native-cookbook/tree/master/chapter-6/multiple-animations/assets/images。确保将图像放在/assets/images文件夹中。
如何做...
- 让我们从打开
App.js并添加我们的导入开始:
import React, { Component } from 'react';
import {
View,
Animated,
Image,
Easing,
Dimensions,
StyleSheet,
} from 'react-native';
- 此外,我们需要定义一些常量,并要求我们将用于动画的图像。请注意,我们将在这个配方中将相同的云图像视为
cloudImage1和cloudImage2,但我们将把它们视为单独的实体:
const { width, height } = Dimensions.get('window');
const cloudImage1 = require('./assets/images/cloud.png');
const cloudImage2 = require('./assets/images/cloud.png');
const planeImage = require('./assets/images/plane.gif');
const cloudHeight = 100;
const cloudWidth = 150;
const planeHeight = 60;
const planeWidth = 100;
- 在下一步中,当组件被创建时,我们将创建
animatedValue实例,然后在组件完全渲染时开始动画。我们正在创建一个在无限循环中运行的动画。初始值将为1,最终值将为0。如果您对这段代码不清楚,请确保阅读本章的第一个配方:
export default class App extends Component {
componentWillMount() {
this.animatedValue = new Animated.Value();
}
componentDidMount() {
this.startAnimation();
}
startAnimation () {
this.animatedValue.setValue(1);
Animated.timing(
this.animatedValue,
{
toValue: 0,
duration: 6000,
easing: Easing.linear,
}
).start(() => this.startAnimation());
}
render() {
// Defined in a later step
}
}
const styles = StyleSheet.create({
// Defined in a later step
});
- 在本示例中,
render方法将与上一个示例有很大不同。在本示例中,我们将使用相同的animatedValue来动画两个图像。动画值将返回从1到0的值;但是,我们希望将云从右向左移动,因此我们需要为每个元素设置left值。
为了设置正确的值,我们需要对animatedValue进行插值。对于较小的云,我们将把初始的left值设为设备的宽度,但对于较大的云,我们将把初始的left值设得远离设备的右边缘。这将使移动距离更大,因此移动速度会更快:
render() {
const left1 = this.animatedValue.interpolate({
inputRange: [0, 1],
outputRange: [-cloudWidth, width],
});
const left2 = this.animatedValue.interpolate({
inputRange: [0, 1],
outputRange: [-cloudWidth*5, width + cloudWidth*5],
});
// Defined in a later step
}
- 一旦我们有了正确的
left值,我们需要定义我们想要动画的元素。在这里,我们将把插值值设置为left样式属性:
render() {
// Defined in a later step
return (
<View style={styles.background}>
<Animated.Image
style={[
styles.cloud1,
{ left: left1 },
]}
source={cloudImage1}
/>
<Image
style={styles.plane}
source={planeImage}
/>
<Animated.Image
style={[
styles.cloud2,
{ left: left2 },
]}
source={cloudImage2}
/>
</View>
);
}
- 至于最后一步,我们需要定义一些样式,只需设置每朵云的
width和height以及为top分配样式即可。
const styles = StyleSheet.create({
background: {
flex: 1,
backgroundColor: 'cyan',
},
cloud1: {
position: 'absolute',
width: cloudWidth,
height: cloudHeight,
top: height / 3 - cloudWidth / 2,
},
cloud2: {
position: 'absolute',
width: cloudWidth * 1.5,
height: cloudHeight * 1.5,
top: height/2,
},
plane: {
position: 'absolute',
height: planeHeight,
width: planeWidth,
top: height / 2 - planeHeight,
left: width / 2 - planeWidth,
}
});
- 如果我们刷新应用,我们应该能看到动画:
工作原理...
在步骤 4中,我们定义了插值以获取每朵云的left值。interpolate方法接收一个具有两个必需配置的对象,inputRange和outputRange。
inputRange配置接收一个值数组。这些值应始终是升序值;您也可以使用负值,只要值是升序的。
outputRange应该与inputRange中定义的值的数量匹配。这些是我们需要作为插值结果的值。
对于本示例,inputRange从0到1,这些是我们的animatedValue的值。在outputRange中,我们定义了我们需要的移动的限制。
创建动画通知
在本示例中,我们将从头开始创建一个通知组件。在显示通知时,组件将从屏幕顶部滑入。几秒钟后,我们将自动隐藏它,将其滑出。
准备工作
我们将创建一个应用。让我们称之为notification-animation。
如何做...
- 我们将从
App组件开始工作。首先,让我们导入所有必需的依赖项:
import React, { Component } from 'react';
import {
Text,
TouchableOpacity,
StyleSheet,
View,
SafeAreaView,
} from 'react-native';
import Notification from './Notification';
- 一旦我们导入了所有依赖项,我们就可以定义
App类。在这种情况下,我们将使用notify属性等于false来初始化state。我们将使用此属性来显示或隐藏通知。默认情况下,通知不会显示在屏幕上。为了简化事情,我们将在state中定义message属性,其中包含我们想要显示的文本:
export default class App extends Component {
state = {
notify: false,
message: 'This is a notification!',
};
toggleNotification = () => {
// Defined on later step
}
render() {
// Defined on later step
}
}
const styles = StyleSheet.create({
// Defined on later step
});
- 在
render方法内,我们需要仅在notify属性为true时显示通知。我们可以通过使用if语句来实现这一点:
render() {
const notify = this.state.notify
? <Notification
autoHide
message={this.state.message}
onClose={this.toggleNotification}
/>
: null;
// Defined on next step
}
- 在上一步中,我们只定义了对
Notification组件的引用,但还没有使用它。让我们定义一个return,其中包含此应用程序所需的所有 JSX。为了保持简单,我们只会定义一个工具栏、一些文本和一个按钮,以在按下时切换通知的状态:
render() {
// Code from previous step
return (
<SafeAreaView>
<Text style={styles.toolbar}>Main toolbar</Text>
<View style={styles.content}>
<Text>
Lorem ipsum dolor sit amet, consectetur adipiscing
elit,
sed do eiusmod tempor incididunt ut labore et
dolore magna.
</Text>
<TouchableOpacity
onPress={this.toggleNotification}
style={styles.btn}
>
<Text style={styles.text}>Show notification</Text>
</TouchableOpacity>
<Text>
Sed ut perspiciatis unde omnis iste natus error sit
accusantium doloremque laudantium.
</Text>
{notify}
</View>
</SafeAreaView>
);
}
- 我们还需要定义一个方法,用于在
state上切换notify属性,这非常简单:
toggleNotification = () => {
this.setState({
notify: !this.state.notify,
});
}
- 我们几乎完成了这个类。剩下的只有样式。在这种情况下,我们只会添加基本样式,如
color、padding、fontSize、backgroundColor和margin,没有什么特别的:
const styles = StyleSheet.create({
toolbar: {
backgroundColor: '#8e44ad',
color: '#fff',
fontSize: 22,
padding: 20,
textAlign: 'center',
},
content: {
padding: 10,
overflow: 'hidden',
},
btn: {
margin: 10,
backgroundColor: '#9b59b6',
borderRadius: 3,
padding: 10,
},
text: {
textAlign: 'center',
color: '#fff',
},
});
- 如果我们尝试运行应用程序,我们会看到一个错误,即无法解析
./Notification模块。让我们通过定义Notification组件来解决这个问题。让我们创建一个Notifications文件夹,其中包含一个index.js文件。然后,我们可以导入我们的依赖项:
import React, { Componen } from 'react';
import {
Animated,
Easing,
StyleSheet,
Text,
} from 'react-native';
- 一旦我们导入了依赖项,让我们定义新组件的 props 和初始状态。我们将定义一些非常简单的东西,只是一个用于接收要显示的消息的属性,以及两个
callback函数,允许在通知出现在屏幕上和关闭时运行一些操作。我们还将添加一个属性来设置在自动隐藏通知之前显示通知的毫秒数:
export default class Notification extends Component {
static defaultProps = {
delay: 5000,
onClose: () => {},
onOpen: () => {},
};
state = {
height: -1000,
};
}
- 终于是时候开始处理动画了!我们需要在组件被渲染时立即开始动画。如果以下代码中有什么不清楚的地方,我建议你看一下本章的第一和第二个示例:
componentWillMount() {
this.animatedValue = new Animated.Value();
}
componentDidMount() {
this.startSlideIn();
}
getAnimation(value, autoHide) {
const { delay } = this.props;
return Animated.timing(
this.animatedValue,
{
toValue: value,
duration: 500,
easing: Easing.cubic,
delay: autoHide ? delay : 0,
}
);
}
- 到目前为止,我们已经定义了一个获取动画的方法。对于滑入运动,我们需要计算从
0到1的值。动画完成后,我们需要运行onOpen回调。如果autoHide属性在调用onOpen方法时设置为true,我们将自动运行滑出动画以删除组件:
startSlideIn () {
const { onOpen, autoHide } = this.props;
this.animatedValue.setValue(0);
this.getAnimation(1)
.start(() => {
onOpen();
if (autoHide){
this.startSlideOut();
}
});
}
- 与前面的步骤类似,我们需要一个用于滑出运动的方法。在这里,我们需要计算从
1到0的值。我们将autoHide值作为参数发送到getAnimation方法。这将自动延迟动画,延迟时间由delay属性定义(在我们的例子中为 5 秒)。动画完成后,我们需要运行onClose回调函数,这将从App类中删除组件:
startSlideOut() {
const { autoHide, onClose } = this.props;
this.animatedValue.setValue(1);
this.getAnimation(0, autoHide)
.start(() => onClose());
}
- 最后,让我们添加
render方法。在这里,我们将获取props提供的message值。我们还需要组件的height来将组件移动到动画的初始位置;默认情况下是-1000,但我们将在下一步在运行时设置正确的值。animatedValue从0到1或从1到0,取决于通知是打开还是关闭;因此,我们需要对其进行插值以获得实际值。动画将从组件的负高度到0;这将导致一个漂亮的滑入/滑出动画:
render() {
const { message } = this.props;
const { height } = this.state;
const top = this.animatedValue.interpolate({
inputRange: [0, 1],
outputRange: [-height, 0],
});
// Defined on next step
}
}
- 为了尽可能简单,我们将返回一个带有一些文本的
Animated.View。在这里,我们正在使用插值结果设置top样式,这意味着我们将对顶部样式进行动画处理。如前所述,我们需要在运行时计算组件的高度。为了实现这一点,我们需要使用视图的onLayout属性。此函数将在每次布局更新时调用,并将新的组件尺寸作为参数发送:
render() {
// Code from previous step
return (
<Animated.View
onLayout={this.onLayoutChange}
style={[
styles.main,
{ top }
]}
>
<Text style={styles.text}>{message}</Text>
</Animated.View>
);
}
}
onLayoutChange方法将非常简单。我们只需要获取新的height并更新state。此方法接收一个event。从这个对象中,我们可以获取有用的信息。对于我们的目的,我们将在event对象的nativeEvent.layout中访问数据。layout对象包含屏幕的width和height,以及Animated.View调用此函数时屏幕上的x和y位置:
onLayoutChange = (event) => {
const {layout: { height } } = event.nativeEvent;
this.setState({ height });
}
- 在最后一步,我们将为通知组件添加一些样式。由于我们希望该组件在任何其他内容之上进行动画,我们需要将
position设置为absolute,并将left和right属性设置为0。我们还将添加一些颜色和填充:
const styles = StyleSheet.create({
main: {
backgroundColor: 'rgba(0, 0, 0, 0.7)',
padding: 10,
position: 'absolute',
left: 0,
right: 0,
},
text: {
color: '#fff',
},
});
- 最终应用程序应该看起来像以下截图:
工作原理...
在步骤 3中,我们定义了Notification组件。该组件接收三个参数:一个标志,用于在几秒后自动隐藏组件,我们要显示的消息,以及在通知关闭时将执行的callback函数。
当onClose回调被执行时,我们将切换notify属性以移除Notification实例并清除内存。
在步骤 4中,我们定义了用于渲染应用程序组件的 JSX。重要的是要在其他组件之后渲染Notification组件,以便该组件显示在所有其他组件之上。
在步骤 6中,我们定义了组件的state。defaultProps对象为每个属性设置了默认值。如果给定属性没有赋值,这些值将被应用。
我们将每个callback的默认值定义为空函数。这样,我们在尝试执行它们之前不必检查这些 props 是否有值。
对于初始的state,我们定义了height属性。实际的height值将根据message属性中接收的内容在运行时计算。这意味着我们需要最初将组件远离原始位置进行渲染。由于在计算布局时存在短暂延迟,我们不希望在移动到正确位置之前显示通知。
在步骤 9中,我们创建了动画。getAnimation方法接收两个参数:要应用的delay和autoHide布尔值,用于确定通知是否自动关闭。我们在步骤 10和步骤 11中使用了这个方法。
在步骤 13中,我们为该组件定义了 JSX。onLayout函数在更新布局时非常有用,可以获取组件的尺寸。例如,如果设备方向发生变化,尺寸将发生变化,这种情况下,我们希望更新动画的初始和最终坐标。
还有更多...
当前的实现效果相当不错,但是我们应该解决一个性能问题。目前,onLayout方法在每一帧动画上都会被执行,这意味着我们在每一帧上都在更新state,这导致组件在每一帧上重新渲染!我们应该避免这种情况,只更新一次以获得实际的高度。
为了解决这个问题,我们可以添加一个简单的验证,只有在当前值与初始值不同时才更新状态。这将避免在每一帧上更新state,我们也不会一遍又一遍地强制渲染:
onLayoutChange = (event) => {
const {layout: { height } } = event.nativeEvent;
if (this.state.height === -1000) {
this.setState({ height });
}
}
虽然这对我们的目的有效,但我们也可以进一步确保在方向改变时height也会更新。然而,我们会在这里停下,因为这个方法已经相当长了。
展开和折叠容器
在这个方法中,我们将创建一个带有title和content的自定义容器元素。当用户按下标题时,内容将折叠或展开。这个方法将允许我们探索LayoutAnimation API。
做好准备
让我们从创建一个新的应用程序开始。我们将其称为collapsable-containers。
一旦我们创建了应用程序,让我们还创建一个Panel文件夹,里面有一个index.js文件,用于存放我们的Panel组件。
如何做...
- 让我们首先专注于
Panel组件。首先,我们需要导入我们将在这个类中使用的所有依赖项:
import React, { Component } from 'react';
import {
View,
LayoutAnimation,
StyleSheet,
Text,
TouchableOpacity,
} from 'react-native';
- 一旦我们有了依赖项,让我们声明
defaultProps来初始化这个组件。在这个方法中,我们只需要将expanded属性初始化为false:
export default class Panel extends Component {
static defaultProps = {
expanded: false
};
}
const styles = StyleSheet.create({
// Defined on later step
});
- 我们将使用
state对象上的height属性来展开或折叠容器。这个组件第一次被创建时,我们需要检查expanded属性,以设置正确的初始height:
state = {
height: this.props.expanded ? null : 0,
};
- 让我们为这个组件渲染所需的 JSX 元素。我们需要从
state中获取height的值,并将其设置为内容的样式视图。当按下title元素时,我们将执行toggle方法(稍后定义)来改变state的height值:
render() {
const { children, style, title } = this.props;
const { height } = this.state;
return (
<View style={[styles.main, style]}>
<TouchableOpacity onPress={this.toggle}>
<Text style={styles.title}>
{title}
</Text>
</TouchableOpacity>
<View style={{ height }}>
{children}
</View>
</View>
);
}
- 如前所述,当按下
title元素时,toggle方法将被执行。在这里,我们将在state上切换height并在下一个渲染周期更新样式时调用我们想要使用的动画:
toggle = () => {
LayoutAnimation.spring();
this.setState({
height: this.state.height === null ? 0 : null,
})
}
- 为了完成这个组件,让我们添加一些简单的样式。我们需要将
overflow设置为hidden,否则在组件折叠时内容将被显示出来。
const styles = StyleSheet.create({
main: {
backgroundColor: '#fff',
borderRadius: 3,
overflow: 'hidden',
paddingLeft: 30,
paddingRight: 30,
},
title: {
fontWeight: 'bold',
paddingTop: 15,
paddingBottom: 15,
}
- 一旦我们定义了
Panel组件,让我们在App类中使用它。首先,我们需要在App.js中要求所有的依赖项:
import React, { Component } from 'react';
import {
Text,
StyleSheet,
View,
SafeAreaView,
Platform,
UIManager
} from 'react-native';
import Panel from './Panel';
- 在上一步中,我们导入了
Panel组件。我们将在 JSX 中声明这个类的三个实例:
export default class App extends Component {
render() {
return (
<SafeAreaView style={[styles.main]}>
<Text style={styles.toolbar}>Animated containers</Text>
<View style={styles.content}>
<Panel
title={'Container 1'}
style={styles.panel}
>
<Text style={styles.panelText}>
Temporibus autem quibusdam et aut officiis
debitis aut rerum necessitatibus saepe
eveniet ut et voluptates repudiandae sint et
molestiae non recusandae.
</Text>
</Panel>
<Panel
title={'Container 2'}
style={styles.panel}
>
<Text style={styles.panelText}>
Et harum quidem rerum facilis est et expedita
distinctio. Nam libero tempore,
cum soluta nobis est eligendi optio cumque.
</Text>
</Panel>
<Panel
expanded
title={'Container 3'}
style={styles.panel}
>
<Text style={styles.panelText}>
Nullam lobortis eu lorem ut vulputate.
</Text>
<Text style={styles.panelText}>
Donec id elementum orci. Donec fringilla lobortis
ipsum, vitae commodo urna.
</Text>
</Panel>
</View>
</SafeAreaView>
);
}
}
- 在这个示例中,我们在 React Native 中使用了
LayoutAnimationAPI。在当前版本的 React Native 中,这个 API 在 Android 上默认是禁用的。在App组件挂载之前,我们将使用Platform助手和UIManager在 Android 设备上启用这个功能:
componentWillMount() {
if (Platform.OS === 'android') {
UIManager.setLayoutAnimationEnabledExperimental(true);
}
}
- 最后,让我们为工具栏和主容器添加一些样式。我们只需要一些你现在可能已经习惯的简单样式:
padding,margin和color。
const styles = StyleSheet.create({
main: {
flex: 1,
},
toolbar: {
backgroundColor: '#3498db',
color: '#fff',
fontSize: 22,
padding: 20,
textAlign: 'center',
},
content: {
padding: 10,
backgroundColor: '#ecf0f1',
flex: 1,
},
panel: {
marginBottom: 10,
},
panelText: {
paddingBottom: 15,
}
});
- 最终的应用程序应该类似于以下截图:
工作原理...
在步骤 3中,我们设置了内容的初始height。如果expanded属性设置为true,那么我们应该显示内容。通过将height值设置为null,布局系统将根据内容计算height;否则,我们需要将值设置为0,这将在组件折叠时隐藏内容。
在步骤 4中,我们为Panel组件定义了所有 JSX。这一步中有一些值得介绍的概念。首先,children属性是从props对象中传入的,当这个组件在App类中使用时,它将包含在<Panel>和</Panel>之间定义的任何元素。这非常有帮助,因为通过使用这个属性,我们允许这个组件接收任何其他组件作为子组件。
在同一步骤中,我们还从state对象中获取height并将其设置为应用于可折叠内容的View的style。这将更新height,导致组件相应地展开或折叠。我们还声明了onPress回调,当按下title元素时,它会切换state上的height。
在步骤 7中,我们定义了toggle方法,它可以切换height值。在这里,我们使用了LayoutAnimation类。通过调用spring方法,布局系统将在下一次渲染时对布局发生的每一次变化进行动画处理。在这种情况下,我们只改变了height,但我们也可以改变任何其他属性,比如opacity,position或color。
LayoutAnimation类包含一些预定义的动画。在这个示例中,我们使用了spring,但我们也可以使用linear或easeInEaseOut,或者使用configureNext方法创建自己的动画。
如果我们移除LayoutAnimation,我们将看不到动画;组件将通过从0到总高度跳跃来展开和折叠。但通过添加那一行代码,我们可以轻松地添加一个漂亮、平滑的动画。如果您需要更多对动画的控制,您可能会想使用动画 API。
在步骤 9中,我们在Platform助手上检查了 OS 属性,它返回了'android'或'ios'字符串,取决于应用程序运行在哪个设备上。如果应用程序在 Andriod 上运行,我们使用UIManager助手的setLayoutAnimationEnabledExperimental方法来启用LayoutAnimation API。
另请参阅
-
LayoutAnimationAPI 文档在facebook.github.io/react-native/docs/layoutanimation.html -
在
codeburst.io/a-quick-intro-to-reacts-props-children-cb3d2fce4891快速介绍 React 的props.children。
创建带有加载动画的按钮
在这个示例中,我们将继续使用LayoutAnimation类。在这里,我们将创建一个按钮,当用户按下按钮时,我们将显示一个加载指示器并动画化样式。
准备工作
要开始,我们需要创建一个空的应用程序。让我们称之为button-loading-animation。
让我们还创建一个Button文件夹,里面有一个index.js文件,用于我们的Button组件。
如何做...
- 让我们从
Button/index.js文件开始。首先,我们将导入这个组件所需的所有依赖项:
import React, { Component } from 'react';
import {
ActivityIndicator,
LayoutAnimation,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
- 对于这个组件,我们将只使用四个 props:一个
label,一个loading布尔值,用于切换显示加载指示器或按钮内的标签,一个在按钮被按下时执行的回调函数,以及自定义样式。在这里,我们将init默认的loading为false,并将handleButtonPress设置为空函数:
export default class Button extends Component {
static defaultProps = {
loading: false,
onPress: () => {},
};
// Defined on later steps
}
- 我们将尽可能简化这个组件的
render方法。我们将根据loading属性的值来渲染标签和活动指示器:
render() {
const { loading, style } = this.props;
return (
<TouchableOpacity
style={[
styles.main,
style,
loading ? styles.loading : null,
]}
activeOpacity={0.6}
onPress={this.handleButtonPress}
>
<View>
{this.renderLabel()}
{this.renderActivityIndicator()}
</View>
</TouchableOpacity>
);
}
- 为了渲染
label,我们需要检查loading属性是否为false。如果是,那么我们只返回一个带有从props接收到的label的Text元素:
renderLabel() {
const { label, loading } = this.props;
if(!loading) {
return (
<Text style={styles.label}>{label}</Text>
);
}
}
- 同样,
renderActivityIndicator指示器应该只在loading属性的值为true时应用。如果是这样,我们将返回ActivityIndicator组件。我们将使用ActivityIndicator的 props 来定义一个小的size和白色的color(#fff):
renderActivityIndicator() {
if (this.props.loading) {
return (
<ActivityIndicator size="small" color="#fff" />
);
}
}
- 我们的类中还缺少一个方法:
handleButtonPress。当按钮被按下时,我们需要通知这个组件的父组件,这可以通过调用通过props传递给这个组件的onPress回调来实现。我们还将使用LayoutAnimation在下一次渲染时排队一个动画:
handleButtonPress = () => {
const { loading, onPress } = this.props;
LayoutAnimation.easeInEaseOut();
onPress(!loading);
}
- 为了完成这个组件,我们需要添加一些样式。我们将定义一些颜色,圆角,对齐,填充等。对于显示加载指示器时将应用的
loading样式,我们将更新填充以创建一个围绕加载指示器的圆形:
const styles = StyleSheet.create({
main: {
backgroundColor: '#e67e22',
borderRadius: 20,
padding: 10,
paddingLeft: 50,
paddingRight: 50,
},
label: {
color: '#fff',
fontWeight: 'bold',
textAlign: 'center',
backgroundColor: 'transparent',
},
loading: {
padding: 10,
paddingLeft: 10,
paddingRight: 10,
},
});
- 我们已经完成了
Button组件。现在,让我们来处理App类。让我们首先导入所有的依赖项:
import React, { Component } from 'react';
import {
Text,
StyleSheet,
View,
SafeAreaView,
Platform,
UIManager
} from 'react-native';
import Button from './Button';
App类相对简单。我们只需要在state对象上定义一个loading属性,它将切换Button的动画。我们还将渲染一个toolbar和一个Button:
export default class App extends Component {
state = {
loading: false,
};
// Defined on next step
handleButtonPress = (loading) => {
this.setState({ loading });
}
render() {
const { loading } = this.state;
return (
<SafeAreaView style={[styles.main, android]}>
<Text style={styles.toolbar}>Animated containers</Text>
<View style={styles.content}>
<Button
label="Login"
loading={loading}
onPress={this.handleButtonPress}
/>
</View>
</SafeAreaView>
);
}
}
- 与上一个示例一样,我们需要在 Android 设备上手动启用
LayoutAnimationAPI:
componentWillMount() {
if (Platform.OS === 'android') {
UIManager.setLayoutAnimationEnabledExperimental(true);
}
}
- 最后,我们将添加一些
styles,只是一些颜色,填充和居中对齐按钮在屏幕上:
const styles = StyleSheet.create({
main: {
flex: 1,
},
toolbar: {
backgroundColor: '#f39c12',
color: '#fff',
fontSize: 22,
padding: 20,
textAlign: 'center',
},
content: {
padding: 10,
backgroundColor: '#ecf0f1',
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
});
- 最终的应用程序应该类似于以下截图:
工作原理...
在步骤 3中,我们为Button组件添加了render方法。在这里,我们接收了loading属性,并根据该值将相应的样式应用于TouchableOpacity按钮元素。我们还使用了两种方法:一种用于渲染标签,另一种用于渲染活动指示器。
在步骤 6中,我们执行了onPress回调。默认情况下,我们声明了一个空函数,因此我们不必检查值是否存在。
这个按钮的父组件应该负责在调用onPress回调时更新loading属性。从这个组件中,我们只负责在按下此按钮时通知父组件。
LayoutAnimation.eadeInEaseOut方法只是将动画排队到下一个渲染阶段,这意味着动画不会立即执行。我们负责更改我们想要动画的样式。如果我们不改变任何样式,那么我们就看不到任何动画。
Button组件不知道loading属性是如何更新的。这可能是因为获取请求、超时或任何其他操作。父组件负责更新loading属性。无论发生任何变化,我们都会将新样式应用于按钮,并进行平滑的动画。
在步骤 9中,我们定义了App类的内容。在这里,我们使用了我们的Button组件。当按下按钮时,loading属性的state将被更新,这将导致每次按下按钮时动画运行。
结论
在本章中,我们已经介绍了如何为您的 React Native 应用程序添加动画的基础知识。这些示例旨在提供有用的实际代码解决方案,并建立如何使用基本构建块,以便您更好地创建适合您的应用程序的动画。希望到目前为止,您应该已经开始熟悉Animated和LayoutAnimation动画助手。在第七章中,为您的应用程序添加高级动画,我们将结合我们在这里学到的东西来构建更复杂和有趣的应用程序 UI 动画。