事件与手势

Gesture

Web-based interfaces are usually designed with mouse-based controllers in mind. We use things like hover state to indicate interactivity and respond to user interaction. For mobile, unsurprisingly, it’s touch that matters. Mobile platforms have their own norms around interactions that you’ll want to design for. This varies somewhat from platform to platform: iOS behaves differently from Android, which behaves differently yet again from Windows Phone. React Native provides a number of APIs for you to leverage as you build touch-ready interfaces. In this section, we’ll look at the <TouchableHighlight> container component, as well as the lower-level APIs provided by PanResponder and the Gesture Responder system.

Using TouchableHighlight

Any interface elements that respond to user touch (think buttons, control elements, and so on) should usually have a wrapper. TouchableHighlight causes an overlay to appear when the view is touched, giving the user visual feedback. This is one of the key interactions that causes a mobile application to feel native, as opposed to a mobile-optimized website, where touch feedback is limited. As a general rule of thumb, you should use anywhere that would be a button or a link on the web. At its most basic usage, you just need to wrap your component in a , which will add a simple overlay when pressed. The component also gives you hooks for events such as onPressIn, onPressOut, onLongPress, and so on. You could use these, for instance, to build menus that only appear on long presses; and so on. Here’s an example of how we can wrap a component in a in order to give the user feedback:

<TouchableHighlight
  onPressIn={this._onPressIn}
  onPressOut={this._onPressOut}
  style={styles.touchable}>
    <View style={styles.button}>
      <Text style={styles.welcome}>
        {this.state.pressing ? 'EEK!' : 'PUSH ME'}
      </Text>
    </View>
</TouchableHighlight>

When the user taps the button, an overlay appears, and the text changes.

This is a contrived example, but it illustrates the basic interactions that make a button “feel” touchable on iOS. The overlay is a key piece of feedback that informs the user that an element can be pressed. Note that in order to apply the overlay, we don’t need to apply any logic to our styles; the handles the logic of that for us.Here’s the full code for this button component:

'use strict';

var React = require('react-native');
var {
  AppRegistry,
  StyleSheet,
  Text,
  View,
  TouchableHighlight
} = React;

var Button = React.createClass({
  getInitialState: function() {
    return {pressing: false};
  },
  _onPressIn: function() {
    this.setState({pressing: true});
  },
  _onPressOut: function() {
    this.setState({pressing: false});
  },
  render: function() {
    return (
      <View style={styles.container}>
        <TouchableHighlight onPressIn={this._onPressIn}
                            onPressOut={this._onPressOut}
                            style={styles.touchable}>
          <View style={styles.button}>
            <Text style={styles.welcome}>
              {this.state.pressing ? 'EEK!' : 'PUSH ME'}
            </Text>
          </View>
        </TouchableHighlight>
      </View>
    );
  }
});

// Styles
var styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  welcome: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
    color: '#FFFFFF'
  },
  touchable: {
    borderRadius: 100
  },
  button: {
    backgroundColor: '#FF0000',
    borderRadius: 100,
    height: 200,
    width: 200,
    justifyContent: 'center'
  },
});

module.exports = Button;

The Gesture Responder System

What if you want to do more than just make things “tappable”? React Native also exposes two APIs for custom touch handling: GestureResponder and PanResponder. GestureResponder is a lower-level API, while PanResponder provides a useful abstraction. We’ll start by looking at how the GestureResponder system works, because it’s the basis for PanResponder’s API.

Touch on mobile is fairly complicated. Most mobile platforms support multitouch, which means that there can be multiple touch points active on the screen at once. (Not all of these are necessarily fingers, either; think about the difficulty of, e.g., detecting the user’s palm resting on the corner of the screen.) Additionally, there’s the issue of which view should handle a given touch. This problem is similar to how mouse events are processed on the web, and the default behavior is also similar: the topmost child handles the touch event by default. With React Native’s gesture responder system, however, we can override this behavior if we so choose.

The touch responder is the view which handles a given touch event. In the previous section, we saw that the `` component acts as a touch responder. We can cause our own components to become the touch responder, too. The lifecycle by which this process is negotiated is a little complicated. A view which wishes to obtain touch responder status should implement four props:

  • View.props.onStartShouldSetResponder
  • View.props.onMoveShouldSetResponder
  • View.props.onResponderGrant
  • View.props.onResponderReject

These then get invoked according to the following flow, in order to determine if the view will receive responder status:

