Skip to content

Commit 23543ea

Browse files
committed
Add PanResponder examples to docs
1 parent ea02e65 commit 23543ea

File tree

13 files changed

+4050
-4
lines changed

13 files changed

+4050
-4
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Meta, Props, Preview, Story } from '@storybook/addon-docs/blocks';
2+
import * as Stories from './examples';
3+
4+
<Meta title="APIs|PanResponder" />
5+
6+
# PanResponder
7+
8+
## Examples
9+
10+
<Preview withSource='none'>
11+
<Story name="draggableCircle">
12+
<Stories.draggableCircle />
13+
</Story>
14+
</Preview>
15+
16+
<Preview withSource='none'>
17+
<Story name="locationXY">
18+
<Stories.locationXY />
19+
</Story>
20+
</Preview>
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/**
2+
* @flow
3+
*/
4+
5+
import React, { PureComponent } from 'react';
6+
import { PanResponder, StyleSheet, View } from 'react-native';
7+
8+
const CIRCLE_SIZE = 80;
9+
10+
export default class DraggableCircle extends PureComponent {
11+
_panResponder = {};
12+
_previousLeft = 0;
13+
_previousTop = 0;
14+
_circleStyles = {};
15+
circle = (null: ?{ setNativeProps(props: Object): void });
16+
17+
constructor() {
18+
super();
19+
this._panResponder = PanResponder.create({
20+
onStartShouldSetPanResponder: this._handleStartShouldSetPanResponder,
21+
onMoveShouldSetPanResponder: this._handleMoveShouldSetPanResponder,
22+
onPanResponderGrant: this._handlePanResponderGrant,
23+
onPanResponderMove: this._handlePanResponderMove,
24+
onPanResponderRelease: this._handlePanResponderEnd,
25+
onPanResponderTerminate: this._handlePanResponderEnd
26+
});
27+
this._previousLeft = 20;
28+
this._previousTop = 84;
29+
this._circleStyles = {
30+
style: {
31+
left: this._previousLeft,
32+
top: this._previousTop,
33+
backgroundColor: 'green'
34+
}
35+
};
36+
}
37+
38+
componentDidMount() {
39+
this._updateNativeStyles();
40+
}
41+
42+
render() {
43+
return (
44+
<View style={styles.container}>
45+
<View ref={this._setCircleRef} style={styles.circle} {...this._panResponder.panHandlers} />
46+
</View>
47+
);
48+
}
49+
50+
_setCircleRef = circle => {
51+
this.circle = circle;
52+
};
53+
54+
_highlight() {
55+
this._circleStyles.style.backgroundColor = 'blue';
56+
this._updateNativeStyles();
57+
}
58+
59+
_unHighlight() {
60+
this._circleStyles.style.backgroundColor = 'green';
61+
this._updateNativeStyles();
62+
}
63+
64+
_updateNativeStyles() {
65+
this.circle && this.circle.setNativeProps(this._circleStyles);
66+
}
67+
68+
_handleStartShouldSetPanResponder = (e: Object, gestureState: Object): boolean => {
69+
// Should we become active when the user presses down on the circle?
70+
return true;
71+
};
72+
73+
_handleMoveShouldSetPanResponder = (e: Object, gestureState: Object): boolean => {
74+
// Should we become active when the user moves a touch over the circle?
75+
return true;
76+
};
77+
78+
_handlePanResponderGrant = (e: Object, gestureState: Object) => {
79+
this._highlight();
80+
};
81+
82+
_handlePanResponderMove = (e: Object, gestureState: Object) => {
83+
this._circleStyles.style.left = this._previousLeft + gestureState.dx;
84+
this._circleStyles.style.top = this._previousTop + gestureState.dy;
85+
this._updateNativeStyles();
86+
};
87+
88+
_handlePanResponderEnd = (e: Object, gestureState: Object) => {
89+
this._unHighlight();
90+
this._previousLeft += gestureState.dx;
91+
this._previousTop += gestureState.dy;
92+
};
93+
}
94+
95+
const styles = StyleSheet.create({
96+
circle: {
97+
width: CIRCLE_SIZE,
98+
height: CIRCLE_SIZE,
99+
borderRadius: CIRCLE_SIZE / 2,
100+
position: 'absolute',
101+
left: 0,
102+
top: 0,
103+
touchAction: 'none'
104+
},
105+
container: {
106+
flex: 1,
107+
minHeight: 400,
108+
paddingTop: 64
109+
}
110+
});
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import React, { Component } from 'react';
2+
import { StyleSheet, View, PanResponder } from 'react-native';
3+
4+
export default class LocationXY extends Component {
5+
constructor() {
6+
super();
7+
this.state = { translateX: 0 };
8+
this.panResponder = PanResponder.create({
9+
onStartShouldSetPanResponder: () => true,
10+
onStartShouldSetPanResponderCapture: () => true,
11+
onPanResponderMove: this._handlePanResponderMove,
12+
onPanResponderTerminationRequest: () => true
13+
});
14+
}
15+
16+
_handlePanResponderMove = (e, gestureState) => {
17+
console.log(e.nativeEvent.locationX, e.nativeEvent.locationY);
18+
this.setState(state => ({
19+
...state,
20+
translateX: gestureState.dx
21+
}));
22+
};
23+
24+
render() {
25+
const transform = { transform: [{ translateX: this.state.translateX }] };
26+
return (
27+
<View style={styles.app}>
28+
<View style={styles.outer} {...this.panResponder.panHandlers}>
29+
<View style={[styles.inner, transform]} />
30+
</View>
31+
</View>
32+
);
33+
}
34+
}
35+
36+
const styles = StyleSheet.create({
37+
app: {
38+
justifyContent: 'center',
39+
alignItems: 'center'
40+
},
41+
outer: {
42+
width: 250,
43+
height: 50,
44+
backgroundColor: 'skyblue'
45+
},
46+
inner: {
47+
width: 30,
48+
height: 30,
49+
backgroundColor: 'lightblue'
50+
}
51+
});
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { default as draggableCircle } from './DraggableCircle';
2+
export { default as locationXY } from './LocationXY';
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
# Responder Event System
2+
3+
The Responder Event System is a gesture system that manages the lifecycle of gestures. It was designed for [React Native](https://reactnative.dev/docs/next/gesture-responder-system) to help support the development of native-quality gestures. A pointer may transition through several different phases while the gesture is being determined (e.g., tap, scroll, swipe) and be used simultaneously alongside other pointers. The Responder Event System provides a single, global “interaction lock” on views. For a view to become the “responder” means that pointer interactions are exclusive to that view and none other. A view can negotiate to become the “responder” without requiring knowledge of other views.
4+
5+
## How it works
6+
7+
A view can become the "responder" after the following native events: `scroll`, `selectionchange`, `touchstart`, `touchmove`, `mousedown`, `mousemove`. If nothing is already the "responder", the event propagates to (capture) and from (bubble) the event target until a view returns `true` for `on*ShouldSetResponder(Capture)`.
8+
9+
If something is *already* the responder, the negotiation event propagates to (capture) and from (bubble) the lowest common ancestor of the event target and the current responder. Then negotiation happens between the current responder and the view that wants to become the responder.
10+
11+
## API
12+
13+
### useResponderEvents
14+
15+
The `useResponderEvents` hook takes a ref to a host element and an object of responder callbacks.
16+
17+
```js
18+
function View(props) {
19+
const hostRef = useRef(null);
20+
21+
const callbacks: ResponderCallbacks = {
22+
onMoveShouldSetResponder: props.onMoveShouldSetResponder,
23+
onMoveShouldSetResponderCapture: props.onMoveShouldSetResponderCapture,
24+
onResponderEnd: props.onResponderEnd,
25+
onResponderGrant: props.onResponderGrant,
26+
onResponderMove: props.onResponderMove,
27+
onResponderReject: props.onResponderReject,
28+
onResponderRelease: props.onResponderRelease,
29+
onResponderStart: props.onResponderStart,
30+
onResponderTerminate: props.onResponderTerminate,
31+
onResponderTerminationRequest: props.onResponderTerminationRequest,
32+
onScrollShouldSetResponder: props.onScrollShouldSetResponder,
33+
onScrollShouldSetResponderCapture: props.onScrollShouldSetResponderCapture,
34+
onSelectionChangeShouldSetResponder: props.onSelectionChangeShouldSetResponder,
35+
onSelectionChangeShouldSetResponderCapture: props.onSelectionChangeShouldSetResponderCapture,
36+
onStartShouldSetResponder: props.onStartShouldSetResponder,
37+
onStartShouldSetResponderCapture: props.onStartShouldSetResponderCapture
38+
}
39+
40+
useResponderEvents(hostRef, callbacks);
41+
42+
return (
43+
<div ref={hostRef} />
44+
);
45+
}
46+
```
47+
48+
### Responder negotiation
49+
50+
A view can become the responder by using the negotiation methods. During the capture phase the deepest node is called last. During the bubble phase the deepest node is called first. The capture phase should be used when a view wants to prevent a descendant from becoming the responder. The first view to return `true` from any of the `on*ShouldSetResponderCapture`/`on*ShouldSetResponder` methods will either become the responder or enter into negotiation with the existing responder.
51+
52+
N.B. If `stopPropagation` is called on the event for any of the negotiation methods, it only stops further negotiation within the Responder System. It will not stop the propagation of the native event (which has already bubbled to the `document` by this time.)
53+
54+
#### onStartShouldSetResponder / onStartShouldSetResponderCapture
55+
56+
On pointer down, should this view attempt to become the responder? If the view is not the responder, these methods may be called for every pointer start on the view.
57+
58+
#### onMoveShouldSetResponder / onMoveShouldSetResponderCapture
59+
60+
On pointer move, should this view attempt to become the responder? If the view is not the responder, these methods may be called for every pointer move on the view.
61+
62+
#### onScrollShouldSetResponder / onScrollShouldSetResponderCapture
63+
64+
On scroll, should this view attempt to become the responder? If the view is not the responder, these methods may be called for every scroll on the view.
65+
66+
#### onSelectionChangeShouldSetResponder / onSelectionChangeShouldSetResponderCapture
67+
68+
On text selection change, should this view attempt to become the responder? Does not capture or bubble and is only called on the view that is the first ancestor of the selection `anchorNode`.
69+
70+
#### onResponderTerminationRequest
71+
72+
The view is the responder, but another view now wants to become the responder. Should this view release the responder? Returning `true` allows the responder to be released.
73+
74+
### Responder transfer
75+
76+
If a view returns `true` for a negotiation method then it will either become the responder (if none exists) or be involved in the responder transfer. The following methods are called only for the views involved in the responder transfer (i.e., no bubbling.)
77+
78+
#### onResponderGrant
79+
80+
The view is granted the responder and is now responding to pointer events. The lifecycle methods will be called for this view. This is the point at which you should provide visual feedback for users that the interaction has begun.
81+
82+
#### onResponderReject
83+
84+
The view was not granted the responder. It was rejected because another view is already the responder and will not release it.
85+
86+
#### onResponderTerminate
87+
88+
The responder has been taken from this view. It may have been taken by another view after a call to `onResponderTerminationRequest`, or it might have been taken by the browser without asking (e.g., window blur, document scroll, context menu open). This is the point at which you should provide visual feedback for users that the interaction has been cancelled.
89+
90+
### Responder lifecycle
91+
92+
If a view is the responder, the following methods will be called only for this view (i.e., no bubbling.) These methods are *always* bookended by `onResponderGrant` (before) and either `onResponderRelease` or `onResponderTerminate` (after).
93+
94+
#### onResponderStart
95+
96+
A pointer down event occured on the screen. The responder is notified of all start events, even if the pointer target is not this view (i.e., additional pointers are being used). Therefore, this method may be called multiple times while the view is the responder.
97+
98+
#### onResponderMove
99+
100+
A pointer move event occured on the screen. The responder is notified of all move events, even if the pointer target is not this view (i.e., additional pointers are being used). Therefore, this method may be called multiple times while the view is the responder.
101+
102+
#### onResponderEnd
103+
104+
A pointer up event occured on the screen. The responder is notified of all end events, even if the pointer target is not this view (i.e., additional pointers are being used). Therefore, this method may be called multiple times while the view is the responder.
105+
106+
#### onResponderRelease
107+
108+
As soon as there are no more pointers that *started* inside descendants of the responder, this method is called on the responder and the interaction lock is released. This is the point at which you should provide visual feedback for users that the interaction is over.
109+
110+
### Responder events
111+
112+
Every method is called with a responder event. The type of the event is shown below. The `currentTarget` of the event is always `null` for the negotiation methods. Data dervied from the native events, e.g., the native `target` and pointer coordinates, can be used to determine the return value of the negotiation methods, etc.
113+
114+
## Types
115+
116+
```js
117+
type ResponderCallbacks = {
118+
onResponderEnd?: ?(e: ResponderEvent) => void,
119+
onResponderGrant?: ?(e: ResponderEvent) => void,
120+
onResponderMove?: ?(e: ResponderEvent) => void,
121+
onResponderRelease?: ?(e: ResponderEvent) => void,
122+
onResponderReject?: ?(e: ResponderEvent) => void,
123+
onResponderStart?: ?(e: ResponderEvent) => void,
124+
onResponderTerminate?: ?(e: ResponderEvent) => void,
125+
onResponderTerminationRequest?: ?(e: ResponderEvent) => boolean,
126+
onStartShouldSetResponder?: ?(e: ResponderEvent) => boolean,
127+
onStartShouldSetResponderCapture?: ?(e: ResponderEvent) => boolean,
128+
onMoveShouldSetResponder?: ?(e: ResponderEvent) => boolean,
129+
onMoveShouldSetResponderCapture?: ?(e: ResponderEvent) => boolean,
130+
onScrollShouldSetResponder?: ?(e: ResponderEvent) => boolean,
131+
onScrollShouldSetResponderCapture?: ?(e: ResponderEvent) => boolean,
132+
onSelectionChangeShouldSetResponder?: ?(e: ResponderEvent) => boolean,
133+
onSelectionChangeShouldSetResponderCapture?: ?(e: ResponderEvent) => boolean
134+
};
135+
```
136+
137+
```js
138+
type ResponderEvent = {
139+
// The DOM element acting as the responder view
140+
currentTarget: ?HTMLElement,
141+
defaultPrevented: boolean,
142+
eventPhase: ?number,
143+
isDefaultPrevented: () => boolean,
144+
isPropagationStopped: () => boolean,
145+
isTrusted: boolean,
146+
preventDefault: () => void,
147+
stopPropagation: () => void,
148+
nativeEvent: TouchEvent,
149+
persist: () => void,
150+
target: HTMLElement,
151+
timeStamp: number,
152+
touchHistory: $ReadOnly<{|
153+
indexOfSingleActiveTouch: number,
154+
mostRecentTimeStamp: number,
155+
numberActiveTouches: number,
156+
touchBank: Array<{|
157+
currentPageX: number,
158+
currentPageY: number,
159+
currentTimeStamp: number,
160+
previousPageX: number,
161+
previousPageY: number,
162+
previousTimeStamp: number,
163+
startPageX: number,
164+
startPageY: number,
165+
startTimeStamp: number,
166+
touchActive: boolean
167+
|}>
168+
|}>
169+
};
170+
```
171+
172+
```js
173+
type TouchEvent = {
174+
// Array of all touch events that have changed since the last event
175+
changedTouches: Array<Touch>,
176+
force: number,
177+
// ID of the touch
178+
identifier: number,
179+
// The X position of the pointer, relative to the currentTarget
180+
locationX: number,
181+
// The Y position of the pointer, relative to the currentTarget
182+
locationY: number,
183+
// The X position of the pointer, relative to the page
184+
pageX: number,
185+
// The Y position of the pointer, relative to the page
186+
pageY: number,
187+
// The DOM element receiving the pointer event
188+
target: HTMLElement,
189+
// A time identifier for the pointer, useful for velocity calculation
190+
timestamp: number,
191+
// Array of all current touches on the screen
192+
touches: Array<Touch>
193+
};
194+
```
195+
196+
```js
197+
type Touch = {
198+
force: number,
199+
identifier: number,
200+
locationX: number,
201+
locationY: number,
202+
pageX: number,
203+
pageY: number,
204+
target: HTMLElement,
205+
timestamp: number
206+
};
207+
```

0 commit comments

Comments
 (0)