123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872 |
- /**
- * 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 strict-local
- * @format
- */
- 'use strict';
- import {isHoverEnabled} from './HoverState';
- import invariant from 'invariant';
- import SoundManager from '../Components/Sound/SoundManager';
- import {normalizeRect, type RectOrSize} from '../StyleSheet/Rect';
- import type {
- BlurEvent,
- FocusEvent,
- PressEvent,
- MouseEvent,
- } from '../Types/CoreEventTypes';
- import Platform from '../Utilities/Platform';
- import UIManager from '../ReactNative/UIManager';
- import type {HostComponent} from '../Renderer/shims/ReactNativeTypes';
- import * as React from 'react';
- export type PressabilityConfig = $ReadOnly<{|
- /**
- * Whether a press gesture can be interrupted by a parent gesture such as a
- * scroll event. Defaults to true.
- */
- cancelable?: ?boolean,
- /**
- * Whether to disable initialization of the press gesture.
- */
- disabled?: ?boolean,
- /**
- * Amount to extend the `VisualRect` by to create `HitRect`.
- */
- hitSlop?: ?RectOrSize,
- /**
- * Amount to extend the `HitRect` by to create `PressRect`.
- */
- pressRectOffset?: ?RectOrSize,
- /**
- * Whether to disable the systemm sound when `onPress` fires on Android.
- **/
- android_disableSound?: ?boolean,
- /**
- * Duration to wait after hover in before calling `onHoverIn`.
- */
- delayHoverIn?: ?number,
- /**
- * Duration to wait after hover out before calling `onHoverOut`.
- */
- delayHoverOut?: ?number,
- /**
- * Duration (in addition to `delayPressIn`) after which a press gesture is
- * considered a long press gesture. Defaults to 500 (milliseconds).
- */
- delayLongPress?: ?number,
- /**
- * Duration to wait after press down before calling `onPressIn`.
- */
- delayPressIn?: ?number,
- /**
- * Duration to wait after letting up before calling `onPressOut`.
- */
- delayPressOut?: ?number,
- /**
- * Minimum duration to wait between calling `onPressIn` and `onPressOut`.
- */
- minPressDuration?: ?number,
- /**
- * Called after the element loses focus.
- */
- onBlur?: ?(event: BlurEvent) => mixed,
- /**
- * Called after the element is focused.
- */
- onFocus?: ?(event: FocusEvent) => mixed,
- /**
- * Called when the hover is activated to provide visual feedback.
- */
- onHoverIn?: ?(event: MouseEvent) => mixed,
- /**
- * Called when the hover is deactivated to undo visual feedback.
- */
- onHoverOut?: ?(event: MouseEvent) => mixed,
- /**
- * Called when a long press gesture has been triggered.
- */
- onLongPress?: ?(event: PressEvent) => mixed,
- /**
- * Called when a press gestute has been triggered.
- */
- onPress?: ?(event: PressEvent) => mixed,
- /**
- * Called when the press is activated to provide visual feedback.
- */
- onPressIn?: ?(event: PressEvent) => mixed,
- /**
- * Called when the press location moves. (This should rarely be used.)
- */
- onPressMove?: ?(event: PressEvent) => mixed,
- /**
- * Called when the press is deactivated to undo visual feedback.
- */
- onPressOut?: ?(event: PressEvent) => mixed,
- /**
- * Returns whether a long press gesture should cancel the press gesture.
- * Defaults to true.
- */
- onLongPressShouldCancelPress_DEPRECATED?: ?() => boolean,
- /**
- * If `cancelable` is set, this will be ignored.
- *
- * Returns whether to yield to a lock termination request (e.g. if a native
- * scroll gesture attempts to steal the responder lock).
- */
- onResponderTerminationRequest_DEPRECATED?: ?() => boolean,
- /**
- * If `disabled` is set, this will be ignored.
- *
- * Returns whether to start a press gesture.
- *
- * @deprecated
- */
- onStartShouldSetResponder_DEPRECATED?: ?() => boolean,
- |}>;
- export type EventHandlers = $ReadOnly<{|
- onBlur: (event: BlurEvent) => void,
- onClick: (event: PressEvent) => void,
- onFocus: (event: FocusEvent) => void,
- onMouseEnter?: (event: MouseEvent) => void,
- onMouseLeave?: (event: MouseEvent) => void,
- onResponderGrant: (event: PressEvent) => void,
- onResponderMove: (event: PressEvent) => void,
- onResponderRelease: (event: PressEvent) => void,
- onResponderTerminate: (event: PressEvent) => void,
- onResponderTerminationRequest: () => boolean,
- onStartShouldSetResponder: () => boolean,
- |}>;
- type TouchState =
- | 'NOT_RESPONDER'
- | 'RESPONDER_INACTIVE_PRESS_IN'
- | 'RESPONDER_INACTIVE_PRESS_OUT'
- | 'RESPONDER_ACTIVE_PRESS_IN'
- | 'RESPONDER_ACTIVE_PRESS_OUT'
- | 'RESPONDER_ACTIVE_LONG_PRESS_IN'
- | 'RESPONDER_ACTIVE_LONG_PRESS_OUT'
- | 'ERROR';
- type TouchSignal =
- | 'DELAY'
- | 'RESPONDER_GRANT'
- | 'RESPONDER_RELEASE'
- | 'RESPONDER_TERMINATED'
- | 'ENTER_PRESS_RECT'
- | 'LEAVE_PRESS_RECT'
- | 'LONG_PRESS_DETECTED';
- const Transitions = Object.freeze({
- NOT_RESPONDER: {
- DELAY: 'ERROR',
- RESPONDER_GRANT: 'RESPONDER_INACTIVE_PRESS_IN',
- RESPONDER_RELEASE: 'ERROR',
- RESPONDER_TERMINATED: 'ERROR',
- ENTER_PRESS_RECT: 'ERROR',
- LEAVE_PRESS_RECT: 'ERROR',
- LONG_PRESS_DETECTED: 'ERROR',
- },
- RESPONDER_INACTIVE_PRESS_IN: {
- DELAY: 'RESPONDER_ACTIVE_PRESS_IN',
- RESPONDER_GRANT: 'ERROR',
- RESPONDER_RELEASE: 'NOT_RESPONDER',
- RESPONDER_TERMINATED: 'NOT_RESPONDER',
- ENTER_PRESS_RECT: 'RESPONDER_INACTIVE_PRESS_IN',
- LEAVE_PRESS_RECT: 'RESPONDER_INACTIVE_PRESS_OUT',
- LONG_PRESS_DETECTED: 'ERROR',
- },
- RESPONDER_INACTIVE_PRESS_OUT: {
- DELAY: 'RESPONDER_ACTIVE_PRESS_OUT',
- RESPONDER_GRANT: 'ERROR',
- RESPONDER_RELEASE: 'NOT_RESPONDER',
- RESPONDER_TERMINATED: 'NOT_RESPONDER',
- ENTER_PRESS_RECT: 'RESPONDER_INACTIVE_PRESS_IN',
- LEAVE_PRESS_RECT: 'RESPONDER_INACTIVE_PRESS_OUT',
- LONG_PRESS_DETECTED: 'ERROR',
- },
- RESPONDER_ACTIVE_PRESS_IN: {
- DELAY: 'ERROR',
- RESPONDER_GRANT: 'ERROR',
- RESPONDER_RELEASE: 'NOT_RESPONDER',
- RESPONDER_TERMINATED: 'NOT_RESPONDER',
- ENTER_PRESS_RECT: 'RESPONDER_ACTIVE_PRESS_IN',
- LEAVE_PRESS_RECT: 'RESPONDER_ACTIVE_PRESS_OUT',
- LONG_PRESS_DETECTED: 'RESPONDER_ACTIVE_LONG_PRESS_IN',
- },
- RESPONDER_ACTIVE_PRESS_OUT: {
- DELAY: 'ERROR',
- RESPONDER_GRANT: 'ERROR',
- RESPONDER_RELEASE: 'NOT_RESPONDER',
- RESPONDER_TERMINATED: 'NOT_RESPONDER',
- ENTER_PRESS_RECT: 'RESPONDER_ACTIVE_PRESS_IN',
- LEAVE_PRESS_RECT: 'RESPONDER_ACTIVE_PRESS_OUT',
- LONG_PRESS_DETECTED: 'ERROR',
- },
- RESPONDER_ACTIVE_LONG_PRESS_IN: {
- DELAY: 'ERROR',
- RESPONDER_GRANT: 'ERROR',
- RESPONDER_RELEASE: 'NOT_RESPONDER',
- RESPONDER_TERMINATED: 'NOT_RESPONDER',
- ENTER_PRESS_RECT: 'RESPONDER_ACTIVE_LONG_PRESS_IN',
- LEAVE_PRESS_RECT: 'RESPONDER_ACTIVE_LONG_PRESS_OUT',
- LONG_PRESS_DETECTED: 'RESPONDER_ACTIVE_LONG_PRESS_IN',
- },
- RESPONDER_ACTIVE_LONG_PRESS_OUT: {
- DELAY: 'ERROR',
- RESPONDER_GRANT: 'ERROR',
- RESPONDER_RELEASE: 'NOT_RESPONDER',
- RESPONDER_TERMINATED: 'NOT_RESPONDER',
- ENTER_PRESS_RECT: 'RESPONDER_ACTIVE_LONG_PRESS_IN',
- LEAVE_PRESS_RECT: 'RESPONDER_ACTIVE_LONG_PRESS_OUT',
- LONG_PRESS_DETECTED: 'ERROR',
- },
- ERROR: {
- DELAY: 'NOT_RESPONDER',
- RESPONDER_GRANT: 'RESPONDER_INACTIVE_PRESS_IN',
- RESPONDER_RELEASE: 'NOT_RESPONDER',
- RESPONDER_TERMINATED: 'NOT_RESPONDER',
- ENTER_PRESS_RECT: 'NOT_RESPONDER',
- LEAVE_PRESS_RECT: 'NOT_RESPONDER',
- LONG_PRESS_DETECTED: 'NOT_RESPONDER',
- },
- });
- const isActiveSignal = signal =>
- signal === 'RESPONDER_ACTIVE_PRESS_IN' ||
- signal === 'RESPONDER_ACTIVE_LONG_PRESS_IN';
- const isActivationSignal = signal =>
- signal === 'RESPONDER_ACTIVE_PRESS_OUT' ||
- signal === 'RESPONDER_ACTIVE_PRESS_IN';
- const isPressInSignal = signal =>
- signal === 'RESPONDER_INACTIVE_PRESS_IN' ||
- signal === 'RESPONDER_ACTIVE_PRESS_IN' ||
- signal === 'RESPONDER_ACTIVE_LONG_PRESS_IN';
- const isTerminalSignal = signal =>
- signal === 'RESPONDER_TERMINATED' || signal === 'RESPONDER_RELEASE';
- const DEFAULT_LONG_PRESS_DELAY_MS = 500;
- const DEFAULT_PRESS_RECT_OFFSETS = {
- bottom: 30,
- left: 20,
- right: 20,
- top: 20,
- };
- const DEFAULT_MIN_PRESS_DURATION = 130;
- /**
- * Pressability implements press handling capabilities.
- *
- * =========================== Pressability Tutorial ===========================
- *
- * The `Pressability` class helps you create press interactions by analyzing the
- * geometry of elements and observing when another responder (e.g. ScrollView)
- * has stolen the touch lock. It offers hooks for your component to provide
- * interaction feedback to the user:
- *
- * - When a press has activated (e.g. highlight an element)
- * - When a press has deactivated (e.g. un-highlight an element)
- * - When a press sould trigger an action, meaning it activated and deactivated
- * while within the geometry of the element without the lock being stolen.
- *
- * A high quality interaction isn't as simple as you might think. There should
- * be a slight delay before activation. Moving your finger beyond an element's
- * bounds should trigger deactivation, but moving the same finger back within an
- * element's bounds should trigger reactivation.
- *
- * In order to use `Pressability`, do the following:
- *
- * 1. Instantiate `Pressability` and store it on your component's state.
- *
- * state = {
- * pressability: new Pressability({
- * // ...
- * }),
- * };
- *
- * 2. Choose the rendered component who should collect the press events. On that
- * element, spread `pressability.getEventHandlers()` into its props.
- *
- * return (
- * <View {...this.state.pressability.getEventHandlers()} />
- * );
- *
- * 3. Reset `Pressability` when your component unmounts.
- *
- * componentWillUnmount() {
- * this.state.pressability.reset();
- * }
- *
- * ==================== Pressability Implementation Details ====================
- *
- * `Pressability` only assumes that there exists a `HitRect` node. The `PressRect`
- * is an abstract box that is extended beyond the `HitRect`.
- *
- * # Geometry
- *
- * ┌────────────────────────┐
- * │ ┌──────────────────┐ │ - Presses start anywhere within `HitRect`, which
- * │ │ ┌────────────┐ │ │ is expanded via the prop `hitSlop`.
- * │ │ │ VisualRect │ │ │
- * │ │ └────────────┘ │ │ - When pressed down for sufficient amount of time
- * │ │ HitRect │ │ before letting up, `VisualRect` activates for
- * │ └──────────────────┘ │ as long as the press stays within `PressRect`.
- * │ PressRect o │
- * └────────────────────│───┘
- * Out Region └────── `PressRect`, which is expanded via the prop
- * `pressRectOffset`, allows presses to move
- * beyond `HitRect` while maintaining activation
- * and being eligible for a "press".
- *
- * # State Machine
- *
- * ┌───────────────┐ ◀──── RESPONDER_RELEASE
- * │ NOT_RESPONDER │
- * └───┬───────────┘ ◀──── RESPONDER_TERMINATED
- * │
- * │ RESPONDER_GRANT (HitRect)
- * │
- * ▼
- * ┌─────────────────────┐ ┌───────────────────┐ ┌───────────────────┐
- * │ RESPONDER_INACTIVE_ │ DELAY │ RESPONDER_ACTIVE_ │ T + DELAY │ RESPONDER_ACTIVE_ │
- * │ PRESS_IN ├────────▶ │ PRESS_IN ├────────────▶ │ LONG_PRESS_IN │
- * └─┬───────────────────┘ └─┬─────────────────┘ └─┬─────────────────┘
- * │ ▲ │ ▲ │ ▲
- * │LEAVE_ │ │LEAVE_ │ │LEAVE_ │
- * │PRESS_RECT │ENTER_ │PRESS_RECT │ENTER_ │PRESS_RECT │ENTER_
- * │ │PRESS_RECT │ │PRESS_RECT │ │PRESS_RECT
- * ▼ │ ▼ │ ▼ │
- * ┌─────────────┴───────┐ ┌─────────────┴─────┐ ┌─────────────┴─────┐
- * │ RESPONDER_INACTIVE_ │ DELAY │ RESPONDER_ACTIVE_ │ │ RESPONDER_ACTIVE_ │
- * │ PRESS_OUT ├────────▶ │ PRESS_OUT │ │ LONG_PRESS_OUT │
- * └─────────────────────┘ └───────────────────┘ └───────────────────┘
- *
- * T + DELAY => LONG_PRESS_DELAY + DELAY
- *
- * Not drawn are the side effects of each transition. The most important side
- * effect is the invocation of `onPress` and `onLongPress` that occur when a
- * responder is release while in the "press in" states.
- */
- export default class Pressability {
- _config: PressabilityConfig;
- _eventHandlers: ?EventHandlers = null;
- _hoverInDelayTimeout: ?TimeoutID = null;
- _hoverOutDelayTimeout: ?TimeoutID = null;
- _isHovered: boolean = false;
- _longPressDelayTimeout: ?TimeoutID = null;
- _pressDelayTimeout: ?TimeoutID = null;
- _pressOutDelayTimeout: ?TimeoutID = null;
- _responderID: ?number | React.ElementRef<HostComponent<mixed>> = null;
- _responderRegion: ?$ReadOnly<{|
- bottom: number,
- left: number,
- right: number,
- top: number,
- |}> = null;
- _touchActivatePosition: ?$ReadOnly<{|
- pageX: number,
- pageY: number,
- |}>;
- _touchActivateTime: ?number;
- _touchState: TouchState = 'NOT_RESPONDER';
- constructor(config: PressabilityConfig) {
- this.configure(config);
- }
- configure(config: PressabilityConfig): void {
- this._config = config;
- }
- /**
- * Resets any pending timers. This should be called on unmount.
- */
- reset(): void {
- this._cancelHoverInDelayTimeout();
- this._cancelHoverOutDelayTimeout();
- this._cancelLongPressDelayTimeout();
- this._cancelPressDelayTimeout();
- this._cancelPressOutDelayTimeout();
- }
- /**
- * Returns a set of props to spread into the interactive element.
- */
- getEventHandlers(): EventHandlers {
- if (this._eventHandlers == null) {
- this._eventHandlers = this._createEventHandlers();
- }
- return this._eventHandlers;
- }
- _createEventHandlers(): EventHandlers {
- const focusEventHandlers = {
- onBlur: (event: BlurEvent): void => {
- const {onBlur} = this._config;
- if (onBlur != null) {
- onBlur(event);
- }
- },
- onFocus: (event: FocusEvent): void => {
- const {onFocus} = this._config;
- if (onFocus != null) {
- onFocus(event);
- }
- },
- };
- const responderEventHandlers = {
- onStartShouldSetResponder: (): boolean => {
- const {disabled} = this._config;
- if (disabled == null) {
- const {onStartShouldSetResponder_DEPRECATED} = this._config;
- return onStartShouldSetResponder_DEPRECATED == null
- ? true
- : onStartShouldSetResponder_DEPRECATED();
- }
- return !disabled;
- },
- onResponderGrant: (event: PressEvent): void => {
- event.persist();
- this._cancelPressOutDelayTimeout();
- this._responderID = event.currentTarget;
- this._touchState = 'NOT_RESPONDER';
- this._receiveSignal('RESPONDER_GRANT', event);
- const delayPressIn = normalizeDelay(this._config.delayPressIn);
- if (delayPressIn > 0) {
- this._pressDelayTimeout = setTimeout(() => {
- this._receiveSignal('DELAY', event);
- }, delayPressIn);
- } else {
- this._receiveSignal('DELAY', event);
- }
- const delayLongPress = normalizeDelay(
- this._config.delayLongPress,
- 10,
- DEFAULT_LONG_PRESS_DELAY_MS - delayPressIn,
- );
- this._longPressDelayTimeout = setTimeout(() => {
- this._handleLongPress(event);
- }, delayLongPress + delayPressIn);
- },
- onResponderMove: (event: PressEvent): void => {
- if (this._config.onPressMove != null) {
- this._config.onPressMove(event);
- }
- // Region may not have finished being measured, yet.
- const responderRegion = this._responderRegion;
- if (responderRegion == null) {
- return;
- }
- const touch = getTouchFromPressEvent(event);
- if (touch == null) {
- this._cancelLongPressDelayTimeout();
- this._receiveSignal('LEAVE_PRESS_RECT', event);
- return;
- }
- if (this._touchActivatePosition != null) {
- const deltaX = this._touchActivatePosition.pageX - touch.pageX;
- const deltaY = this._touchActivatePosition.pageY - touch.pageY;
- if (Math.hypot(deltaX, deltaY) > 10) {
- this._cancelLongPressDelayTimeout();
- }
- }
- if (this._isTouchWithinResponderRegion(touch, responderRegion)) {
- this._receiveSignal('ENTER_PRESS_RECT', event);
- } else {
- this._cancelLongPressDelayTimeout();
- this._receiveSignal('LEAVE_PRESS_RECT', event);
- }
- },
- onResponderRelease: (event: PressEvent): void => {
- this._receiveSignal('RESPONDER_RELEASE', event);
- },
- onResponderTerminate: (event: PressEvent): void => {
- this._receiveSignal('RESPONDER_TERMINATED', event);
- },
- onResponderTerminationRequest: (): boolean => {
- const {cancelable} = this._config;
- if (cancelable == null) {
- const {onResponderTerminationRequest_DEPRECATED} = this._config;
- return onResponderTerminationRequest_DEPRECATED == null
- ? true
- : onResponderTerminationRequest_DEPRECATED();
- }
- return cancelable;
- },
- onClick: (event: PressEvent): void => {
- const {onPress} = this._config;
- if (onPress != null) {
- onPress(event);
- }
- },
- };
- if (process.env.NODE_ENV === 'test') {
- // We are setting this in order to find this node in ReactNativeTestTools
- responderEventHandlers.onStartShouldSetResponder.testOnly_pressabilityConfig = () =>
- this._config;
- }
- const mouseEventHandlers =
- Platform.OS === 'ios' || Platform.OS === 'android'
- ? null
- : {
- onMouseEnter: (event: MouseEvent): void => {
- if (isHoverEnabled()) {
- this._isHovered = true;
- this._cancelHoverOutDelayTimeout();
- const {onHoverIn} = this._config;
- if (onHoverIn != null) {
- const delayHoverIn = normalizeDelay(
- this._config.delayHoverIn,
- );
- if (delayHoverIn > 0) {
- this._hoverInDelayTimeout = setTimeout(() => {
- onHoverIn(event);
- }, delayHoverIn);
- } else {
- onHoverIn(event);
- }
- }
- }
- },
- onMouseLeave: (event: MouseEvent): void => {
- if (this._isHovered) {
- this._isHovered = false;
- this._cancelHoverInDelayTimeout();
- const {onHoverOut} = this._config;
- if (onHoverOut != null) {
- const delayHoverOut = normalizeDelay(
- this._config.delayHoverOut,
- );
- if (delayHoverOut > 0) {
- this._hoverInDelayTimeout = setTimeout(() => {
- onHoverOut(event);
- }, delayHoverOut);
- } else {
- onHoverOut(event);
- }
- }
- }
- },
- };
- return {
- ...focusEventHandlers,
- ...responderEventHandlers,
- ...mouseEventHandlers,
- };
- }
- /**
- * Receives a state machine signal, performs side effects of the transition
- * and stores the new state. Validates the transition as well.
- */
- _receiveSignal(signal: TouchSignal, event: PressEvent): void {
- const prevState = this._touchState;
- const nextState = Transitions[prevState]?.[signal];
- if (this._responderID == null && signal === 'RESPONDER_RELEASE') {
- return;
- }
- invariant(
- nextState != null && nextState !== 'ERROR',
- 'Pressability: Invalid signal `%s` for state `%s` on responder: %s',
- signal,
- prevState,
- typeof this._responderID === 'number'
- ? this._responderID
- : '<<host component>>',
- );
- if (prevState !== nextState) {
- this._performTransitionSideEffects(prevState, nextState, signal, event);
- this._touchState = nextState;
- }
- }
- /**
- * Performs a transition between touchable states and identify any activations
- * or deactivations (and callback invocations).
- */
- _performTransitionSideEffects(
- prevState: TouchState,
- nextState: TouchState,
- signal: TouchSignal,
- event: PressEvent,
- ): void {
- if (isTerminalSignal(signal)) {
- this._touchActivatePosition = null;
- this._cancelLongPressDelayTimeout();
- }
- const isInitialTransition =
- prevState === 'NOT_RESPONDER' &&
- nextState === 'RESPONDER_INACTIVE_PRESS_IN';
- const isActivationTransiton =
- !isActivationSignal(prevState) && isActivationSignal(nextState);
- if (isInitialTransition || isActivationTransiton) {
- this._measureResponderRegion();
- }
- if (isPressInSignal(prevState) && signal === 'LONG_PRESS_DETECTED') {
- const {onLongPress} = this._config;
- if (onLongPress != null) {
- onLongPress(event);
- }
- }
- const isPrevActive = isActiveSignal(prevState);
- const isNextActive = isActiveSignal(nextState);
- if (!isPrevActive && isNextActive) {
- this._activate(event);
- } else if (isPrevActive && !isNextActive) {
- this._deactivate(event);
- }
- if (isPressInSignal(prevState) && signal === 'RESPONDER_RELEASE') {
- const {onLongPress, onPress, android_disableSound} = this._config;
- if (onPress != null) {
- const isPressCanceledByLongPress =
- onLongPress != null &&
- prevState === 'RESPONDER_ACTIVE_LONG_PRESS_IN' &&
- this._shouldLongPressCancelPress();
- if (!isPressCanceledByLongPress) {
- // If we never activated (due to delays), activate and deactivate now.
- if (!isNextActive && !isPrevActive) {
- this._activate(event);
- this._deactivate(event);
- }
- if (Platform.OS === 'android' && android_disableSound !== true) {
- SoundManager.playTouchSound();
- }
- onPress(event);
- }
- }
- }
- this._cancelPressDelayTimeout();
- }
- _activate(event: PressEvent): void {
- const {onPressIn} = this._config;
- const touch = getTouchFromPressEvent(event);
- this._touchActivatePosition = {
- pageX: touch.pageX,
- pageY: touch.pageY,
- };
- this._touchActivateTime = Date.now();
- if (onPressIn != null) {
- onPressIn(event);
- }
- }
- _deactivate(event: PressEvent): void {
- const {onPressOut} = this._config;
- if (onPressOut != null) {
- const minPressDuration = normalizeDelay(
- this._config.minPressDuration,
- 0,
- DEFAULT_MIN_PRESS_DURATION,
- );
- const pressDuration = Date.now() - (this._touchActivateTime ?? 0);
- const delayPressOut = Math.max(
- minPressDuration - pressDuration,
- normalizeDelay(this._config.delayPressOut),
- );
- if (delayPressOut > 0) {
- this._pressOutDelayTimeout = setTimeout(() => {
- onPressOut(event);
- }, delayPressOut);
- } else {
- onPressOut(event);
- }
- }
- this._touchActivateTime = null;
- }
- _measureResponderRegion(): void {
- if (this._responderID == null) {
- return;
- }
- if (typeof this._responderID === 'number') {
- UIManager.measure(this._responderID, this._measureCallback);
- } else {
- this._responderID.measure(this._measureCallback);
- }
- }
- _measureCallback = (left, top, width, height, pageX, pageY) => {
- if (!left && !top && !width && !height && !pageX && !pageY) {
- return;
- }
- this._responderRegion = {
- bottom: pageY + height,
- left: pageX,
- right: pageX + width,
- top: pageY,
- };
- };
- _isTouchWithinResponderRegion(
- touch: $PropertyType<PressEvent, 'nativeEvent'>,
- responderRegion: $ReadOnly<{|
- bottom: number,
- left: number,
- right: number,
- top: number,
- |}>,
- ): boolean {
- const hitSlop = normalizeRect(this._config.hitSlop);
- const pressRectOffset = normalizeRect(this._config.pressRectOffset);
- let regionBottom = responderRegion.bottom;
- let regionLeft = responderRegion.left;
- let regionRight = responderRegion.right;
- let regionTop = responderRegion.top;
- if (hitSlop != null) {
- if (hitSlop.bottom != null) {
- regionBottom += hitSlop.bottom;
- }
- if (hitSlop.left != null) {
- regionLeft -= hitSlop.left;
- }
- if (hitSlop.right != null) {
- regionRight += hitSlop.right;
- }
- if (hitSlop.top != null) {
- regionTop -= hitSlop.top;
- }
- }
- regionBottom +=
- pressRectOffset?.bottom ?? DEFAULT_PRESS_RECT_OFFSETS.bottom;
- regionLeft -= pressRectOffset?.left ?? DEFAULT_PRESS_RECT_OFFSETS.left;
- regionRight += pressRectOffset?.right ?? DEFAULT_PRESS_RECT_OFFSETS.right;
- regionTop -= pressRectOffset?.top ?? DEFAULT_PRESS_RECT_OFFSETS.top;
- return (
- touch.pageX > regionLeft &&
- touch.pageX < regionRight &&
- touch.pageY > regionTop &&
- touch.pageY < regionBottom
- );
- }
- _handleLongPress(event: PressEvent): void {
- if (
- this._touchState === 'RESPONDER_ACTIVE_PRESS_IN' ||
- this._touchState === 'RESPONDER_ACTIVE_LONG_PRESS_IN'
- ) {
- this._receiveSignal('LONG_PRESS_DETECTED', event);
- }
- }
- _shouldLongPressCancelPress(): boolean {
- return (
- this._config.onLongPressShouldCancelPress_DEPRECATED == null ||
- this._config.onLongPressShouldCancelPress_DEPRECATED()
- );
- }
- _cancelHoverInDelayTimeout(): void {
- if (this._hoverInDelayTimeout != null) {
- clearTimeout(this._hoverInDelayTimeout);
- this._hoverInDelayTimeout = null;
- }
- }
- _cancelHoverOutDelayTimeout(): void {
- if (this._hoverOutDelayTimeout != null) {
- clearTimeout(this._hoverOutDelayTimeout);
- this._hoverOutDelayTimeout = null;
- }
- }
- _cancelLongPressDelayTimeout(): void {
- if (this._longPressDelayTimeout != null) {
- clearTimeout(this._longPressDelayTimeout);
- this._longPressDelayTimeout = null;
- }
- }
- _cancelPressDelayTimeout(): void {
- if (this._pressDelayTimeout != null) {
- clearTimeout(this._pressDelayTimeout);
- this._pressDelayTimeout = null;
- }
- }
- _cancelPressOutDelayTimeout(): void {
- if (this._pressOutDelayTimeout != null) {
- clearTimeout(this._pressOutDelayTimeout);
- this._pressOutDelayTimeout = null;
- }
- }
- }
- function normalizeDelay(delay: ?number, min = 0, fallback = 0): number {
- return Math.max(min, delay ?? fallback);
- }
- const getTouchFromPressEvent = (event: PressEvent) => {
- const {changedTouches, touches} = event.nativeEvent;
- if (touches != null && touches.length > 0) {
- return touches[0];
- }
- if (changedTouches != null && changedTouches.length > 0) {
- return changedTouches[0];
- }
- return event.nativeEvent;
- };
|