Skip to content

Latest commit

 

History

History
847 lines (680 loc) · 25.8 KB

File metadata and controls

847 lines (680 loc) · 25.8 KB

二十、警报、通知和确认

本章的目的是向您展示如何以非常规的方式向用户展示信息。传统的方法是使用View组件,并直接在屏幕上渲染。然而,有时用户需要查看一些重要信息,但我们不一定要将它们从当前页面上删除。

我们将从重要信息的快速讨论开始。了解什么是重要信息以及何时使用它,您将了解如何获得用户对错误和成功场景的确认。然后,我们将实现一些被动通知,向用户显示发生了什么事情。最后,我们将实现模态视图,向用户显示后台正在发生的事情。

重要信息

在我们开始执行警报、通知和确认之前,让我们花几分钟想想这些项目中的每一个意味着什么。我认为这很重要,因为如果你最后被动地通知用户一个错误,它很容易被忽略,而这不是你想发生的事情。以下是我对这些项目的定义:

  • 警报:发生了一些重要的事情,我们需要确保用户能够看到发生了什么。用户可能需要确认警报。
  • 通知:发生了一些事情,但其重要性不足以完全阻止用户的操作。这些通常会自行消失。

确认实际上是警报的一部分。例如,如果用户刚刚执行了一个操作,然后想确保在继续之前知道该操作是否成功,则必须确认已看到该信息才能关闭该模式。确认也可能存在于警报中,警告用户他们将要执行的操作。

诀窍是尝试在信息很好知道但不重要的地方使用通知。只有在用户未确认正在发生的情况下,功能的工作流无法继续时,才使用确认。在以下部分中,您将看到用于不同目的的警报和通知示例。

获取用户确认

在本节中,您将学习如何显示模态视图以获得用户的确认。我们将从成功的场景开始,在该场景中,一个操作生成了一个我们希望用户知道的成功结果。然后我们将查看错误场景,其中出现了一些错误,您不希望用户在未确认问题的情况下继续前进。

成功确认

让我们从实现一个模式视图开始,该视图显示为用户成功执行操作的结果。以下是用于向用户显示成功确认的Modal组件:

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

import styles from './styles'; 

// Uses "<Modal>" to display the underlying view 
// on top of the current view. Properties passed to 
// this component are also passed to the modal. 
const ConfirmationModal = props => ( 
  <Modal {...props}> 
    { /* Slightly confusing, but we need an inner and 
         an outer "<View>" to style the height of the 
         modal correctly. */ } 
    <View style={styles.modalContainer}> 
      <View style={styles.modalInner}> 
        {/* The confirmation message... */} 
        <Text style={styles.modalText}>Dude, srsly?</Text> 

        { /* The confirmation and the cancel buttons. Each 
             button triggers a different callback function 
             that's passed in from the container 
             component. */} 
        <Text 
          style={styles.modalButton} 
          onPress={props.onPressConfirm} 
        > 
          Yep 
        </Text> 
        <Text 
          style={styles.modalButton} 
          onPress={props.onPressCancel} 
        > 
          Nope 
        </Text> 
      </View> 
    </View> 
  </Modal> 
); 

ConfirmationModal.propTypes = { 
  visible: PropTypes.bool.isRequired, 
  onPressConfirm: PropTypes.func.isRequired, 
  onPressCancel: PropTypes.func.isRequired, 
}; 

ConfirmationModal.defaultProps = { 
  transparent: true, 
  onRequestClose: () => {}, 
}; 

export default ConfirmationModal; 

如您所见,传递给ConfirmationModal的属性被转发给 React NativeModal组件。你马上就会明白为什么了。首先,让我们看看这个确认模式是什么样子的:

Success confirmation

用户完成操作后显示的模式有我们自己的样式和确认消息。它也有两个操作,但可能只需要一个,这取决于此确认是预操作还是后操作。以下是用于此模式的样式:

modalContainer: { 
  flex: 1, 
  justifyContent: 'center', 
  alignItems: 'center', 
}, 