Yikes, that looks complicated! Let’s tease this apart. First, a touch event has three main lifecycle stages: start, move, and release. (These correspond to mouseDown, mouseMove, and mouseUp in the browser.) A view can request to be the touch responder during the start or the move phase. This behavior is specified by onStartShouldSetResponder and onMoveShouldSetResponder. When one of those functions returns true, the view attempts to claim responder status. After a view has attempted to claim responder status, its attempt may be granted or rejected. The appropriate callback — either onResponderGrant or onResponderReject — will be invoked. The responder negotiation functions are called in a bubbling pattern. If multiple views attempt to claim responder status, the deepest component will become the responder. This is typically the desired behavior; otherwise, you would have difficulty adding touchable components such as buttons to a larger view. If you want to override this behavior, parent components can make use of onStartShouldSetResponderCapture and onMoveShouldSetResponderCapture. Returning true from either of these will prevent a component’s children from becoming the touch responder. After a view has successfully claimed touch responder status, its relevant event handlers may be called. Here’s the excerpt from the Gesture Responder documentation: View.props.onResponderMove The user is moving their finger View.props.onResponderRelease Fired at the end of the touch, ie “touchUp” View.props.onResponderTerminationRequest Something else wants to become responder. Should this view release the responder? Returning true allows release View.props.onResponderTerminate The responder has been taken from the View. Might be taken by other views after a call to onResponderTerminationRequest, or might be taken by the OS without asking (happens with control center/ notification center on iOS) Most of the time, you will primarily be concerned with onResponderMove and onResponderRelease. All of these methods receive a synthetic touch event object, which adheres to the following format (again, excerpted from the documentation): changedTouches - Array of all touch events that have changed since the last event identifier - The ID of the touch locationX - The X position of the touch, relative to the element locationY - The Y position of the touch, relative to the element pageX - The X position of the touch, relative to the screen pageY - The Y position of the touch, relative to the screen target - The node id of the element receiving the touch event timestamp - A time identifier for the touch, useful for velocity calculation touches - Array of all current touches on the screen You can make use of this information when deciding whether or not to respond to a touch event. Perhaps your view only cares about two-finger touches, for example. This is a fairly low-level API; if you want to detect and respond to gestures in this way, you will need to spend a decent amount of time tuning the correct parameters and figuring out which values you should care about. In the next section, we will take a look at PanResponder, which supplies a somewhat higher-level interpretation of user gestures. PanResponder Unlike , PanResponder is not a component, but rather a class provided by React Native. It provides a slightly higher-level API than the basic events returned by the Gesture Responder system, while still providing access to those raw events. A PanResponder gestureState object gives you access to the following, in accordance with the PanResponder documentation: stateID - ID of the gestureState- persisted as long as there at least one touch on screen moveX - the latest screen coordinates of the recently-moved touch moveY - the latest screen coordinates of the recently-moved touch x0 - the screen coordinates of the responder grant y0 - the screen coordinates of the responder grant dx - accumulated distance of the gesture since the touch started dy - accumulated distance of the gesture since the touch started vx - current velocity of the gesture vy - current velocity of the gesture numberActiveTouches - Number of touches currently on screeen As you can see, in addition to raw position data, a gestureState object also includes information such as the current velocity of the touch and the accumulated distance. To make use of PanResponder in a component, we need to create a PanResponder object and then attach it to a component in the render method. Creating a PanResponder requires us to specify the proper handlers for PanResponder events: this._panResponder = PanResponder.create({ onStartShouldSetPanResponder: this._handleStartShouldSetPanResponder, onMoveShouldSetPanResponder: this._handleMoveShouldSetPanResponder, onPanResponderGrant: this._handlePanResponderGrant, onPanResponderMove: this._handlePanResponderMove, onPanResponderRelease: this._handlePanResponderEnd, onPanResponderTerminate: this._handlePanResponderEnd, }); Then, we use spread syntax to attach the PanResponder to the view in the component’s render method. render: function() { return ( <View {…this._panResponder.panHandlers}> { /_ View contents here _/ } ); } After this, the handlers that you passed to the PanResponder.create call will be invoked during the appropriate move events, if the touch originates within this view. Here’s a modified version of the PanResponder example code provided by React Native. This version listens to touch events on the container view, as opposed to just the circle, and so that the values are printed to the screen as you interact with the application. If you plan on implementing your own gesture recognizers, I suggest experimenting with this application on a real device, so that you can get a feel for how these values respond.

// Adapted from https://github.com/facebook/react-native/blob/master/Examples/UIExplorer/PanResponderExample.js

"use strict";

var React = require("react-native");
var { StyleSheet, PanResponder, View, Text } = React;

var CIRCLE_SIZE = 40;
var CIRCLE_COLOR = "blue";
var CIRCLE_HIGHLIGHT_COLOR = "green";

