mirror of
https://github.com/zoriya/react-native-web.git
synced 2026-06-03 10:46:19 +00:00
Alternative PanResponder API
This commit is contained in:
@@ -0,0 +1,362 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*
|
||||||
|
* @flow
|
||||||
|
* @format
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PAN RESPONDER
|
||||||
|
*
|
||||||
|
* `PanResponder` uses the Responder System to reconcile several touches into
|
||||||
|
* a single gesture. It makes single-touch gestures resilient to extra touches,
|
||||||
|
* and can be used to recognize simple multi-touch gestures. For each handler,
|
||||||
|
* it provides a `gestureState` object alongside the ResponderEvent object.
|
||||||
|
*
|
||||||
|
* By default, `PanResponder` holds an `InteractionManager` handle to block
|
||||||
|
* long-running JS events from interrupting active gestures.
|
||||||
|
*
|
||||||
|
* A graphical explanation of the touch data flow:
|
||||||
|
*
|
||||||
|
* +----------------------------+ +--------------------------------+
|
||||||
|
* | ResponderTouchHistoryStore | |TouchHistoryMath |
|
||||||
|
* +----------------------------+ +----------+---------------------+
|
||||||
|
* |Global store of touchHistory| |Allocation-less math util |
|
||||||
|
* |including activeness, start | |on touch history (centroids |
|
||||||
|
* |position, prev/cur position.| |and multitouch movement etc) |
|
||||||
|
* | | | |
|
||||||
|
* +----^-----------------------+ +----^---------------------------+
|
||||||
|
* | |
|
||||||
|
* | (records relevant history |
|
||||||
|
* | of touches relevant for |
|
||||||
|
* | implementing higher level |
|
||||||
|
* | gestures) |
|
||||||
|
* | |
|
||||||
|
* +----+-----------------------+ +----|---------------------------+
|
||||||
|
* | ResponderEventPlugin | | | Your App/Component |
|
||||||
|
* +----------------------------+ +----|---------------------------+
|
||||||
|
* |Negotiates which view gets | Low level | | High level |
|
||||||
|
* |onResponderMove events. | events w/ | +-+-------+ events w/ |
|
||||||
|
* |Also records history into | touchHistory| | Pan | multitouch + |
|
||||||
|
* |ResponderTouchHistoryStore. +---------------->Responder+-----> accumulative|
|
||||||
|
* +----------------------------+ attached to | | | distance and |
|
||||||
|
* each event | +---------+ velocity. |
|
||||||
|
* | |
|
||||||
|
* | |
|
||||||
|
* +--------------------------------+
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import type { PressEvent } from '../../vendor/react-native/Types/CoreEventTypes';
|
||||||
|
|
||||||
|
import InteractionManager from '../InteractionManager';
|
||||||
|
import TouchHistoryMath from '../../vendor/react-native/TouchHistoryMath';
|
||||||
|
|
||||||
|
export type GestureState = {|
|
||||||
|
// ID of the gestureState; persisted as long as there's a pointer on screen
|
||||||
|
stateID: number,
|
||||||
|
// The latest screen coordinates of the gesture
|
||||||
|
x: number,
|
||||||
|
// The latest screen coordinates of the gesture
|
||||||
|
y: number,
|
||||||
|
// The screen coordinates of the responder grant
|
||||||
|
initialX: number,
|
||||||
|
// The screen coordinates of the responder grant
|
||||||
|
initialY: number,
|
||||||
|
// Accumulated distance of the gesture since it started
|
||||||
|
deltaX: number,
|
||||||
|
// Accumulated distance of the gesture since it started
|
||||||
|
deltaY: number,
|
||||||
|
// Current velocity of the gesture
|
||||||
|
velocityX: number,
|
||||||
|
// Current velocity of the gesture
|
||||||
|
velocityY: number,
|
||||||
|
// Number of touches currently on screen
|
||||||
|
numberActiveTouches: number,
|
||||||
|
_accountsForMovesUpTo: number
|
||||||
|
|};
|
||||||
|
|
||||||
|
type ActiveCallback = (event: PressEvent, gestureState: GestureState) => boolean;
|
||||||
|
type PassiveCallback = (event: PressEvent, gestureState: GestureState) => void;
|
||||||
|
|
||||||
|
type PanResponderConfig = $ReadOnly<{|
|
||||||
|
// Negotiate for the responder
|
||||||
|
onMoveShouldSetResponder?: ?ActiveCallback,
|
||||||
|
onMoveShouldSetResponderCapture?: ?ActiveCallback,
|
||||||
|
onStartShouldSetResponder?: ?ActiveCallback,
|
||||||
|
onStartShouldSetResponderCapture?: ?ActiveCallback,
|
||||||
|
onPanTerminationRequest?: ?ActiveCallback,
|
||||||
|
// Gesture started
|
||||||
|
onPanGrant?: ?PassiveCallback,
|
||||||
|
// Gesture rejected
|
||||||
|
onPanReject?: ?PassiveCallback,
|
||||||
|
// A pointer touched the screen
|
||||||
|
onPanStart?: ?PassiveCallback,
|
||||||
|
// A pointer moved
|
||||||
|
onPanMove?: ?PassiveCallback,
|
||||||
|
// A pointer was removed from the screen
|
||||||
|
onPanEnd?: ?PassiveCallback,
|
||||||
|
// All pointers removed, gesture successful
|
||||||
|
onPanRelease?: ?PassiveCallback,
|
||||||
|
// Gesture cancelled
|
||||||
|
onPanTerminate?: ?PassiveCallback
|
||||||
|
|}>;
|
||||||
|
|
||||||
|
const {
|
||||||
|
currentCentroidX,
|
||||||
|
currentCentroidY,
|
||||||
|
currentCentroidXOfTouchesChangedAfter,
|
||||||
|
currentCentroidYOfTouchesChangedAfter,
|
||||||
|
previousCentroidXOfTouchesChangedAfter,
|
||||||
|
previousCentroidYOfTouchesChangedAfter
|
||||||
|
} = TouchHistoryMath;
|
||||||
|
|
||||||
|
const PanResponder = {
|
||||||
|
_initializeGestureState(gestureState: GestureState) {
|
||||||
|
gestureState.x = 0;
|
||||||
|
gestureState.y = 0;
|
||||||
|
gestureState.initialX = 0;
|
||||||
|
gestureState.initialY = 0;
|
||||||
|
gestureState.deltaX = 0;
|
||||||
|
gestureState.deltaY = 0;
|
||||||
|
gestureState.velocityX = 0;
|
||||||
|
gestureState.velocityY = 0;
|
||||||
|
gestureState.numberActiveTouches = 0;
|
||||||
|
// All `gestureState` accounts for timeStamps up until:
|
||||||
|
gestureState._accountsForMovesUpTo = 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Take all recently moved touches, calculate how the centroid has changed just for those
|
||||||
|
* recently moved touches, and append that change to an accumulator. This is
|
||||||
|
* to (at least) handle the case where the user is moving three fingers, and
|
||||||
|
* then one of the fingers stops but the other two continue.
|
||||||
|
*
|
||||||
|
* This is very different than taking all of the recently moved touches and
|
||||||
|
* storing their centroid as `dx/dy`. For correctness, we must *accumulate
|
||||||
|
* changes* in the centroid of recently moved touches.
|
||||||
|
*
|
||||||
|
* There is also some nuance with how we handle multiple moved touches in a
|
||||||
|
* single event. Multiple touches generate two 'move' events, each of
|
||||||
|
* them triggering `onResponderMove`. But with the way `PanResponder` works,
|
||||||
|
* all of the gesture inference is performed on the first dispatch, since it
|
||||||
|
* looks at all of the touches. Therefore, `PanResponder` does not call
|
||||||
|
* `onResponderMove` passed the first dispatch. This diverges from the
|
||||||
|
* typical responder callback pattern (without using `PanResponder`), but
|
||||||
|
* avoids more dispatches than necessary.
|
||||||
|
*
|
||||||
|
* When moving two touches in opposite directions, the cumulative
|
||||||
|
* distance is zero in each dimension. When two touches move in parallel five
|
||||||
|
* pixels in the same direction, the cumulative distance is five, not ten. If
|
||||||
|
* two touches start, one moves five in a direction, then stops and the other
|
||||||
|
* touch moves fives in the same direction, the cumulative distance is ten.
|
||||||
|
*
|
||||||
|
* This logic requires a kind of processing of time "clusters" of touch events
|
||||||
|
* so that two touch moves that essentially occur in parallel but move every
|
||||||
|
* other frame respectively, are considered part of the same movement.
|
||||||
|
*
|
||||||
|
* x/y: If a move event has been observed, `(x, y)` is the centroid of the most
|
||||||
|
* recently moved "cluster" of active touches.
|
||||||
|
* deltaX/deltaY: Cumulative touch distance. Accounts for touch moves that are
|
||||||
|
* clustered together in time, moving the same direction. Only valid when
|
||||||
|
* currently responder (otherwise, it only represents the drag distance below
|
||||||
|
* the threshold).
|
||||||
|
*/
|
||||||
|
_updateGestureStateOnMove(
|
||||||
|
gestureState: GestureState,
|
||||||
|
touchHistory: $PropertyType<PressEvent, 'touchHistory'>
|
||||||
|
) {
|
||||||
|
const movedAfter = gestureState._accountsForMovesUpTo;
|
||||||
|
const prevX = previousCentroidXOfTouchesChangedAfter(touchHistory, movedAfter);
|
||||||
|
const prevY = previousCentroidYOfTouchesChangedAfter(touchHistory, movedAfter);
|
||||||
|
const prevDeltaX = gestureState.deltaX;
|
||||||
|
const prevDeltaY = gestureState.deltaY;
|
||||||
|
|
||||||
|
const x = currentCentroidXOfTouchesChangedAfter(touchHistory, movedAfter);
|
||||||
|
const y = currentCentroidYOfTouchesChangedAfter(touchHistory, movedAfter);
|
||||||
|
const deltaX = prevDeltaX + (x - prevX);
|
||||||
|
const deltaY = prevDeltaY + (y - prevY);
|
||||||
|
// TODO: This must be filtered intelligently.
|
||||||
|
const dt = touchHistory.mostRecentTimeStamp - gestureState._accountsForMovesUpTo;
|
||||||
|
|
||||||
|
gestureState.deltaX = deltaX;
|
||||||
|
gestureState.deltaY = deltaY;
|
||||||
|
gestureState.numberActiveTouches = touchHistory.numberActiveTouches;
|
||||||
|
gestureState.velocityX = (deltaX - prevDeltaX) / dt;
|
||||||
|
gestureState.velocityY = (deltaY - prevDeltaY) / dt;
|
||||||
|
gestureState.x = x;
|
||||||
|
gestureState.y = y;
|
||||||
|
gestureState._accountsForMovesUpTo = touchHistory.mostRecentTimeStamp;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enhanced versions of all of the responder callbacks that provide not only
|
||||||
|
* the `ResponderEvent`, but also the `PanResponder` gesture state.
|
||||||
|
*
|
||||||
|
* In general, for events that have capture equivalents, we update the
|
||||||
|
* gestureState once in the capture phase and can use it in the bubble phase
|
||||||
|
* as well.
|
||||||
|
*/
|
||||||
|
create(config: PanResponderConfig) {
|
||||||
|
const interactionState = {
|
||||||
|
handle: (null: ?number)
|
||||||
|
};
|
||||||
|
const gestureState: GestureState = {
|
||||||
|
// Useful for debugging
|
||||||
|
stateID: Math.random(),
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
initialX: 0,
|
||||||
|
initialY: 0,
|
||||||
|
deltaX: 0,
|
||||||
|
deltaY: 0,
|
||||||
|
velocityX: 0,
|
||||||
|
velocityY: 0,
|
||||||
|
numberActiveTouches: 0,
|
||||||
|
_accountsForMovesUpTo: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
onStartShouldSetResponder,
|
||||||
|
onStartShouldSetResponderCapture,
|
||||||
|
onMoveShouldSetResponder,
|
||||||
|
onMoveShouldSetResponderCapture,
|
||||||
|
onPanGrant,
|
||||||
|
onPanStart,
|
||||||
|
onPanMove,
|
||||||
|
onPanEnd,
|
||||||
|
onPanRelease,
|
||||||
|
onPanReject,
|
||||||
|
onPanTerminate,
|
||||||
|
onPanTerminationRequest
|
||||||
|
} = config;
|
||||||
|
|
||||||
|
const panHandlers = {
|
||||||
|
onStartShouldSetResponder(event: PressEvent): boolean {
|
||||||
|
return onStartShouldSetResponder != null
|
||||||
|
? onStartShouldSetResponder(event, gestureState)
|
||||||
|
: false;
|
||||||
|
},
|
||||||
|
onMoveShouldSetResponder(event: PressEvent): boolean {
|
||||||
|
return onMoveShouldSetResponder != null
|
||||||
|
? onMoveShouldSetResponder(event, gestureState)
|
||||||
|
: false;
|
||||||
|
},
|
||||||
|
onStartShouldSetResponderCapture(event: PressEvent): boolean {
|
||||||
|
// TODO: Actually, we should reinitialize the state any time
|
||||||
|
// touches.length increases from 0 active to > 0 active.
|
||||||
|
if (event.nativeEvent.touches.length === 1) {
|
||||||
|
PanResponder._initializeGestureState(gestureState);
|
||||||
|
}
|
||||||
|
gestureState.numberActiveTouches = event.touchHistory.numberActiveTouches;
|
||||||
|
return onStartShouldSetResponderCapture != null
|
||||||
|
? onStartShouldSetResponderCapture(event, gestureState)
|
||||||
|
: false;
|
||||||
|
},
|
||||||
|
|
||||||
|
onMoveShouldSetResponderCapture(event: PressEvent): boolean {
|
||||||
|
const touchHistory = event.touchHistory;
|
||||||
|
// Responder system incorrectly dispatches should* to current responder
|
||||||
|
// Filter out any touch moves past the first one - we would have
|
||||||
|
// already processed multi-touch geometry during the first event.
|
||||||
|
// NOTE: commented out because new responder system should get it right.
|
||||||
|
//if (gestureState._accountsForMovesUpTo === touchHistory.mostRecentTimeStamp) {
|
||||||
|
// return false;
|
||||||
|
//}
|
||||||
|
PanResponder._updateGestureStateOnMove(gestureState, touchHistory);
|
||||||
|
return onMoveShouldSetResponderCapture != null
|
||||||
|
? onMoveShouldSetResponderCapture(event, gestureState)
|
||||||
|
: false;
|
||||||
|
},
|
||||||
|
|
||||||
|
onResponderGrant(event: PressEvent): void {
|
||||||
|
if (!interactionState.handle) {
|
||||||
|
interactionState.handle = InteractionManager.createInteractionHandle();
|
||||||
|
}
|
||||||
|
gestureState.initialX = currentCentroidX(event.touchHistory);
|
||||||
|
gestureState.initialY = currentCentroidY(event.touchHistory);
|
||||||
|
gestureState.deltaX = 0;
|
||||||
|
gestureState.deltaY = 0;
|
||||||
|
if (onPanGrant != null) {
|
||||||
|
onPanGrant(event, gestureState);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onResponderReject(event: PressEvent): void {
|
||||||
|
clearInteractionHandle(interactionState, onPanReject, event, gestureState);
|
||||||
|
},
|
||||||
|
|
||||||
|
onResponderStart(event: PressEvent): void {
|
||||||
|
const { numberActiveTouches } = event.touchHistory;
|
||||||
|
gestureState.numberActiveTouches = numberActiveTouches;
|
||||||
|
if (onPanStart != null) {
|
||||||
|
onPanStart(event, gestureState);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onResponderMove(event: PressEvent): void {
|
||||||
|
const touchHistory = event.touchHistory;
|
||||||
|
// Guard against the dispatch of two touch moves when there are two
|
||||||
|
// simultaneously changed touches.
|
||||||
|
if (gestureState._accountsForMovesUpTo === touchHistory.mostRecentTimeStamp) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Filter out any touch moves past the first one - we would have
|
||||||
|
// already processed multi-touch geometry during the first event.
|
||||||
|
PanResponder._updateGestureStateOnMove(gestureState, touchHistory);
|
||||||
|
if (onPanMove != null) {
|
||||||
|
onPanMove(event, gestureState);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onResponderEnd(event: PressEvent): void {
|
||||||
|
const { numberActiveTouches } = event.touchHistory;
|
||||||
|
gestureState.numberActiveTouches = numberActiveTouches;
|
||||||
|
clearInteractionHandle(interactionState, onPanEnd, event, gestureState);
|
||||||
|
},
|
||||||
|
|
||||||
|
onResponderRelease(event: PressEvent): void {
|
||||||
|
clearInteractionHandle(interactionState, onPanRelease, event, gestureState);
|
||||||
|
PanResponder._initializeGestureState(gestureState);
|
||||||
|
},
|
||||||
|
|
||||||
|
onResponderTerminate(event: PressEvent): void {
|
||||||
|
clearInteractionHandle(interactionState, onPanTerminate, event, gestureState);
|
||||||
|
PanResponder._initializeGestureState(gestureState);
|
||||||
|
},
|
||||||
|
|
||||||
|
onResponderTerminationRequest(event: PressEvent): boolean {
|
||||||
|
return onPanTerminationRequest != null
|
||||||
|
? onPanTerminationRequest(event, gestureState)
|
||||||
|
: true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
panHandlers,
|
||||||
|
getInteractionHandle(): ?number {
|
||||||
|
return interactionState.handle;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function clearInteractionHandle(
|
||||||
|
interactionState: { handle: ?number },
|
||||||
|
callback: ?(ActiveCallback | PassiveCallback),
|
||||||
|
event: PressEvent,
|
||||||
|
gestureState: GestureState
|
||||||
|
) {
|
||||||
|
if (interactionState.handle) {
|
||||||
|
InteractionManager.clearInteractionHandle(interactionState.handle);
|
||||||
|
interactionState.handle = null;
|
||||||
|
}
|
||||||
|
if (callback) {
|
||||||
|
callback(event, gestureState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PanResponder;
|
||||||
Reference in New Issue
Block a user