modalInner: { 
  backgroundColor: 'azure', 
  padding: 20, 
  borderWidth: 1, 
  borderColor: 'lightsteelblue', 
  borderRadius: 2, 
  alignItems: 'center', 
}, 

modalText: { 
  fontSize: 16, 
  margin: 5, 
  color: 'slategrey', 
}, 

modalButton: { 
  fontWeight: 'bold', 
  margin: 5, 
  color: 'slategrey', 
}, 

使用 React-NativeModal组件,您可以决定确认模式视图的外观。将它们视为常规视图,唯一的区别是它们是在其他视图之上渲染的。

很多时候,您可能不想设计自己的模态视图。例如,在 web 浏览器中,您可以简单地调用alert()函数,该函数在由浏览器设置样式的窗口中显示文本。React Native 也有类似的功能:Alert.alert()。这里棘手的部分是,这是一个命令式 API,您不一定要在应用中直接公开它。

相反,让我们实现一个警报确认组件,该组件隐藏此特定 React 本机 API 的详细信息,以便您的应用可以将其与任何其他组件一样处理:

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

// The "actions" Map will map the "visible" 
// property to the "Alert.alert()" function, 
// or to a noop function. 
const actions = new Map([ 
  [true, Alert.alert], 
  [false, () => {}], 
]); 

class ConfirmationAlert extends Component { 
  // When the component is mounted, show the 
  // alert if "visible" is true. Otherwise, 
  // this is a noop. 
  componentDidMount() { 
    actions.get(this.props.visible)( 
      this.props.title, 
      this.props.message, 
      this.props.buttons, 
    ); 
  } 

  // When the "visible" property value changes, 
  // display the alert if the "visible" 
  // property is true. 
  componentWillReceiveProps(props) { 
    actions.get(props.visible)( 
      props.title, 
      props.message, 
      props.buttons, 
    ); 
  } 

  // Since an imperative API is used to display 
  // the alert, we don't actually render anything. 
  render() { 
    return null; 
  } 
} 

ConfirmationAlert.propTypes = { 
  visible: PropTypes.bool.isRequired, 
  title: PropTypes.string, 
  message: PropTypes.string, 
  buttons: PropTypes.array, 
}; 

export default ConfirmationAlert; 

该组件有两个重要方面。首先,看一看actions地图。其键 true 和 false 对应于visible属性值。这些值对应于命令式alert()API 和 noop 函数。这是将我们熟悉和喜爱的声明性 React 组件接口转换为隐藏在视图中的东西的关键。

其次,请注意,render()方法不需要呈现任何内容,因为该组件专门处理命令式 React 本机调用。但是,感觉好像有东西正在呈现给使用ConfirmationAlert的人。很酷,对吧?

以下是 iOS 上的警报:

Success confirmation

就功能而言,这里没有什么真正的不同。它下面有一个标题和文本,但如果您愿意,可以很容易地将其添加到模式视图中。真正的区别在于,这个模式看起来像一个 iOS 模式,而不是应用设计的模式。让我们看看 Android 上此警报的外观:

Success confirmation

这个模式看起来像 Android 模式,我们不必设计它。我认为在大多数情况下,使用警报而不是情态动词是一个更好的选择。将某些东西设计成 iOS 或 Android 的一部分是有意义的。但是,有时您需要更多地控制模式的外观,例如显示错误确认时。下面我们来看看这些。但首先,以下是用于显示模式对话框和警报确认对话框的代码:

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

import styles from './styles'; 
import ConfirmationModal from './ConfirmationModal'; 
import ConfirmationAlert from './ConfirmationAlert'; 

class SuccessConfirmation extends Component { 
  // The two pieces of state used to control 
  // the display of the modal and the alert 
  // views. 
  state = { 
    data: fromJS({ 
      modalVisible: false, 
      alertVisible: false, 
    }), 
  } 

  // Getter for "Immutable.js" state data... 
  get data() { 
    return this.state.data; 
  } 

  // Setter for "Immutable.js" state data... 
  set data(data) { 
    this.setState({ data }); 
  } 