var PanResponderExample = React.createClass({
  statics: {
    title: "PanResponder Sample",
    description: "Basic gesture handling example",
  },

  _panResponder: {},
  _previousLeft: 0,
  _previousTop: 0,
  _circleStyles: {},
  circle: (null: ?{ setNativeProps(props: Object): void }),

  getInitialState: function () {
    return {
      numberActiveTouches: 0,
      moveX: 0,
      moveY: 0,
      x0: 0,
      y0: 0,
      dx: 0,
      dy: 0,
      vx: 0,
      vy: 0,
    };
  },

  componentWillMount: function () {
    this._panResponder = PanResponder.create({
      onStartShouldSetPanResponder: this._handleStartShouldSetPanResponder,
      onMoveShouldSetPanResponder: this._handleMoveShouldSetPanResponder,
      onPanResponderGrant: this._handlePanResponderGrant,
      onPanResponderMove: this._handlePanResponderMove,
      onPanResponderRelease: this._handlePanResponderEnd,
      onPanResponderTerminate: this._handlePanResponderEnd,
    });
    this._previousLeft = 20;
    this._previousTop = 84;
    this._circleStyles = {
      left: this._previousLeft,
      top: this._previousTop,
    };
  },

  componentDidMount: function () {
    this._updatePosition();
  },

  render: function () {
    return (
      <View style={styles.container} {...this._panResponder.panHandlers}>
        <View
          ref={(circle) => {
            this.circle = circle;
          }}
          style={styles.circle}
        />
        <Text>
          {this.state.numberActiveTouches} touches, dx: {this.state.dx}, dy:{" "}
          {this.state.dy}, vx: {this.state.vx}, vy: {this.state.vy}
        </Text>
      </View>
    );
  },

  _highlight: function () {
    this.circle &&
      this.circle.setNativeProps({
        backgroundColor: CIRCLE_HIGHLIGHT_COLOR,
      });
  },

  _unHighlight: function () {
    this.circle &&
      this.circle.setNativeProps({
        backgroundColor: CIRCLE_COLOR,
      });
  },

  _updatePosition: function () {
    this.circle && this.circle.setNativeProps(this._circleStyles);
  },

  _handleStartShouldSetPanResponder: function (
    e: Object,
    gestureState: Object
  ): boolean {
    // Should we become active when the user presses down on the circle?
    return true;
  },

  _handleMoveShouldSetPanResponder: function (
    e: Object,
    gestureState: Object
  ): boolean {
    // Should we become active when the user moves a touch over the circle?
    return true;
  },

  _handlePanResponderGrant: function (e: Object, gestureState: Object) {
    this._highlight();
  },

  _handlePanResponderMove: function (e: Object, gestureState: Object) {
    this.setState({
      stateID: gestureState.stateID,
      moveX: gestureState.moveX,
      moveY: gestureState.moveY,
      x0: gestureState.x0,
      y0: gestureState.y0,
      dx: gestureState.dx,
      dy: gestureState.dy,
      vx: gestureState.vx,
      vy: gestureState.vy,
      numberActiveTouches: gestureState.numberActiveTouches,
    });

    this._circleStyles.left = this._previousLeft + gestureState.dx;
    this._circleStyles.top = this._previousTop + gestureState.dy;
    this._updatePosition();
  },
  _handlePanResponderEnd: function (e: Object, gestureState: Object) {
    this._unHighlight();
    this._previousLeft += gestureState.dx;
    this._previousTop += gestureState.dy;
  },
});

var styles = StyleSheet.create({
  circle: {
    width: CIRCLE_SIZE,
    height: CIRCLE_SIZE,
    borderRadius: CIRCLE_SIZE / 2,
    backgroundColor: CIRCLE_COLOR,
    position: "absolute",
    left: 0,
    top: 0,
  },
  container: {
    flex: 1,
    paddingTop: 64,
  },
});

module.exports = PanResponderExample;

Choosing How to Handle Touch How should you decide when to use the touch and gesture APIs discussed in this section? It depends on what you want to build. In order to provide the user with basic feedback, and indicate that something is “tappable,” like a button, use the component. In order to implement your own, custom touch interfaces, use either the raw Gesture Responder system, or a PanResponder. Chances are that you will almost always prefer the PanResponder approach, because it also gives you access to the simpler touch events provided by the Gesture Responder system. If you are designing a game, or an application with an unusual interface, you’ll need to spend some time building out the interactions you want by using these APIs. For many applications, you won’t need to implement any custom touch handling with either the Gesture Responder system or the PanResponder. In the next section, we’ll look at some of the higher-level components which implement common UI patterns for you.

Keyboard Event

react-native-keyboard-spacer

上一页
下一页