123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984 |
- /**
- * 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 (
- * <View
- * onStartShouldSetResponder={this.touchableHandleStartShouldSetResponder}
- * onResponderTerminationRequest={this.touchableHandleResponderTerminationRequest}
- * onResponderGrant={this.touchableHandleResponderGrant}
- * onResponderMove={this.touchableHandleResponderMove}
- * onResponderRelease={this.touchableHandleResponderRelease}
- * onResponderTerminate={this.touchableHandleResponderTerminate}>
- * <View>
- * 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.
- * </View>
- * </View>
- * );
- *
- * - 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
- : '<<host component>>' + '`',
- );
- }
- 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 (
- <View
- pointerEvents="none"
- style={[
- styles.debug,
- /* $FlowFixMe(>=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;
|