  // A "modal" button was pressed. So show 
  // or hide the modal based on its current state. 
  toggleModal = () => { 
    this.data = this.data 
      .update('modalVisible', v => !v); 
  } 

  // A "alert" button was pressed. So show 
  // or hide the alert based on its current state. 
  toggleAlert = () => { 
    this.data = this.data 
      .update('alertVisible', v => !v); 
  } 

  render() { 
    const { 
      modalVisible, 
      alertVisible, 
    } = this.data.toJS(); 

    const { 
      toggleModal, 
      toggleAlert, 
    } = this; 

    return ( 
      <View style={styles.container}> 
        { /* Renders the "<ConfirmationModal>" component, 
             which is hidden by default and controlled 
             by the "modalVisible" state. */ } 
        <ConfirmationModal 
          animationType="fade" 
          visible={modalVisible} 
          onPressConfirm={toggleModal} 
          onPressCancel={toggleModal} 
        /> 

        { /* Renders the "<ConfirmationAlert>" component, 
             which doesn't actually render anything since 
             it controls an imperative API under the hood. 
             The "alertVisible" state controls this API. */ } 
        <ConfirmationAlert 

          message="For realz?" 
          visible={alertVisible} 
          buttons={[ 
            { 
              text: 'Nope', 
              onPress: toggleAlert, 
            }, 
            { 
              text: 'Yep', 
              onPress: toggleAlert, 
            }, 
          ]} 
        /> 

        { /* Shows the "<ConfirmationModal>" component 
             by changing the "modalVisible" state. */ } 
        <Text 
          style={styles.text} 
          onPress={toggleModal} 
        > 
          Show Confirmation Modal 
        </Text> 

        { /* Shows the "<ConfirmationAlert>" component 
             by changing the "alertVisible" state. */ } 
        <Text 
          style={styles.text} 
          onPress={toggleAlert} 
        > 
          Show Confimation Alert 
        </Text> 
      </View> 
    ); 
  } 
} 

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

如您所见,呈现模态的方法与呈现警报的方法不同。然而,它们仍然是声明性组件,根据属性值的变化而变化,这是关键。

错误确认

当您需要用户确认错误时,您在上一节中学习的所有原则都适用。如果需要对显示进行更多控制,请使用模式。例如,您可能希望模态为红色且外观恐怖:

Error confirmation

以下是用于创建此外观的样式。也许你想要一些更微妙的东西,但关键是你可以让它看起来像你想要的:

import { StyleSheet } from 'react-native'; 

export default StyleSheet.create({ 
  container: { 
    flex: 1, 
    justifyContent: 'center', 
    alignItems: 'center', 
    backgroundColor: 'ghostwhite', 
  }, 

  text: { 
    color: 'slategrey', 
  }, 

  modalContainer: { 
    flex: 1, 
    justifyContent: 'center', 
    alignItems: 'center', 
  }, 

  modalInner: { 
    backgroundColor: 'azure', 
    padding: 20, 
    borderWidth: 1, 
    borderColor: 'lightsteelblue', 
    borderRadius: 2, 
    alignItems: 'center', 
  }, 

  modalInnerError: { 
    backgroundColor: 'lightcoral', 
    borderColor: 'darkred', 
  }, 

  modalText: { 
    fontSize: 16, 
    margin: 5, 
    color: 'slategrey', 
  }, 

  modalTextError: { 
    fontSize: 18, 
    color: 'darkred', 
  }, 

  modalButton: { 
    fontWeight: 'bold', 
    margin: 5, 
    color: 'slategrey', 
  }, 

  modalButtonError: { 
    color: 'black', 
  }, 
}); 

您用于成功确认的相同模态样式仍在此处。这是因为错误确认模式需要许多相同的样式。以下是如何将这两种方法应用于Modal组件:

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

import styles from './styles'; 

// Declares styles for the error modal by 
// combining regular modal styles with 
// error styles. 
const innerViewStyle = [ 
  styles.modalInner, 
  styles.modalInnerError, 
]; 

const textStyle = [ 
  styles.modalText, 
  styles.modalTextError, 
]; 

