事件与手势
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
<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
'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
// 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
Keyboard Event
react-native-keyboard-spacer