Skip to content

Latest commit

 

History

History
475 lines (367 loc) · 16.7 KB

File metadata and controls

475 lines (367 loc) · 16.7 KB

十七、展示进度

本章是关于向用户传达进度的。React Native 有不同的组件来处理您想要交流的不同类型的进度。我们将从一个简短的讨论开始,首先讨论为什么我们需要像这样传达进展。然后,我们将开始实施进度指标和进度条。之后,您将看到一些具体的示例,这些示例演示了如何在加载数据时将进度指示器与导航一起使用,以及如何使用进度条在一系列步骤中传达当前位置。

进步与可用性

想象一下,你有一个微波炉,没有窗户,也没有声音。与之互动的唯一方法是按下一个标记为 cook 的按钮。尽管这个设备听起来很荒谬,但它是许多软件用户所面临的,而且没有任何进展迹象。微波炉在煮东西吗?如果是这样,我们如何知道何时会这样做?

改善微波状况的一个方法是增加声音。这样,用户在按下 cook 按钮后会得到反馈。所以,我们克服了一个障碍,但用户仍然在猜测我的食物在哪里?在我们停业之前,我们最好添加一些进度测量显示。计时器!明亮的

严肃地说,这并不是说 UI 程序员不理解这种可用性关注的基本原则;只是我们有一件事要做,而这类事情就优先级而言只是从裂缝中溜走了。在 React Native 中,有一些组件用于向用户提供不确定的进度反馈,以及提供精确的进度度量。如果你想要一个好的用户体验,最好把这些事情放在首位。

表示进展

在本节中,您将学习如何使用<ActivityIndicator>组件。顾名思义,当需要向用户指示发生了什么事情时,可以呈现此组件。实际的进展可能是不确定的,但至少你有一个标准化的方法来显示正在发生的事情,尽管还没有结果显示出来。

我们将创建一个超级简单的示例,这样您就可以看到这个组件的样子。以下是该应用的主要模块:

import React from 'react'; 
import { 
  AppRegistry, 
  View, 
  ActivityIndicator, 
} from 'react-native'; 

import styles from './styles'; 

// Renders an "<ActivityIndicator>" component in the 
// middle of the screen. It will animate on it's own 
// while displayed. 
const IndicatingProgress = () => ( 
  <View style={styles.container}> 
    <ActivityIndicator size="large" /> 
  </View> 
); 

AppRegistry.registerComponent( 
  'IndicatingProgress', 
  () => IndicatingProgress 
); 

<ActivityIndicator>组件与平台无关。以下是它在 iOS 上的外观:

Indicating progress

正如你所看到的,这只是在屏幕中间渲染一个动画旋转器。这是size属性中指定的大微调器。ActivityIndicator微调器也可以很小,如果您在另一个较小的元素中渲染它,这会更有意义。现在让我们来看一下 Android 设备的外观:

Indicating progress

旋转器看起来应该是不同的,但我们的应用在两个平台上传达了相同的东西,我们正在等待一些东西。

这个例子一直在旋转。别担心,下面将有一个更现实的进度指示器示例,它向您展示如何使用导航和加载 API 数据。

测量进度

仅仅表明正在取得进展的缺点是用户看不到尽头。这会导致一种不安的感觉,就像在没有定时器的微波炉里等待食物一样。当我们知道已经取得了多少进展,还有多少工作要做时,我们会感觉更好。这就是为什么尽可能使用确定性进度条总是更好的原因。

ActivityIndicator组件不同,React Native for progress bars 中没有平台无关组件。所以,我们必须自己做一个。我们将创建一个在 iOS 上使用<ProgressViewIOS>和在 Android 上使用<ProgressBarAndroid>的组件。

让我们先处理跨平台问题。请记住,React Native 知道根据其扩展导入正确的模块。下面是我们的ProgressBarComponent.ios.js模块的样子:

// Exports the "ProgressViewIOS" as the 
// "ProgressBarComponent" component that 
// our "ProgressBar" expects. 
export { 
  ProgressViewIOS as ProgressBarComponent, 
} from 'react-native'; 

// There are no custom properties needed. 
export const progressProps = {}; 