const buttonStyle = [ 
  styles.modalButton, 
  styles.modalButtonError, 
]; 

// Just like a success modal, accept for the addition of 
// error styles. 
const ErrorModal = props => ( 
  <Modal {...props}> 
    <View style={styles.modalContainer}> 
      <View style={innerViewStyle}> 
        <Text style={textStyle}> 
          Epic fail! 
        </Text> 
        <Text 
          style={buttonStyle} 
          onPress={props.onPressConfirm} 
        > 
          Fix it 
        </Text> 
        <Text 
          style={buttonStyle} 
          onPress={props.onPressCancel} 
        > 
          Ignore it 
        </Text> 
      </View> 
    </View> 
  </Modal> 
); 

ErrorModal.propTypes = { 
  visible: PropTypes.bool.isRequired, 
  onPressConfirm: PropTypes.func.isRequired, 
  onPressCancel: PropTypes.func.isRequired, 
}; 

ErrorModal.defaultProps = { 
  transparent: true, 
  onRequestClose: () => {}, 
}; 

export default ErrorModal; 

这里需要注意的是,在将样式传递到style属性之前,它们是如何组合为数组的。错误样式总是排在最后,因为冲突的样式属性(如backgroundColor)将被数组中稍后出现的任何内容覆盖。

除了错误确认中的样式之外,您还可以包括任何您想要的高级控件。这实际上取决于应用如何让用户处理错误。例如,也许可以采取一些行动。

然而,更常见的情况是,出现了一些问题,除了确保用户了解情况之外,我们无能为力。在这些情况下,您可能只需显示警报即可:

Error confirmation

被动通知

到目前为止,您在本章中检查的通知都需要用户输入。这是经过设计的,因为这是我们强制用户查看的重要信息。然而,你不想做得太多。对于重要但如果忽略不会改变生活的通知,可以使用被动通知。它们的显示方式没有模态那么突出,不需要任何用户操作就可以忽略。

在本节中,我们将创建一个Notification组件,该组件使用 Android 的 Toast API,并为 iOS 创建一个自定义模式。之所以称之为 Toast API,是因为显示的信息看起来像是弹出的一块 Toast。以下是 Android 组件的外观:

import React, { PropTypes } from 'react'; 
import { ToastAndroid } from 'react-native'; 
import { Map as ImmutableMap } from 'immutable'; 

// Toast helper. Always returns "null" so that the 
// output can be rendered as a React element. 
const show = (message, duration) => { 
  ToastAndroid.show(message, duration); 
  return null; 
}; 

// This component will always return null, 
// since it's using an imperative React Native 
// interface to display popup text. If the 
// "message" property was provided, then 
// we display a message. 
const Notification = ({ message, duration }) => 
  ImmutableMap() 
    .set(null, null) 
    .set(undefined, null) 
    .get(message, show(message, duration)); 

Notification.propTypes = { 
  message: PropTypes.string, 
  duration: PropTypes.number.isRequired, 
}; 

Notification.defaultProps = { 
  duration: ToastAndroid.LONG, 
}; 

export default Notification; 

再一次,我们要处理的是一个命令式的 React 本机 API,您不想将它公开给应用的其余部分。相反,该组件将命令式ToastAndroid.show()函数隐藏在声明性 React 组件后面。不管怎样,该组件都返回 null,因为它实际上不呈现任何内容。以下是ToastAndroid通知的内容:

Passive notifications

如您所见,发生了某些事情的通知显示在屏幕底部,并在短时间延迟后删除。关键是通知要低调。

iOS 通知组件更为复杂,因为它需要状态和生命周期事件来使模式视图的行为类似于临时通知。代码如下:

import React, { Component, PropTypes } from 'react'; 
import { 
  View, 
  Modal, 
  Text, 
} from 'react-native'; 
import { Map as ImmutableMap } from 'immutable'; 

import styles from './styles'; 

class Notification extends Component { 

  static propTypes = { 
    message: PropTypes.string, 
    duration: PropTypes.number.isRequired, 
  } 

