/** * 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 */ 'use strict'; const BoundingDimensions = require('./BoundingDimensions'); const Platform = require('../../Utilities/Platform'); const Position = require('./Position'); const React = require('react'); const ReactNative = require('../../Renderer/shims/ReactNative'); const StyleSheet = require('../../StyleSheet/StyleSheet'); const TVEventHandler = require('../AppleTV/TVEventHandler'); const UIManager = require('../../ReactNative/UIManager'); const View = require('../View/View'); const SoundManager = require('../Sound/SoundManager'); const keyMirror = require('fbjs/lib/keyMirror'); const normalizeColor = require('../../StyleSheet/normalizeColor'); import type {EdgeInsetsProp} from '../../StyleSheet/EdgeInsetsPropType'; import type {PressEvent} from '../../Types/CoreEventTypes'; const extractSingleTouch = nativeEvent => { const touches = nativeEvent.touches; const changedTouches = nativeEvent.changedTouches; const hasTouches = touches && touches.length > 0; const hasChangedTouches = changedTouches && changedTouches.length > 0; return !hasTouches && hasChangedTouches ? changedTouches[0] : hasTouches ? touches[0] : nativeEvent; }; /** * `Touchable`: Taps done right. * * You hook your `ResponderEventPlugin` events into `Touchable`. `Touchable` * will measure time/geometry and tells you when to give feedback to the user. * * ====================== Touchable Tutorial =============================== * The `Touchable` mixin helps you handle the "press" interaction. It analyzes * the geometry of elements, and observes when another responder (scroll view * etc) has stolen the touch lock. It notifies your component when it should * give feedback to the user. (bouncing/highlighting/unhighlighting). * * - When a touch was activated (typically you highlight) * - When a touch was deactivated (typically you unhighlight) * - When a touch was "pressed" - a touch ended while still within the geometry * of the element, and no other element (like scroller) has "stolen" touch * lock ("responder") (Typically you bounce the element). * * A good tap interaction isn't as simple as you might think. There should be a * slight delay before showing a highlight when starting a touch. If a * subsequent touch move exceeds the boundary of the element, it should * unhighlight, but if that same touch is brought back within the boundary, it * should rehighlight again. A touch can move in and out of that boundary * several times, each time toggling highlighting, but a "press" is only * triggered if that touch ends while within the element's boundary and no * scroller (or anything else) has stolen the lock on touches. * * To create a new type of component that handles interaction using the * `Touchable` mixin, do the following: * * - Initialize the `Touchable` state. * * getInitialState: function() { * return merge(this.touchableGetInitialState(), yourComponentState); * } * * - Choose the rendered component who's touches should start the interactive * sequence. On that rendered node, forward all `Touchable` responder * handlers. You can choose any rendered node you like. Choose a node whose * hit target you'd like to instigate the interaction sequence: * * // In render function: * return ( * * * Even though the hit detection/interactions are triggered by the * wrapping (typically larger) node, we usually end up implementing * custom logic that highlights this inner one. * * * ); * * - You may set up your own handlers for each of these events, so long as you * also invoke the `touchable*` handlers inside of your custom handler. * * - Implement the handlers on your component class in order to provide * feedback to the user. See documentation for each of these class methods * that you should implement. * * touchableHandlePress: function() { * this.performBounceAnimation(); // or whatever you want to do. * }, * touchableHandleActivePressIn: function() { * this.beginHighlighting(...); // Whatever you like to convey activation * }, * touchableHandleActivePressOut: function() { * this.endHighlighting(...); // Whatever you like to convey deactivation * }, * * - There are more advanced methods you can implement (see documentation below): * touchableGetHighlightDelayMS: function() { * return 20; * } * // In practice, *always* use a predeclared constant (conserve memory). * touchableGetPressRectOffset: function() { * return {top: 20, left: 20, right: 20, bottom: 100}; * } */ /** * Touchable states. */ const States = keyMirror({ NOT_RESPONDER: null, // Not the responder RESPONDER_INACTIVE_PRESS_IN: null, // Responder, inactive, in the `PressRect` RESPONDER_INACTIVE_PRESS_OUT: null, // Responder, inactive, out of `PressRect` RESPONDER_ACTIVE_PRESS_IN: null, // Responder, active, in the `PressRect` RESPONDER_ACTIVE_PRESS_OUT: null, // Responder, active, out of `PressRect` RESPONDER_ACTIVE_LONG_PRESS_IN: null, // Responder, active, in the `PressRect`, after long press threshold RESPONDER_ACTIVE_LONG_PRESS_OUT: null, // Responder, active, out of `PressRect`, after long press threshold ERROR: null, }); type State = | typeof States.NOT_RESPONDER | typeof States.RESPONDER_INACTIVE_PRESS_IN | typeof States.RESPONDER_INACTIVE_PRESS_OUT | typeof States.RESPONDER_ACTIVE_PRESS_IN | typeof States.RESPONDER_ACTIVE_PRESS_OUT | typeof States.RESPONDER_ACTIVE_LONG_PRESS_IN | typeof States.RESPONDER_ACTIVE_LONG_PRESS_OUT | typeof States.ERROR; /* * Quick lookup map for states that are considered to be "active" */ const baseStatesConditions = { NOT_RESPONDER: false, RESPONDER_INACTIVE_PRESS_IN: false, RESPONDER_INACTIVE_PRESS_OUT: false, RESPONDER_ACTIVE_PRESS_IN: false, RESPONDER_ACTIVE_PRESS_OUT: false, RESPONDER_ACTIVE_LONG_PRESS_IN: false, RESPONDER_ACTIVE_LONG_PRESS_OUT: false, ERROR: false, }; const IsActive = { ...baseStatesConditions, RESPONDER_ACTIVE_PRESS_OUT: true, RESPONDER_ACTIVE_PRESS_IN: true, }; /** * Quick lookup for states that are considered to be "pressing" and are * therefore eligible to result in a "selection" if the press stops. */ const IsPressingIn = { ...baseStatesConditions, RESPONDER_INACTIVE_PRESS_IN: true, RESPONDER_ACTIVE_PRESS_IN: true, RESPONDER_ACTIVE_LONG_PRESS_IN: true, }; const IsLongPressingIn = { ...baseStatesConditions, RESPONDER_ACTIVE_LONG_PRESS_IN: true, }; /** * Inputs to the state machine. */ const Signals = keyMirror({ DELAY: null, RESPONDER_GRANT: null, RESPONDER_RELEASE: null, RESPONDER_TERMINATED: null, ENTER_PRESS_RECT: null, LEAVE_PRESS_RECT: null, LONG_PRESS_DETECTED: null, }); type Signal = | typeof Signals.DELAY | typeof Signals.RESPONDER_GRANT | typeof Signals.RESPONDER_RELEASE | typeof Signals.RESPONDER_TERMINATED | typeof Signals.ENTER_PRESS_RECT | typeof Signals.LEAVE_PRESS_RECT | typeof Signals.LONG_PRESS_DETECTED; /** * Mapping from States x Signals => States */ const Transitions = { NOT_RESPONDER: { DELAY: States.ERROR, RESPONDER_GRANT: States.RESPONDER_INACTIVE_PRESS_IN, RESPONDER_RELEASE: States.ERROR, RESPONDER_TERMINATED: States.ERROR, ENTER_PRESS_RECT: States.ERROR, LEAVE_PRESS_RECT: States.ERROR, LONG_PRESS_DETECTED: States.ERROR, }, RESPONDER_INACTIVE_PRESS_IN: { DELAY: States.RESPONDER_ACTIVE_PRESS_IN, RESPONDER_GRANT: States.ERROR, RESPONDER_RELEASE: States.NOT_RESPONDER, RESPONDER_TERMINATED: States.NOT_RESPONDER, ENTER_PRESS_RECT: States.RESPONDER_INACTIVE_PRESS_IN, LEAVE_PRESS_RECT: States.RESPONDER_INACTIVE_PRESS_OUT, LONG_PRESS_DETECTED: States.ERROR, }, RESPONDER_INACTIVE_PRESS_OUT: { DELAY: States.RESPONDER_ACTIVE_PRESS_OUT, RESPONDER_GRANT: States.ERROR, RESPONDER_RELEASE: States.NOT_RESPONDER, RESPONDER_TERMINATED: States.NOT_RESPONDER, ENTER_PRESS_RECT: States.RESPONDER_INACTIVE_PRESS_IN, LEAVE_PRESS_RECT: States.RESPONDER_INACTIVE_PRESS_OUT, LONG_PRESS_DETECTED: States.ERROR, }, RESPONDER_ACTIVE_PRESS_IN: { DELAY: States.ERROR, RESPONDER_GRANT: States.ERROR, RESPONDER_RELEASE: States.NOT_RESPONDER, RESPONDER_TERMINATED: States.NOT_RESPONDER, ENTER_PRESS_RECT: States.RESPONDER_ACTIVE_PRESS_IN, LEAVE_PRESS_RECT: States.RESPONDER_ACTIVE_PRESS_OUT, LONG_PRESS_DETECTED: States.RESPONDER_ACTIVE_LONG_PRESS_IN, }, RESPONDER_ACTIVE_PRESS_OUT: { DELAY: States.ERROR, RESPONDER_GRANT: States.ERROR, RESPONDER_RELEASE: States.NOT_RESPONDER, RESPONDER_TERMINATED: States.NOT_RESPONDER, ENTER_PRESS_RECT: States.RESPONDER_ACTIVE_PRESS_IN, LEAVE_PRESS_RECT: States.RESPONDER_ACTIVE_PRESS_OUT, LONG_PRESS_DETECTED: States.ERROR, }, RESPONDER_ACTIVE_LONG_PRESS_IN: { DELAY: States.ERROR, RESPONDER_GRANT: States.ERROR, RESPONDER_RELEASE: States.NOT_RESPONDER, RESPONDER_TERMINATED: States.NOT_RESPONDER, ENTER_PRESS_RECT: States.RESPONDER_ACTIVE_LONG_PRESS_IN, LEAVE_PRESS_RECT: States.RESPONDER_ACTIVE_LONG_PRESS_OUT, LONG_PRESS_DETECTED: States.RESPONDER_ACTIVE_LONG_PRESS_IN, }, RESPONDER_ACTIVE_LONG_PRESS_OUT: { DELAY: States.ERROR, RESPONDER_GRANT: States.ERROR, RESPONDER_RELEASE: States.NOT_RESPONDER, RESPONDER_TERMINATED: States.NOT_RESPONDER, ENTER_PRESS_RECT: States.RESPONDER_ACTIVE_LONG_PRESS_IN, LEAVE_PRESS_RECT: States.RESPONDER_ACTIVE_LONG_PRESS_OUT, LONG_PRESS_DETECTED: States.ERROR, }, error: { DELAY: States.NOT_RESPONDER, RESPONDER_GRANT: States.RESPONDER_INACTIVE_PRESS_IN, RESPONDER_RELEASE: States.NOT_RESPONDER, RESPONDER_TERMINATED: States.NOT_RESPONDER, ENTER_PRESS_RECT: States.NOT_RESPONDER, LEAVE_PRESS_RECT: States.NOT_RESPONDER, LONG_PRESS_DETECTED: States.NOT_RESPONDER, }, }; // ==== Typical Constants for integrating into UI components ==== // var HIT_EXPAND_PX = 20; // var HIT_VERT_OFFSET_PX = 10; const HIGHLIGHT_DELAY_MS = 130; const PRESS_EXPAND_PX = 20; const LONG_PRESS_THRESHOLD = 500; const LONG_PRESS_DELAY_MS = LONG_PRESS_THRESHOLD - HIGHLIGHT_DELAY_MS; const LONG_PRESS_ALLOWED_MOVEMENT = 10; // Default amount "active" region protrudes beyond box /** * By convention, methods prefixed with underscores are meant to be @private, * and not @protected. Mixers shouldn't access them - not even to provide them * as callback handlers. * * * ========== Geometry ========= * `Touchable` only assumes that there exists a `HitRect` node. The `PressRect` * is an abstract box that is extended beyond the `HitRect`. * * +--------------------------+ * | | - "Start" events in `HitRect` cause `HitRect` * | +--------------------+ | to become the responder. * | | +--------------+ | | - `HitRect` is typically expanded around * | | | | | | the `VisualRect`, but shifted downward. * | | | VisualRect | | | - After pressing down, after some delay, * | | | | | | and before letting up, the Visual React * | | +--------------+ | | will become "active". This makes it eligible * | | HitRect | | for being highlighted (so long as the * | +--------------------+ | press remains in the `PressRect`). * | PressRect o | * +----------------------|---+ * Out Region | * +-----+ This gap between the `HitRect` and * `PressRect` allows a touch to move far away * from the original hit rect, and remain * highlighted, and eligible for a "Press". * Customize this via * `touchableGetPressRectOffset()`. * * * * ======= State Machine ======= * * +-------------+ <---+ RESPONDER_RELEASE * |NOT_RESPONDER| * +-------------+ <---+ RESPONDER_TERMINATED * + * | RESPONDER_GRANT (HitRect) * v * +---------------------------+ DELAY +-------------------------+ T + DELAY +------------------------------+ * |RESPONDER_INACTIVE_PRESS_IN|+-------->|RESPONDER_ACTIVE_PRESS_IN| +------------> |RESPONDER_ACTIVE_LONG_PRESS_IN| * +---------------------------+ +-------------------------+ +------------------------------+ * + ^ + ^ + ^ * |LEAVE_ |ENTER_ |LEAVE_ |ENTER_ |LEAVE_ |ENTER_ * |PRESS_RECT |PRESS_RECT |PRESS_RECT |PRESS_RECT |PRESS_RECT |PRESS_RECT * | | | | | | * v + v + v + * +----------------------------+ DELAY +--------------------------+ +-------------------------------+ * |RESPONDER_INACTIVE_PRESS_OUT|+------->|RESPONDER_ACTIVE_PRESS_OUT| |RESPONDER_ACTIVE_LONG_PRESS_OUT| * +----------------------------+ +--------------------------+ +-------------------------------+ * * T + DELAY => LONG_PRESS_DELAY_MS + DELAY * * Not drawn are the side effects of each transition. The most important side * effect is the `touchableHandlePress` abstract method invocation that occurs * when a responder is released while in either of the "Press" states. * * The other important side effects are the highlight abstract method * invocations (internal callbacks) to be implemented by the mixer. * * * @lends Touchable.prototype */ const TouchableMixin = { componentDidMount: function() { if (!Platform.isTV) { return; } this._tvEventHandler = new TVEventHandler(); this._tvEventHandler.enable(this, function(cmp, evt) { const myTag = ReactNative.findNodeHandle(cmp); evt.dispatchConfig = {}; if (myTag === evt.tag) { if (evt.eventType === 'focus') { cmp.touchableHandleFocus(evt); } else if (evt.eventType === 'blur') { cmp.touchableHandleBlur(evt); } else if (evt.eventType === 'select' && Platform.OS !== 'android') { cmp.touchableHandlePress && !cmp.props.disabled && cmp.touchableHandlePress(evt); } } }); }, /** * Clear all timeouts on unmount */ componentWillUnmount: function() { if (this._tvEventHandler) { this._tvEventHandler.disable(); delete this._tvEventHandler; } this.touchableDelayTimeout && clearTimeout(this.touchableDelayTimeout); this.longPressDelayTimeout && clearTimeout(this.longPressDelayTimeout); this.pressOutDelayTimeout && clearTimeout(this.pressOutDelayTimeout); }, /** * It's prefer that mixins determine state in this way, having the class * explicitly mix the state in the one and only `getInitialState` method. * * @return {object} State object to be placed inside of * `this.state.touchable`. */ touchableGetInitialState: function(): $TEMPORARY$object<{| touchable: $TEMPORARY$object<{|responderID: null, touchState: void|}>, |}> { return { touchable: {touchState: undefined, responderID: null}, }; }, // ==== Hooks to Gesture Responder system ==== /** * Must return true if embedded in a native platform scroll view. */ touchableHandleResponderTerminationRequest: function(): any { return !this.props.rejectResponderTermination; }, /** * Must return true to start the process of `Touchable`. */ touchableHandleStartShouldSetResponder: function(): any { return !this.props.disabled; }, /** * Return true to cancel press on long press. */ touchableLongPressCancelsPress: function(): boolean { return true; }, /** * Place as callback for a DOM element's `onResponderGrant` event. * @param {SyntheticEvent} e Synthetic event from event system. * */ touchableHandleResponderGrant: function(e: PressEvent) { const dispatchID = e.currentTarget; // Since e is used in a callback invoked on another event loop // (as in setTimeout etc), we need to call e.persist() on the // event to make sure it doesn't get reused in the event object pool. e.persist(); this.pressOutDelayTimeout && clearTimeout(this.pressOutDelayTimeout); this.pressOutDelayTimeout = null; this.state.touchable.touchState = States.NOT_RESPONDER; this.state.touchable.responderID = dispatchID; this._receiveSignal(Signals.RESPONDER_GRANT, e); let delayMS = this.touchableGetHighlightDelayMS !== undefined ? Math.max(this.touchableGetHighlightDelayMS(), 0) : HIGHLIGHT_DELAY_MS; delayMS = isNaN(delayMS) ? HIGHLIGHT_DELAY_MS : delayMS; if (delayMS !== 0) { this.touchableDelayTimeout = setTimeout( this._handleDelay.bind(this, e), delayMS, ); } else { this._handleDelay(e); } let longDelayMS = this.touchableGetLongPressDelayMS !== undefined ? Math.max(this.touchableGetLongPressDelayMS(), 10) : LONG_PRESS_DELAY_MS; longDelayMS = isNaN(longDelayMS) ? LONG_PRESS_DELAY_MS : longDelayMS; this.longPressDelayTimeout = setTimeout( this._handleLongDelay.bind(this, e), longDelayMS + delayMS, ); }, /** * Place as callback for a DOM element's `onResponderRelease` event. */ touchableHandleResponderRelease: function(e: PressEvent) { this.pressInLocation = null; this._receiveSignal(Signals.RESPONDER_RELEASE, e); }, /** * Place as callback for a DOM element's `onResponderTerminate` event. */ touchableHandleResponderTerminate: function(e: PressEvent) { this.pressInLocation = null; this._receiveSignal(Signals.RESPONDER_TERMINATED, e); }, /** * Place as callback for a DOM element's `onResponderMove` event. */ touchableHandleResponderMove: function(e: PressEvent) { // Measurement may not have returned yet. if (!this.state.touchable.positionOnActivate) { return; } const positionOnActivate = this.state.touchable.positionOnActivate; const dimensionsOnActivate = this.state.touchable.dimensionsOnActivate; const pressRectOffset = this.touchableGetPressRectOffset ? this.touchableGetPressRectOffset() : { left: PRESS_EXPAND_PX, right: PRESS_EXPAND_PX, top: PRESS_EXPAND_PX, bottom: PRESS_EXPAND_PX, }; let pressExpandLeft = pressRectOffset.left; let pressExpandTop = pressRectOffset.top; let pressExpandRight = pressRectOffset.right; let pressExpandBottom = pressRectOffset.bottom; const hitSlop = this.touchableGetHitSlop ? this.touchableGetHitSlop() : null; if (hitSlop) { pressExpandLeft += hitSlop.left || 0; pressExpandTop += hitSlop.top || 0; pressExpandRight += hitSlop.right || 0; pressExpandBottom += hitSlop.bottom || 0; } const touch = extractSingleTouch(e.nativeEvent); const pageX = touch && touch.pageX; const pageY = touch && touch.pageY; if (this.pressInLocation) { const movedDistance = this._getDistanceBetweenPoints( pageX, pageY, this.pressInLocation.pageX, this.pressInLocation.pageY, ); if (movedDistance > LONG_PRESS_ALLOWED_MOVEMENT) { this._cancelLongPressDelayTimeout(); } } const isTouchWithinActive = pageX > positionOnActivate.left - pressExpandLeft && pageY > positionOnActivate.top - pressExpandTop && pageX < positionOnActivate.left + dimensionsOnActivate.width + pressExpandRight && pageY < positionOnActivate.top + dimensionsOnActivate.height + pressExpandBottom; if (isTouchWithinActive) { const prevState = this.state.touchable.touchState; this._receiveSignal(Signals.ENTER_PRESS_RECT, e); const curState = this.state.touchable.touchState; if ( curState === States.RESPONDER_INACTIVE_PRESS_IN && prevState !== States.RESPONDER_INACTIVE_PRESS_IN ) { // fix for t7967420 this._cancelLongPressDelayTimeout(); } } else { this._cancelLongPressDelayTimeout(); this._receiveSignal(Signals.LEAVE_PRESS_RECT, e); } }, /** * Invoked when the item receives focus. Mixers might override this to * visually distinguish the `VisualRect` so that the user knows that it * currently has the focus. Most platforms only support a single element being * focused at a time, in which case there may have been a previously focused * element that was blurred just prior to this. This can be overridden when * using `Touchable.Mixin.withoutDefaultFocusAndBlur`. */ touchableHandleFocus: function(e: Event) { this.props.onFocus && this.props.onFocus(e); }, /** * Invoked when the item loses focus. Mixers might override this to * visually distinguish the `VisualRect` so that the user knows that it * no longer has focus. Most platforms only support a single element being * focused at a time, in which case the focus may have moved to another. * This can be overridden when using * `Touchable.Mixin.withoutDefaultFocusAndBlur`. */ touchableHandleBlur: function(e: Event) { this.props.onBlur && this.props.onBlur(e); }, // ==== Abstract Application Callbacks ==== /** * Invoked when the item should be highlighted. Mixers should implement this * to visually distinguish the `VisualRect` so that the user knows that * releasing a touch will result in a "selection" (analog to click). * * @abstract * touchableHandleActivePressIn: function, */ /** * Invoked when the item is "active" (in that it is still eligible to become * a "select") but the touch has left the `PressRect`. Usually the mixer will * want to unhighlight the `VisualRect`. If the user (while pressing) moves * back into the `PressRect` `touchableHandleActivePressIn` will be invoked * again and the mixer should probably highlight the `VisualRect` again. This * event will not fire on an `touchEnd/mouseUp` event, only move events while * the user is depressing the mouse/touch. * * @abstract * touchableHandleActivePressOut: function */ /** * Invoked when the item is "selected" - meaning the interaction ended by * letting up while the item was either in the state * `RESPONDER_ACTIVE_PRESS_IN` or `RESPONDER_INACTIVE_PRESS_IN`. * * @abstract * touchableHandlePress: function */ /** * Invoked when the item is long pressed - meaning the interaction ended by * letting up while the item was in `RESPONDER_ACTIVE_LONG_PRESS_IN`. If * `touchableHandleLongPress` is *not* provided, `touchableHandlePress` will * be called as it normally is. If `touchableHandleLongPress` is provided, by * default any `touchableHandlePress` callback will not be invoked. To * override this default behavior, override `touchableLongPressCancelsPress` * to return false. As a result, `touchableHandlePress` will be called when * lifting up, even if `touchableHandleLongPress` has also been called. * * @abstract * touchableHandleLongPress: function */ /** * Returns the number of millis to wait before triggering a highlight. * * @abstract * touchableGetHighlightDelayMS: function */ /** * Returns the amount to extend the `HitRect` into the `PressRect`. Positive * numbers mean the size expands outwards. * * @abstract * touchableGetPressRectOffset: function */ // ==== Internal Logic ==== /** * Measures the `HitRect` node on activation. The Bounding rectangle is with * respect to viewport - not page, so adding the `pageXOffset/pageYOffset` * should result in points that are in the same coordinate system as an * event's `globalX/globalY` data values. * * - Consider caching this for the lifetime of the component, or possibly * being able to share this cache between any `ScrollMap` view. * * @sideeffects * @private */ _remeasureMetricsOnActivation: function() { const responderID = this.state.touchable.responderID; if (responderID == null) { return; } if (typeof responderID === 'number') { UIManager.measure(responderID, this._handleQueryLayout); } else { responderID.measure(this._handleQueryLayout); } }, _handleQueryLayout: function( l: number, t: number, w: number, h: number, globalX: number, globalY: number, ) { //don't do anything UIManager failed to measure node if (!l && !t && !w && !h && !globalX && !globalY) { return; } this.state.touchable.positionOnActivate && Position.release(this.state.touchable.positionOnActivate); this.state.touchable.dimensionsOnActivate && BoundingDimensions.release(this.state.touchable.dimensionsOnActivate); this.state.touchable.positionOnActivate = Position.getPooled( globalX, globalY, ); this.state.touchable.dimensionsOnActivate = BoundingDimensions.getPooled( w, h, ); }, _handleDelay: function(e: PressEvent) { this.touchableDelayTimeout = null; this._receiveSignal(Signals.DELAY, e); }, _handleLongDelay: function(e: PressEvent) { this.longPressDelayTimeout = null; const curState = this.state.touchable.touchState; if ( curState === States.RESPONDER_ACTIVE_PRESS_IN || curState === States.RESPONDER_ACTIVE_LONG_PRESS_IN ) { this._receiveSignal(Signals.LONG_PRESS_DETECTED, e); } }, /** * Receives a state machine signal, performs side effects of the transition * and stores the new state. Validates the transition as well. * * @param {Signals} signal State machine signal. * @throws Error if invalid state transition or unrecognized signal. * @sideeffects */ _receiveSignal: function(signal: Signal, e: PressEvent) { const responderID = this.state.touchable.responderID; const curState = this.state.touchable.touchState; const nextState = Transitions[curState] && Transitions[curState][signal]; if (!responderID && signal === Signals.RESPONDER_RELEASE) { return; } if (!nextState) { throw new Error( 'Unrecognized signal `' + signal + '` or state `' + curState + '` for Touchable responder `' + typeof this.state.touchable.responderID === 'number' ? this.state.touchable.responderID : 'host component' + '`', ); } if (nextState === States.ERROR) { throw new Error( 'Touchable cannot transition from `' + curState + '` to `' + signal + '` for responder `' + typeof this.state.touchable.responderID === 'number' ? this.state.touchable.responderID : '<>' + '`', ); } if (curState !== nextState) { this._performSideEffectsForTransition(curState, nextState, signal, e); this.state.touchable.touchState = nextState; } }, _cancelLongPressDelayTimeout: function() { this.longPressDelayTimeout && clearTimeout(this.longPressDelayTimeout); this.longPressDelayTimeout = null; }, _isHighlight: function(state: State): boolean { return ( state === States.RESPONDER_ACTIVE_PRESS_IN || state === States.RESPONDER_ACTIVE_LONG_PRESS_IN ); }, _savePressInLocation: function(e: PressEvent) { const touch = extractSingleTouch(e.nativeEvent); const pageX = touch && touch.pageX; const pageY = touch && touch.pageY; const locationX = touch && touch.locationX; const locationY = touch && touch.locationY; this.pressInLocation = {pageX, pageY, locationX, locationY}; }, _getDistanceBetweenPoints: function( aX: number, aY: number, bX: number, bY: number, ): number { const deltaX = aX - bX; const deltaY = aY - bY; return Math.sqrt(deltaX * deltaX + deltaY * deltaY); }, /** * Will perform a transition between touchable states, and identify any * highlighting or unhighlighting that must be performed for this particular * transition. * * @param {States} curState Current Touchable state. * @param {States} nextState Next Touchable state. * @param {Signal} signal Signal that triggered the transition. * @param {Event} e Native event. * @sideeffects */ _performSideEffectsForTransition: function( curState: State, nextState: State, signal: Signal, e: PressEvent, ) { const curIsHighlight = this._isHighlight(curState); const newIsHighlight = this._isHighlight(nextState); const isFinalSignal = signal === Signals.RESPONDER_TERMINATED || signal === Signals.RESPONDER_RELEASE; if (isFinalSignal) { this._cancelLongPressDelayTimeout(); } const isInitialTransition = curState === States.NOT_RESPONDER && nextState === States.RESPONDER_INACTIVE_PRESS_IN; const isActiveTransition = !IsActive[curState] && IsActive[nextState]; if (isInitialTransition || isActiveTransition) { this._remeasureMetricsOnActivation(); } if (IsPressingIn[curState] && signal === Signals.LONG_PRESS_DETECTED) { this.touchableHandleLongPress && this.touchableHandleLongPress(e); } if (newIsHighlight && !curIsHighlight) { this._startHighlight(e); } else if (!newIsHighlight && curIsHighlight) { this._endHighlight(e); } if (IsPressingIn[curState] && signal === Signals.RESPONDER_RELEASE) { const hasLongPressHandler = !!this.props.onLongPress; const pressIsLongButStillCallOnPress = IsLongPressingIn[curState] && // We *are* long pressing.. // But either has no long handler (!hasLongPressHandler || !this.touchableLongPressCancelsPress()); // or we're told to ignore it. const shouldInvokePress = !IsLongPressingIn[curState] || pressIsLongButStillCallOnPress; if (shouldInvokePress && this.touchableHandlePress) { if (!newIsHighlight && !curIsHighlight) { // we never highlighted because of delay, but we should highlight now this._startHighlight(e); this._endHighlight(e); } if (Platform.OS === 'android' && !this.props.touchSoundDisabled) { SoundManager.playTouchSound(); } this.touchableHandlePress(e); } } this.touchableDelayTimeout && clearTimeout(this.touchableDelayTimeout); this.touchableDelayTimeout = null; }, _startHighlight: function(e: PressEvent) { this._savePressInLocation(e); this.touchableHandleActivePressIn && this.touchableHandleActivePressIn(e); }, _endHighlight: function(e: PressEvent) { if (this.touchableHandleActivePressOut) { if ( this.touchableGetPressOutDelayMS && this.touchableGetPressOutDelayMS() ) { this.pressOutDelayTimeout = setTimeout(() => { this.touchableHandleActivePressOut(e); }, this.touchableGetPressOutDelayMS()); } else { this.touchableHandleActivePressOut(e); } } }, withoutDefaultFocusAndBlur: ({}: $TEMPORARY$object<{||}>), }; /** * Provide an optional version of the mixin where `touchableHandleFocus` and * `touchableHandleBlur` can be overridden. This allows appropriate defaults to * be set on TV platforms, without breaking existing implementations of * `Touchable`. */ const { touchableHandleFocus, touchableHandleBlur, ...TouchableMixinWithoutDefaultFocusAndBlur } = TouchableMixin; TouchableMixin.withoutDefaultFocusAndBlur = TouchableMixinWithoutDefaultFocusAndBlur; const Touchable = { Mixin: TouchableMixin, TOUCH_TARGET_DEBUG: false, // Highlights all touchable targets. Toggle with Inspector. /** * Renders a debugging overlay to visualize touch target with hitSlop (might not work on Android). */ renderDebugView: ({ color, hitSlop, }: { color: string | number, hitSlop: EdgeInsetsProp, ... }): null | React.Node => { if (!Touchable.TOUCH_TARGET_DEBUG) { return null; } if (!__DEV__) { throw Error( 'Touchable.TOUCH_TARGET_DEBUG should not be enabled in prod!', ); } const debugHitSlopStyle = {}; hitSlop = hitSlop || {top: 0, bottom: 0, left: 0, right: 0}; for (const key in hitSlop) { debugHitSlopStyle[key] = -hitSlop[key]; } const normalizedColor = normalizeColor(color); if (typeof normalizedColor !== 'number') { return null; } const hexColor = '#' + ('00000000' + normalizedColor.toString(16)).substr(-8); return ( =0.111.0 site=react_native_fb) This comment suppresses * an error found when Flow v0.111 was deployed. To see the error, * delete this comment and run Flow. */ { borderColor: hexColor.slice(0, -2) + '55', // More opaque backgroundColor: hexColor.slice(0, -2) + '0F', // Less opaque ...debugHitSlopStyle, }, ]} /> ); }, }; const styles = StyleSheet.create({ debug: { position: 'absolute', borderWidth: 1, borderStyle: 'dashed', }, }); module.exports = Touchable;