如您所见,我们直接从 React Native 导出ProgressViewIOS组件。我们还导出特定于平台的组件属性。在本例中,它是一个空对象,因为没有特定于<ProgressViewIOS>的属性。现在,让我们看一下 ToeT2 模块:

// Exports the "ProgressBarAndroid" component as 
// "ProgressBarComponent" that our "ProgressBar" 
// expects. 
export { 
  ProgressBarAndroid as ProgressBarComponent, 
} from 'react-native'; 

// The "styleAttr" and "indeterminate" props are 
// necessary to make "ProgressBarAndroid" look like 
// "ProgressViewIOS". 
export const progressProps = { 
  styleAttr: 'Horizontal', 
  indeterminate: false, 
}; 

该模块使用与ProgressBarComponent.ios.js模块完全相同的方法。它导出特定于 Android 的组件以及要传递给它的特定于 Android 的属性。现在,让我们构建应用将使用的ProgressBar组件:

import React, { PropTypes } from 'react'; 
import { 
  View, 
  Text, 
} from 'react-native'; 

// Imports the "ProgressBarComponent" which is the 
// actual react-native implementation. The actual 
// component that's imported is platform-specific. 
// The custom props in "progressProps" is also 
// platform-specific. 
import { 
  ProgressBarComponent, 
  progressProps, 
} from './ProgressBarComponent'; // eslint-disable-line import/no-unresolved 

import styles from './styles'; 

// The "ProgressLabel" component determines what to 
// render as a label, based on the boolean "label" 
// prop. If true, then we render some text that shows 
// the progress percentage. If false, we render nothing. 
const ProgressLabel = ({ show, progress }) => 
  new Map([ 
    [true, ( 
      <Text style={styles.progressText}> 
        {Math.round(progress * 100)}% 
      </Text> 
    )], 
    [false, null], 
  ]) 
  .get(show); 

// Our generic progress bar component... 
const ProgressBar = ({ 
  progress, 
  label, 
}) => ( 
  <View style={styles.progress}> 
    <ProgressLabel 
      show={label} 
      progress={progress} 
    /> 
    { /* "<ProgressBarComponent>" is really a ""<ProgressViewIOS>" 
         or a "<ProgressBarAndroid>". */ } 
    <ProgressBarComponent 
      {...progressProps} 
      style={styles.progress} 
      progress={progress} 
    /> 
  </View> 
); 

ProgressBar.propTypes = { 
  progress: PropTypes.number.isRequired, 
  label: PropTypes.bool.isRequired, 
}; 

ProgressBar.defaultProps = { 
  progress: 0, 
  label: true, 
}; 

export default ProgressBar; 

我们将从导入开始,逐步了解本模块中的内容。ProgressBarComponentprogressProps值从我们的ProgressBarComponent模块导入。React Native 确定从哪个模块导入此文件。

接下来,我们有ProgressLabel实用程序组件。它根据show属性计算进度条的标签。如果为 false,则不会呈现任何内容。如果为 true,则呈现一个<Text>组件,以百分比形式显示进度。

最后,我们的应用将导入并使用ProgressBar组件本身。这只是呈现标签和相应的进度条组件。它接受一个progress属性,该属性的值介于 0 和 1 之间。现在,让我们在主应用中使用此组件:

import React, { Component } from 'react'; 
import { 
  AppRegistry, 
  View, 
} from 'react-native'; 

import styles from './styles'; 
import ProgressBar from './ProgressBar'; 

class MeasuringProgress extends Component { 
  // Initially at 0% progress. Changing this state 
  // updates the progress bar. 
  state = { 
    progress: 0, 
  } 

  componentDidMount() { 
    // Continuously increments the "progress" state 
    // every 300MS, until we're at 100%. 
    const updateProgress = () => { 
      this.setState({ 
        progress: this.state.progress + 0.01, 
      }); 

      if (this.state.progress < 1) { 
        setTimeout(updateProgress, 300); 
      } 
    }; 

    updateProgress(); 
  } 

  render() { 
    return ( 
      <View style={styles.container}> 
        { /* This is awesome. A simple generic 
             "<ProgressBar>" component that works 
             on Android and on iOS. */ } 
        <ProgressBar 
          progress={this.state.progress} 
        /> 
      </View> 
    ); 
  } 
} 