  static defaultProps = { 
    duration: 1500, 
  } 

  // The modal component is either "visible", or not. 
  // The "timer" is used to hide the notification 
  // after some predetermined amount of time. 
  state = { visible: false } 
  timer = null 

  showModal = (message, duration) => { 
    // Update the "visible" state, based on whether 
    // or not there's a "message" value. 
    this.setState({ 
      visible: ImmutableMap() 
        .set(null, false) 
        .set(undefined, false) 
        .get(message, true), 
    }); 

    // Create a timer to hide the modal if a new 
    // "message" is set. 
    this.timer = ImmutableMap() 
      .set(null, () => null) 
      .set(undefined, () => null) 
      .get(message, () => setTimeout( 
        () => this.setState({ visible: false }), 
        duration 
      ))(); 
  } 

  // When the component is mounted, show the modal 
  // if a message was provided. Also set a timeout 
  // that automatically closes the modal. 
  componentWillMount() { 
    this.showModal( 
      this.props.message, 
      this.props.duration, 
    ); 
  } 

  // Does the same thing as "componentWillMount()" 
  // except it's called when new properties are passed. 
  componentWillReceiveProps(props) { 
    this.showModal( 
      props.message, 
      props.duration, 
    ); 
  } 

  componentWillUnmount() { 
    clearTimeout(this.timer); 
  } 

  render() { 
    const modalProps = { 
      animationType: 'fade', 
      transparent: true, 
      visible: this.state.visible, 
    }; 

    return ( 
      <Modal {...modalProps}> 
        <View style={styles.notificationContainer}> 
          <View style={styles.notificationInner}> 
            <Text>{this.props.message}</Text> 
          </View> 
        </View> 
      </Modal> 
    ); 
  } 
} 

Notification.propTypes = { 
  message: PropTypes.string, 
  duration: PropTypes.number.isRequired, 
}; 

Notification.defaultProps = { 
  duration: 1500, 
}; 

export default Notification; 

我们必须设置模式的样式以显示通知文本,以及用于在延迟后隐藏通知的状态。以下是 iOS 的最终结果:

Passive notifications

同样,与ToastAndroidAPI 相同的原则也适用于此处。您可能已经注意到,除了显示通知按钮之外,还有另一个按钮。这是一个重新渲染视图的简单计数器。事实上,展示这一看似迟钝的特性是有原因的,你马上就会看到。以下是主应用视图的代码:

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

import styles from './styles'; 
import Notification from './Notification'; // eslint-disable-line import/no-unresolved 

class PassiveNotifications extends Component { 
  // The initial state is the number of times 
  // the counter button has been clicked, and 
  // the notification message. 
  state = { 
    data: fromJS({ 
      count: 0, 
      message: null, 
    }), 
  } 

  // Getter for "Immutable.js" state data... 
  get data() { 
    return this.state.data; 
  } 

  // Setter for "Immutable.js" state data... 
  set data(data) { 
    this.setState({ data }); 
  } 

  render() { 
    const { 
      count, 
      message, 
    } = this.data.toJS(); 

    return ( 
      <View style={styles.container}> 
        { /* The "Notification" component is 
             only displayed if the "message" state 
             has something in it. */ } 
        <Notification message={message} /> 

        { /* Updates the count. Also needs to make 
             sure that the "message" state is null, 
             even if the message has been hidden 
             already. */ } 
        <Text 
          onPress={() => { 
            this.data = this.data 
              .update('count', c => c + 1) 
              .set('message', null); 
          }} 
        > 
          Pressed {count} 
        </Text> 

        { /* Displays the notification by 
             setting the "message" state. */ } 
        <Text 
          onPress={() => { 
            this.data = this.data 
              .set('message', 'Something happened!'); 
          }} 
        > 
          Show Notification 
        </Text> 
      </View> 
    ); 
  } 
} 

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

press counter 的全部目的是证明,即使Notification组件是声明性的,并且在状态更改时接受新的属性值,但在更改其他状态值时,仍然必须将消息状态设置为 null。原因是,如果重新呈现组件,并且消息状态中仍有一个字符串,它将一遍又一遍地显示相同的通知。

活动模态

在本章的最后一节中,您将实现一个显示进度指示器的模式。其思想是显示模式,然后在承诺解决时隐藏它。以下是通用Activity组件的代码,该组件显示带有活动指示器的模态:

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

import styles from './styles'; 

// The "Activity" component will only display 
// if the "visible" property is try. The modal 
// content is an "<ActivityIndicator>" component. 
const Activity = props => ( 
  <Modal visible={props.visible} transparent> 
    <View style={styles.modalContainer}> 
      <ActivityIndicator size={props.size} /> 
    </View> 
  </Modal> 
); 

Activity.propTypes = { 
  visible: PropTypes.bool.isRequired, 
  size: PropTypes.string.isRequired, 
}; 

Activity.defaultProps = { 
  visible: false, 
  size: 'large', 
}; 

export default Activity; 

您可能会试图将承诺传递给组件,以便在承诺解析时自动隐藏自己。我认为这不是一个好主意,因为这样你就必须在这个组件中引入状态。此外,它将取决于一项承诺才能发挥作用。通过我们实现此组件的方式,您可以仅基于visible属性显示或隐藏模式。以下是 iOS 上的活动模式:

Activity modals

模态上有一个半透明的背景,它放置在主视图上,带有**获取内容。。。**链接。下面是如何创建此效果的:

modalContainer: { 
  flex: 1, 
  justifyContent: 'center', 
  alignItems: 'center', 
  backgroundColor: 'rgba(0, 0, 0, 0.2)', 
}, 

我们没有将实际的Modal组件设置为透明的,而是在backgroundColor中设置了透明度,这提供了覆盖的外观。现在,让我们来看看控制这个组件的代码:

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

import styles from './styles'; 
import Activity from './Activity'; 

class ActivityModals extends Component { 
  // The state is a "fetching" boolean value, 
  // and a "promise" that is used to determine 
  // when the fetching is done. 
  state = { 
    data: fromJS({ 
      fetching: false, 
      promise: Promise.resolve(), 
    }), 
  } 

  // Getter for "Immutable.js" state data... 
  get data() { 
    return this.state.data; 
  } 

  // Setter for "Immutable.js" state data... 
  set data(data) { 
    this.setState({ data }); 
  } 

  // When the fetch button is pressed, the 
  // promise that simulates async activity 
  // is set, along with the "fetching" state. 
  // When the promise resolves, the "fetching" 
  // state goes back to false, hiding the modal. 
  onPress = () => { 
    this.data = this.data 
      .merge({ 
        promise: new Promise(resolve => 
          setTimeout(resolve, 3000) 
        ).then(() => { 
          this.data = this.data 
            .set('fetching', false); 
        }), 
        fetching: true, 
      }); 
  } 

  render() { 
    return ( 
      <View style={styles.container}> 
        { /* The "<Activity>" modal is only visible 
             when the "fetching" state is true. */ } 
        <Activity 
          visible={this.data.get('fetching')} 
        /> 
        <Text onPress={this.onPress}> 
          Fetch Stuff... 
        </Text> 
      </View> 
    ); 
  } 
} 

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

当按下 fetch 链接时,我们创建一个新的 promise 来模拟一些异步网络活动。然后,当承诺解析时,我们必须将fetching状态改回 false,以便隐藏活动对话框。

总结

在本章中,您了解了向移动用户显示重要信息的必要性。这有时涉及用户的明确反馈,即使这仅仅意味着确认消息。在其他情况下,被动通知工作得更好,因为它们比确认模式不那么突兀。

有两种工具可用于向用户显示消息:modals 和 alerts。模态更灵活,因为它们就像常规视图一样。警报适用于显示纯文本,并为我们处理样式问题。在 Android 上,您有额外的ToastAndroid界面。您看到,在 iOS 上也可以这样做,但这需要更多的工作。

在下一章中,我们将深入研究 React Native 中的手势响应系统,它提供了比浏览器更好的移动体验。