AppRegistry.registerComponent( 
  'MeasuringProgress', 
  () => MeasuringProgress 
); 

最初,<ProgressBar>组件渲染为 0%。在componentDidMount()方法中,我们有一个updateProgress()函数,它使用一个计时器来模拟我们想要显示进度的真实过程。以下是 iOS 屏幕的外观:

Measuring progress

以下是 Android 上相同进度条的外观:

Measuring progress

导航指示灯

在本章前面,您已经了解了<ActivityIndicator>组件。在本节中,您将了解如何在导航加载数据的应用时使用它。例如,用户从第 1 页(场景)导航到第 2 页。但是,第二页需要从 API 中获取数据以显示给用户。因此,当网络呼叫发生时,显示进度指示器而不是没有有用信息的屏幕更有意义。

这样做实际上有点棘手,因为我们必须确保每次用户导航到屏幕时都从 API 获取屏幕所需的数据。因此,我们有两个目标:

  • Navigator组件为即将渲染的场景自动获取 API 数据。
  • 使用 API 调用返回的承诺作为显示微调器的手段,并在承诺得到解决后隐藏它。

因为我们的场景组件可能不关心是否显示微调器,所以让我们将其实现为普通的高阶组件:

import React, { Component, PropTypes } from 'react'; 
import { 
  View, 
  ActivityIndicator, 
} from 'react-native'; 

import styles from './styles'; 

// Wraps the "Wrapped" component with a stateful component 
// that renders an "<ActivityIndicator>" when the "loading" 
// state is true. 
const loading = Wrapped => 
  class LoadingWrapper extends Component { 
    static propTypes = { 
      promise: PropTypes.instanceOf(Promise), 
    } 

    state = { 
      loading: true, 
    } 

    // Adds a callback to the "promise" that was 
    // passed in. When the promise resolves, we set 
    // the "loading" state to false. 
    componentDidMount() { 
      this.props.promise.then( 
        () => this.setState({ loading: false }), 
        () => this.setState({ loading: false }) 
      ); 
    } 

    // If "loading" is true, render the "<ActivityIndicator>" 
    // component. Otherwise, render the "<Wrapped>" component. 
    render() { 
      return new Map([ 
        [true, ( 
          <View style={styles.container}> 
            <ActivityIndicator size="large" /> 
          </View> 
        )], 
        [false, ( 
          <Wrapped {...this.props} /> 
        )], 
      ]) 
      .get(this.state.loading); 
    } 
  }; 

export default loading; 

loading()函数接受一个组件Wrapped参数并返回一个LoadingWrapper组件。返回的包装器接受一个promise属性,解析后,它将loading状态更改为 false。正如您在render()方法中看到的,loading状态确定是渲染微调器还是渲染Wrapped组件。

使用了高阶函数,我们看看我们的场景组件,看看它是如何使用的:

import React, { PropTypes } from 'react'; 
import { View, Text } from 'react-native'; 

import styles from '../styles'; 
import loading from '../loading'; 
import second from './second'; 
import third from './third'; 

// Renders links to other scenes... 
const First = ({ navigator }) => ( 
  <View style={styles.container}> 
    <Text 
      style={styles.item} 
      onPress={() => navigator.replace(second)} 
    > 
      Second 
    </Text> 
    <Text 
      style={styles.item} 
      onPress={() => navigator.replace(third)} 
    > 
      Third 
    </Text> 
  </View> 
); 

First.propTypes = { 
  navigator: PropTypes.object.isRequired, 
}; 

// Simulates a real "fetch()" call by returning a promise 
// that's resolved after 1 second. 
const fetchData = () => new Promise( 
  resolve => setTimeout(resolve, 1000) 
); 

// The exported "Scene" component is composed with 
// higher-order "loading()" function. 
export default { 
  Scene: loading(First), 
  fetchData, 
}; 

此模块导出一个Scene组件和一个与 API 对话的fetchData()函数。我们前面创建的loading()函数在这里使用。它包装First组件,以便在fetchData()承诺待定时显示微调器。最后一步是在用户导航到给定页面时,将该承诺引入组件。这发生在主模块的renderScene()功能中:

import React, { Component } from 'react'; 
import { 
  AppRegistry, 
  Navigator, 
} from 'react-native'; 

import first from './scenes/first'; 

// The "<route.Scene>" component gets a promise property 
// passed to it, by calling "route.fetchData()". This 
// promise is what controls the progress indicator display. 
const renderScene = (route, navigator) => ( 
  <route.Scene 
    promise={route.fetchData()} 
    navigator={navigator} 
  /> 
); 

const NavigationIndicators = () => ( 
  <Navigator 
    initialRoute={first} 
    renderScene={renderScene} 
  /> 
); 

AppRegistry.registerComponent( 
  'NavigationIndicators', 
  () => NavigationIndicators 
); 

如您所见,任何给定路由的fetchData()函数都是在渲染之前调用的,这就是promise属性的设置方式。现在,当你在屏幕之间导航时,你会看到一个在屏幕中间显示的旋转器,看起来就像本章中的第一个例子,直到这个承诺解决。

步进

在最后一个示例中,我们将看到通过预定义的步骤数显示用户的进度。例如,将表单拆分为几个逻辑部分并以这样一种方式组织它们可能是有意义的,即当用户完成一个部分时,他们会进入下一步。进度条将对用户提供有用的反馈。

我们将修改本书前面的导航示例。我们将在标题下方的导航栏中插入一个进度条,以便用户知道他们已经走了多远,还有多远要走。我们还将重用您在本章前面实现的ProgressBar组件!

让我们先看看结果。此应用中有四个屏幕供用户导航。以下是第一页(场景)的外观:

Step progress

标题下方的进度条反映了一个事实,即用户在导航中占 25%。让我们看看第三个屏幕是什么样子:

Step progress

更新进度以反映用户在路由堆栈中的位置。让我们来看看实现这一目标所需的代码:

import React from 'react'; 
import { 
  AppRegistry, 
  Navigator, 
  View, 
} from 'react-native'; 

import routes from './routes'; 
import styles from './styles'; 
import ProgressBar from './ProgressBar'; 

const renderScene = route => (<route.Scene />); 

const routeMapper = { 
  Title: (route, navigator) => ( 
    <View style={styles.progress}> 
      <route.Title navigator={navigator} /> 
      { /* The "<ProgressBar>" component is rendered just 
           below the title text. There's no progress label, 
           just the bar itself. The "progress" itself is 
           computed based on where the current route is 
           in the route stack. */ } 
      <ProgressBar 
        label={false} 
        progress={ 
          (routes.indexOf(route) + 1) / routes.length 
        } 
      /> 
    </View> 
  ), 
  LeftButton: (route, navigator) => ( 
    <route.LeftButton navigator={navigator} /> 
  ), 
  RightButton: (route, navigator) => ( 
    <route.RightButton navigator={navigator} /> 
  ), 
}; 

const navigationBar = ( 
  <Navigator.NavigationBar 
    style={styles.nav} 
    routeMapper={routeMapper} 
  /> 
); 

const StepProgress = () => ( 
  <Navigator 
    initialRoute={routes[0]} 
    initialRouteStack={routes} 
    renderScene={renderScene} 
    navigationBar={navigationBar} 
  /> 
); 

AppRegistry.registerComponent( 
  'StepProgress', 
  () => StepProgress 
); 

请看routeMapper中的Title组件。这是呈现<ProgressBar>组件的地方。实际进度值基于当前路由在routes数组中的位置。这将确定在阵列中移动的完整百分比。

总结

在本章中,您学习了如何向用户展示幕后正在发生的事情。首先,我们讨论了为什么显示进度对应用的可用性很重要。然后,您实现了一个基本屏幕,指示正在取得进展。然后,您实现了一个ProgressBar组件,用于测量特定的进度量。

指标适用于不确定的进度,并且您实现了在网络呼叫挂起时显示进度指标的导航。在最后一节中,您实现了一个进度条,该进度条以预定义的步骤数向用户显示他们所在的位置。

在下一章中,您将看到 React 本地地图和地理位置数据的作用。