Touchable.js 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984
  1. /**
  2. * Copyright (c) Facebook, Inc. and its affiliates.
  3. *
  4. * This source code is licensed under the MIT license found in the
  5. * LICENSE file in the root directory of this source tree.
  6. *
  7. * @flow
  8. * @format
  9. */
  10. 'use strict';
  11. const BoundingDimensions = require('./BoundingDimensions');
  12. const Platform = require('../../Utilities/Platform');
  13. const Position = require('./Position');
  14. const React = require('react');
  15. const ReactNative = require('../../Renderer/shims/ReactNative');
  16. const StyleSheet = require('../../StyleSheet/StyleSheet');
  17. const TVEventHandler = require('../AppleTV/TVEventHandler');
  18. const UIManager = require('../../ReactNative/UIManager');
  19. const View = require('../View/View');
  20. const SoundManager = require('../Sound/SoundManager');
  21. const keyMirror = require('fbjs/lib/keyMirror');
  22. const normalizeColor = require('../../StyleSheet/normalizeColor');
  23. import type {EdgeInsetsProp} from '../../StyleSheet/EdgeInsetsPropType';
  24. import type {PressEvent} from '../../Types/CoreEventTypes';
  25. const extractSingleTouch = nativeEvent => {
  26. const touches = nativeEvent.touches;
  27. const changedTouches = nativeEvent.changedTouches;
  28. const hasTouches = touches && touches.length > 0;
  29. const hasChangedTouches = changedTouches && changedTouches.length > 0;
  30. return !hasTouches && hasChangedTouches
  31. ? changedTouches[0]
  32. : hasTouches
  33. ? touches[0]
  34. : nativeEvent;
  35. };
  36. /**
  37. * `Touchable`: Taps done right.
  38. *
  39. * You hook your `ResponderEventPlugin` events into `Touchable`. `Touchable`
  40. * will measure time/geometry and tells you when to give feedback to the user.
  41. *
  42. * ====================== Touchable Tutorial ===============================
  43. * The `Touchable` mixin helps you handle the "press" interaction. It analyzes
  44. * the geometry of elements, and observes when another responder (scroll view
  45. * etc) has stolen the touch lock. It notifies your component when it should
  46. * give feedback to the user. (bouncing/highlighting/unhighlighting).
  47. *
  48. * - When a touch was activated (typically you highlight)
  49. * - When a touch was deactivated (typically you unhighlight)
  50. * - When a touch was "pressed" - a touch ended while still within the geometry
  51. * of the element, and no other element (like scroller) has "stolen" touch
  52. * lock ("responder") (Typically you bounce the element).
  53. *
  54. * A good tap interaction isn't as simple as you might think. There should be a
  55. * slight delay before showing a highlight when starting a touch. If a
  56. * subsequent touch move exceeds the boundary of the element, it should
  57. * unhighlight, but if that same touch is brought back within the boundary, it
  58. * should rehighlight again. A touch can move in and out of that boundary
  59. * several times, each time toggling highlighting, but a "press" is only
  60. * triggered if that touch ends while within the element's boundary and no
  61. * scroller (or anything else) has stolen the lock on touches.
  62. *
  63. * To create a new type of component that handles interaction using the
  64. * `Touchable` mixin, do the following:
  65. *
  66. * - Initialize the `Touchable` state.
  67. *
  68. * getInitialState: function() {
  69. * return merge(this.touchableGetInitialState(), yourComponentState);
  70. * }
  71. *
  72. * - Choose the rendered component who's touches should start the interactive
  73. * sequence. On that rendered node, forward all `Touchable` responder
  74. * handlers. You can choose any rendered node you like. Choose a node whose
  75. * hit target you'd like to instigate the interaction sequence:
  76. *
  77. * // In render function:
  78. * return (
  79. * <View
  80. * onStartShouldSetResponder={this.touchableHandleStartShouldSetResponder}
  81. * onResponderTerminationRequest={this.touchableHandleResponderTerminationRequest}
  82. * onResponderGrant={this.touchableHandleResponderGrant}
  83. * onResponderMove={this.touchableHandleResponderMove}
  84. * onResponderRelease={this.touchableHandleResponderRelease}
  85. * onResponderTerminate={this.touchableHandleResponderTerminate}>
  86. * <View>
  87. * Even though the hit detection/interactions are triggered by the
  88. * wrapping (typically larger) node, we usually end up implementing
  89. * custom logic that highlights this inner one.
  90. * </View>
  91. * </View>
  92. * );
  93. *
  94. * - You may set up your own handlers for each of these events, so long as you
  95. * also invoke the `touchable*` handlers inside of your custom handler.
  96. *
  97. * - Implement the handlers on your component class in order to provide
  98. * feedback to the user. See documentation for each of these class methods
  99. * that you should implement.
  100. *
  101. * touchableHandlePress: function() {
  102. * this.performBounceAnimation(); // or whatever you want to do.
  103. * },
  104. * touchableHandleActivePressIn: function() {
  105. * this.beginHighlighting(...); // Whatever you like to convey activation
  106. * },
  107. * touchableHandleActivePressOut: function() {
  108. * this.endHighlighting(...); // Whatever you like to convey deactivation
  109. * },
  110. *
  111. * - There are more advanced methods you can implement (see documentation below):
  112. * touchableGetHighlightDelayMS: function() {
  113. * return 20;
  114. * }
  115. * // In practice, *always* use a predeclared constant (conserve memory).
  116. * touchableGetPressRectOffset: function() {
  117. * return {top: 20, left: 20, right: 20, bottom: 100};
  118. * }
  119. */
  120. /**
  121. * Touchable states.
  122. */
  123. const States = keyMirror({
  124. NOT_RESPONDER: null, // Not the responder
  125. RESPONDER_INACTIVE_PRESS_IN: null, // Responder, inactive, in the `PressRect`
  126. RESPONDER_INACTIVE_PRESS_OUT: null, // Responder, inactive, out of `PressRect`
  127. RESPONDER_ACTIVE_PRESS_IN: null, // Responder, active, in the `PressRect`
  128. RESPONDER_ACTIVE_PRESS_OUT: null, // Responder, active, out of `PressRect`
  129. RESPONDER_ACTIVE_LONG_PRESS_IN: null, // Responder, active, in the `PressRect`, after long press threshold
  130. RESPONDER_ACTIVE_LONG_PRESS_OUT: null, // Responder, active, out of `PressRect`, after long press threshold
  131. ERROR: null,
  132. });
  133. type State =
  134. | typeof States.NOT_RESPONDER
  135. | typeof States.RESPONDER_INACTIVE_PRESS_IN
  136. | typeof States.RESPONDER_INACTIVE_PRESS_OUT
  137. | typeof States.RESPONDER_ACTIVE_PRESS_IN
  138. | typeof States.RESPONDER_ACTIVE_PRESS_OUT
  139. | typeof States.RESPONDER_ACTIVE_LONG_PRESS_IN
  140. | typeof States.RESPONDER_ACTIVE_LONG_PRESS_OUT
  141. | typeof States.ERROR;
  142. /*
  143. * Quick lookup map for states that are considered to be "active"
  144. */
  145. const baseStatesConditions = {
  146. NOT_RESPONDER: false,
  147. RESPONDER_INACTIVE_PRESS_IN: false,
  148. RESPONDER_INACTIVE_PRESS_OUT: false,
  149. RESPONDER_ACTIVE_PRESS_IN: false,
  150. RESPONDER_ACTIVE_PRESS_OUT: false,
  151. RESPONDER_ACTIVE_LONG_PRESS_IN: false,
  152. RESPONDER_ACTIVE_LONG_PRESS_OUT: false,
  153. ERROR: false,
  154. };
  155. const IsActive = {
  156. ...baseStatesConditions,
  157. RESPONDER_ACTIVE_PRESS_OUT: true,
  158. RESPONDER_ACTIVE_PRESS_IN: true,
  159. };
  160. /**
  161. * Quick lookup for states that are considered to be "pressing" and are
  162. * therefore eligible to result in a "selection" if the press stops.
  163. */
  164. const IsPressingIn = {
  165. ...baseStatesConditions,
  166. RESPONDER_INACTIVE_PRESS_IN: true,
  167. RESPONDER_ACTIVE_PRESS_IN: true,
  168. RESPONDER_ACTIVE_LONG_PRESS_IN: true,
  169. };
  170. const IsLongPressingIn = {
  171. ...baseStatesConditions,
  172. RESPONDER_ACTIVE_LONG_PRESS_IN: true,
  173. };
  174. /**
  175. * Inputs to the state machine.
  176. */
  177. const Signals = keyMirror({
  178. DELAY: null,
  179. RESPONDER_GRANT: null,
  180. RESPONDER_RELEASE: null,
  181. RESPONDER_TERMINATED: null,
  182. ENTER_PRESS_RECT: null,
  183. LEAVE_PRESS_RECT: null,
  184. LONG_PRESS_DETECTED: null,
  185. });
  186. type Signal =
  187. | typeof Signals.DELAY
  188. | typeof Signals.RESPONDER_GRANT
  189. | typeof Signals.RESPONDER_RELEASE
  190. | typeof Signals.RESPONDER_TERMINATED
  191. | typeof Signals.ENTER_PRESS_RECT
  192. | typeof Signals.LEAVE_PRESS_RECT
  193. | typeof Signals.LONG_PRESS_DETECTED;
  194. /**
  195. * Mapping from States x Signals => States
  196. */
  197. const Transitions = {
  198. NOT_RESPONDER: {
  199. DELAY: States.ERROR,
  200. RESPONDER_GRANT: States.RESPONDER_INACTIVE_PRESS_IN,
  201. RESPONDER_RELEASE: States.ERROR,
  202. RESPONDER_TERMINATED: States.ERROR,
  203. ENTER_PRESS_RECT: States.ERROR,
  204. LEAVE_PRESS_RECT: States.ERROR,
  205. LONG_PRESS_DETECTED: States.ERROR,
  206. },
  207. RESPONDER_INACTIVE_PRESS_IN: {
  208. DELAY: States.RESPONDER_ACTIVE_PRESS_IN,
  209. RESPONDER_GRANT: States.ERROR,
  210. RESPONDER_RELEASE: States.NOT_RESPONDER,
  211. RESPONDER_TERMINATED: States.NOT_RESPONDER,
  212. ENTER_PRESS_RECT: States.RESPONDER_INACTIVE_PRESS_IN,
  213. LEAVE_PRESS_RECT: States.RESPONDER_INACTIVE_PRESS_OUT,
  214. LONG_PRESS_DETECTED: States.ERROR,
  215. },
  216. RESPONDER_INACTIVE_PRESS_OUT: {
  217. DELAY: States.RESPONDER_ACTIVE_PRESS_OUT,
  218. RESPONDER_GRANT: States.ERROR,
  219. RESPONDER_RELEASE: States.NOT_RESPONDER,
  220. RESPONDER_TERMINATED: States.NOT_RESPONDER,
  221. ENTER_PRESS_RECT: States.RESPONDER_INACTIVE_PRESS_IN,
  222. LEAVE_PRESS_RECT: States.RESPONDER_INACTIVE_PRESS_OUT,
  223. LONG_PRESS_DETECTED: States.ERROR,
  224. },
  225. RESPONDER_ACTIVE_PRESS_IN: {
  226. DELAY: States.ERROR,
  227. RESPONDER_GRANT: States.ERROR,
  228. RESPONDER_RELEASE: States.NOT_RESPONDER,
  229. RESPONDER_TERMINATED: States.NOT_RESPONDER,
  230. ENTER_PRESS_RECT: States.RESPONDER_ACTIVE_PRESS_IN,
  231. LEAVE_PRESS_RECT: States.RESPONDER_ACTIVE_PRESS_OUT,
  232. LONG_PRESS_DETECTED: States.RESPONDER_ACTIVE_LONG_PRESS_IN,
  233. },
  234. RESPONDER_ACTIVE_PRESS_OUT: {
  235. DELAY: States.ERROR,
  236. RESPONDER_GRANT: States.ERROR,
  237. RESPONDER_RELEASE: States.NOT_RESPONDER,
  238. RESPONDER_TERMINATED: States.NOT_RESPONDER,
  239. ENTER_PRESS_RECT: States.RESPONDER_ACTIVE_PRESS_IN,
  240. LEAVE_PRESS_RECT: States.RESPONDER_ACTIVE_PRESS_OUT,
  241. LONG_PRESS_DETECTED: States.ERROR,
  242. },
  243. RESPONDER_ACTIVE_LONG_PRESS_IN: {
  244. DELAY: States.ERROR,
  245. RESPONDER_GRANT: States.ERROR,
  246. RESPONDER_RELEASE: States.NOT_RESPONDER,
  247. RESPONDER_TERMINATED: States.NOT_RESPONDER,
  248. ENTER_PRESS_RECT: States.RESPONDER_ACTIVE_LONG_PRESS_IN,
  249. LEAVE_PRESS_RECT: States.RESPONDER_ACTIVE_LONG_PRESS_OUT,
  250. LONG_PRESS_DETECTED: States.RESPONDER_ACTIVE_LONG_PRESS_IN,
  251. },
  252. RESPONDER_ACTIVE_LONG_PRESS_OUT: {
  253. DELAY: States.ERROR,
  254. RESPONDER_GRANT: States.ERROR,
  255. RESPONDER_RELEASE: States.NOT_RESPONDER,
  256. RESPONDER_TERMINATED: States.NOT_RESPONDER,
  257. ENTER_PRESS_RECT: States.RESPONDER_ACTIVE_LONG_PRESS_IN,
  258. LEAVE_PRESS_RECT: States.RESPONDER_ACTIVE_LONG_PRESS_OUT,
  259. LONG_PRESS_DETECTED: States.ERROR,
  260. },
  261. error: {
  262. DELAY: States.NOT_RESPONDER,
  263. RESPONDER_GRANT: States.RESPONDER_INACTIVE_PRESS_IN,
  264. RESPONDER_RELEASE: States.NOT_RESPONDER,
  265. RESPONDER_TERMINATED: States.NOT_RESPONDER,
  266. ENTER_PRESS_RECT: States.NOT_RESPONDER,
  267. LEAVE_PRESS_RECT: States.NOT_RESPONDER,
  268. LONG_PRESS_DETECTED: States.NOT_RESPONDER,
  269. },
  270. };
  271. // ==== Typical Constants for integrating into UI components ====
  272. // var HIT_EXPAND_PX = 20;
  273. // var HIT_VERT_OFFSET_PX = 10;
  274. const HIGHLIGHT_DELAY_MS = 130;
  275. const PRESS_EXPAND_PX = 20;
  276. const LONG_PRESS_THRESHOLD = 500;
  277. const LONG_PRESS_DELAY_MS = LONG_PRESS_THRESHOLD - HIGHLIGHT_DELAY_MS;
  278. const LONG_PRESS_ALLOWED_MOVEMENT = 10;
  279. // Default amount "active" region protrudes beyond box
  280. /**
  281. * By convention, methods prefixed with underscores are meant to be @private,
  282. * and not @protected. Mixers shouldn't access them - not even to provide them
  283. * as callback handlers.
  284. *
  285. *
  286. * ========== Geometry =========
  287. * `Touchable` only assumes that there exists a `HitRect` node. The `PressRect`
  288. * is an abstract box that is extended beyond the `HitRect`.
  289. *
  290. * +--------------------------+
  291. * | | - "Start" events in `HitRect` cause `HitRect`
  292. * | +--------------------+ | to become the responder.
  293. * | | +--------------+ | | - `HitRect` is typically expanded around
  294. * | | | | | | the `VisualRect`, but shifted downward.
  295. * | | | VisualRect | | | - After pressing down, after some delay,
  296. * | | | | | | and before letting up, the Visual React
  297. * | | +--------------+ | | will become "active". This makes it eligible
  298. * | | HitRect | | for being highlighted (so long as the
  299. * | +--------------------+ | press remains in the `PressRect`).
  300. * | PressRect o |
  301. * +----------------------|---+
  302. * Out Region |
  303. * +-----+ This gap between the `HitRect` and
  304. * `PressRect` allows a touch to move far away
  305. * from the original hit rect, and remain
  306. * highlighted, and eligible for a "Press".
  307. * Customize this via
  308. * `touchableGetPressRectOffset()`.
  309. *
  310. *
  311. *
  312. * ======= State Machine =======
  313. *
  314. * +-------------+ <---+ RESPONDER_RELEASE
  315. * |NOT_RESPONDER|
  316. * +-------------+ <---+ RESPONDER_TERMINATED
  317. * +
  318. * | RESPONDER_GRANT (HitRect)
  319. * v
  320. * +---------------------------+ DELAY +-------------------------+ T + DELAY +------------------------------+
  321. * |RESPONDER_INACTIVE_PRESS_IN|+-------->|RESPONDER_ACTIVE_PRESS_IN| +------------> |RESPONDER_ACTIVE_LONG_PRESS_IN|
  322. * +---------------------------+ +-------------------------+ +------------------------------+
  323. * + ^ + ^ + ^
  324. * |LEAVE_ |ENTER_ |LEAVE_ |ENTER_ |LEAVE_ |ENTER_
  325. * |PRESS_RECT |PRESS_RECT |PRESS_RECT |PRESS_RECT |PRESS_RECT |PRESS_RECT
  326. * | | | | | |
  327. * v + v + v +
  328. * +----------------------------+ DELAY +--------------------------+ +-------------------------------+
  329. * |RESPONDER_INACTIVE_PRESS_OUT|+------->|RESPONDER_ACTIVE_PRESS_OUT| |RESPONDER_ACTIVE_LONG_PRESS_OUT|
  330. * +----------------------------+ +--------------------------+ +-------------------------------+
  331. *
  332. * T + DELAY => LONG_PRESS_DELAY_MS + DELAY
  333. *
  334. * Not drawn are the side effects of each transition. The most important side
  335. * effect is the `touchableHandlePress` abstract method invocation that occurs
  336. * when a responder is released while in either of the "Press" states.
  337. *
  338. * The other important side effects are the highlight abstract method
  339. * invocations (internal callbacks) to be implemented by the mixer.
  340. *
  341. *
  342. * @lends Touchable.prototype
  343. */
  344. const TouchableMixin = {
  345. componentDidMount: function() {
  346. if (!Platform.isTV) {
  347. return;
  348. }
  349. this._tvEventHandler = new TVEventHandler();
  350. this._tvEventHandler.enable(this, function(cmp, evt) {
  351. const myTag = ReactNative.findNodeHandle(cmp);
  352. evt.dispatchConfig = {};
  353. if (myTag === evt.tag) {
  354. if (evt.eventType === 'focus') {
  355. cmp.touchableHandleFocus(evt);
  356. } else if (evt.eventType === 'blur') {
  357. cmp.touchableHandleBlur(evt);
  358. } else if (evt.eventType === 'select' && Platform.OS !== 'android') {
  359. cmp.touchableHandlePress &&
  360. !cmp.props.disabled &&
  361. cmp.touchableHandlePress(evt);
  362. }
  363. }
  364. });
  365. },
  366. /**
  367. * Clear all timeouts on unmount
  368. */
  369. componentWillUnmount: function() {
  370. if (this._tvEventHandler) {
  371. this._tvEventHandler.disable();
  372. delete this._tvEventHandler;
  373. }
  374. this.touchableDelayTimeout && clearTimeout(this.touchableDelayTimeout);
  375. this.longPressDelayTimeout && clearTimeout(this.longPressDelayTimeout);
  376. this.pressOutDelayTimeout && clearTimeout(this.pressOutDelayTimeout);
  377. },
  378. /**
  379. * It's prefer that mixins determine state in this way, having the class
  380. * explicitly mix the state in the one and only `getInitialState` method.
  381. *
  382. * @return {object} State object to be placed inside of
  383. * `this.state.touchable`.
  384. */
  385. touchableGetInitialState: function(): $TEMPORARY$object<{|
  386. touchable: $TEMPORARY$object<{|responderID: null, touchState: void|}>,
  387. |}> {
  388. return {
  389. touchable: {touchState: undefined, responderID: null},
  390. };
  391. },
  392. // ==== Hooks to Gesture Responder system ====
  393. /**
  394. * Must return true if embedded in a native platform scroll view.
  395. */
  396. touchableHandleResponderTerminationRequest: function(): any {
  397. return !this.props.rejectResponderTermination;
  398. },
  399. /**
  400. * Must return true to start the process of `Touchable`.
  401. */
  402. touchableHandleStartShouldSetResponder: function(): any {
  403. return !this.props.disabled;
  404. },
  405. /**
  406. * Return true to cancel press on long press.
  407. */
  408. touchableLongPressCancelsPress: function(): boolean {
  409. return true;
  410. },
  411. /**
  412. * Place as callback for a DOM element's `onResponderGrant` event.
  413. * @param {SyntheticEvent} e Synthetic event from event system.
  414. *
  415. */
  416. touchableHandleResponderGrant: function(e: PressEvent) {
  417. const dispatchID = e.currentTarget;
  418. // Since e is used in a callback invoked on another event loop
  419. // (as in setTimeout etc), we need to call e.persist() on the
  420. // event to make sure it doesn't get reused in the event object pool.
  421. e.persist();
  422. this.pressOutDelayTimeout && clearTimeout(this.pressOutDelayTimeout);
  423. this.pressOutDelayTimeout = null;
  424. this.state.touchable.touchState = States.NOT_RESPONDER;
  425. this.state.touchable.responderID = dispatchID;
  426. this._receiveSignal(Signals.RESPONDER_GRANT, e);
  427. let delayMS =
  428. this.touchableGetHighlightDelayMS !== undefined
  429. ? Math.max(this.touchableGetHighlightDelayMS(), 0)
  430. : HIGHLIGHT_DELAY_MS;
  431. delayMS = isNaN(delayMS) ? HIGHLIGHT_DELAY_MS : delayMS;
  432. if (delayMS !== 0) {
  433. this.touchableDelayTimeout = setTimeout(
  434. this._handleDelay.bind(this, e),
  435. delayMS,
  436. );
  437. } else {
  438. this._handleDelay(e);
  439. }
  440. let longDelayMS =
  441. this.touchableGetLongPressDelayMS !== undefined
  442. ? Math.max(this.touchableGetLongPressDelayMS(), 10)
  443. : LONG_PRESS_DELAY_MS;
  444. longDelayMS = isNaN(longDelayMS) ? LONG_PRESS_DELAY_MS : longDelayMS;
  445. this.longPressDelayTimeout = setTimeout(
  446. this._handleLongDelay.bind(this, e),
  447. longDelayMS + delayMS,
  448. );
  449. },
  450. /**
  451. * Place as callback for a DOM element's `onResponderRelease` event.
  452. */
  453. touchableHandleResponderRelease: function(e: PressEvent) {
  454. this.pressInLocation = null;
  455. this._receiveSignal(Signals.RESPONDER_RELEASE, e);
  456. },
  457. /**
  458. * Place as callback for a DOM element's `onResponderTerminate` event.
  459. */
  460. touchableHandleResponderTerminate: function(e: PressEvent) {
  461. this.pressInLocation = null;
  462. this._receiveSignal(Signals.RESPONDER_TERMINATED, e);
  463. },
  464. /**
  465. * Place as callback for a DOM element's `onResponderMove` event.
  466. */
  467. touchableHandleResponderMove: function(e: PressEvent) {
  468. // Measurement may not have returned yet.
  469. if (!this.state.touchable.positionOnActivate) {
  470. return;
  471. }
  472. const positionOnActivate = this.state.touchable.positionOnActivate;
  473. const dimensionsOnActivate = this.state.touchable.dimensionsOnActivate;
  474. const pressRectOffset = this.touchableGetPressRectOffset
  475. ? this.touchableGetPressRectOffset()
  476. : {
  477. left: PRESS_EXPAND_PX,
  478. right: PRESS_EXPAND_PX,
  479. top: PRESS_EXPAND_PX,
  480. bottom: PRESS_EXPAND_PX,
  481. };
  482. let pressExpandLeft = pressRectOffset.left;
  483. let pressExpandTop = pressRectOffset.top;
  484. let pressExpandRight = pressRectOffset.right;
  485. let pressExpandBottom = pressRectOffset.bottom;
  486. const hitSlop = this.touchableGetHitSlop
  487. ? this.touchableGetHitSlop()
  488. : null;
  489. if (hitSlop) {
  490. pressExpandLeft += hitSlop.left || 0;
  491. pressExpandTop += hitSlop.top || 0;
  492. pressExpandRight += hitSlop.right || 0;
  493. pressExpandBottom += hitSlop.bottom || 0;
  494. }
  495. const touch = extractSingleTouch(e.nativeEvent);
  496. const pageX = touch && touch.pageX;
  497. const pageY = touch && touch.pageY;
  498. if (this.pressInLocation) {
  499. const movedDistance = this._getDistanceBetweenPoints(
  500. pageX,
  501. pageY,
  502. this.pressInLocation.pageX,
  503. this.pressInLocation.pageY,
  504. );
  505. if (movedDistance > LONG_PRESS_ALLOWED_MOVEMENT) {
  506. this._cancelLongPressDelayTimeout();
  507. }
  508. }
  509. const isTouchWithinActive =
  510. pageX > positionOnActivate.left - pressExpandLeft &&
  511. pageY > positionOnActivate.top - pressExpandTop &&
  512. pageX <
  513. positionOnActivate.left +
  514. dimensionsOnActivate.width +
  515. pressExpandRight &&
  516. pageY <
  517. positionOnActivate.top +
  518. dimensionsOnActivate.height +
  519. pressExpandBottom;
  520. if (isTouchWithinActive) {
  521. const prevState = this.state.touchable.touchState;
  522. this._receiveSignal(Signals.ENTER_PRESS_RECT, e);
  523. const curState = this.state.touchable.touchState;
  524. if (
  525. curState === States.RESPONDER_INACTIVE_PRESS_IN &&
  526. prevState !== States.RESPONDER_INACTIVE_PRESS_IN
  527. ) {
  528. // fix for t7967420
  529. this._cancelLongPressDelayTimeout();
  530. }
  531. } else {
  532. this._cancelLongPressDelayTimeout();
  533. this._receiveSignal(Signals.LEAVE_PRESS_RECT, e);
  534. }
  535. },
  536. /**
  537. * Invoked when the item receives focus. Mixers might override this to
  538. * visually distinguish the `VisualRect` so that the user knows that it
  539. * currently has the focus. Most platforms only support a single element being
  540. * focused at a time, in which case there may have been a previously focused
  541. * element that was blurred just prior to this. This can be overridden when
  542. * using `Touchable.Mixin.withoutDefaultFocusAndBlur`.
  543. */
  544. touchableHandleFocus: function(e: Event) {
  545. this.props.onFocus && this.props.onFocus(e);
  546. },
  547. /**
  548. * Invoked when the item loses focus. Mixers might override this to
  549. * visually distinguish the `VisualRect` so that the user knows that it
  550. * no longer has focus. Most platforms only support a single element being
  551. * focused at a time, in which case the focus may have moved to another.
  552. * This can be overridden when using
  553. * `Touchable.Mixin.withoutDefaultFocusAndBlur`.
  554. */
  555. touchableHandleBlur: function(e: Event) {
  556. this.props.onBlur && this.props.onBlur(e);
  557. },
  558. // ==== Abstract Application Callbacks ====
  559. /**
  560. * Invoked when the item should be highlighted. Mixers should implement this
  561. * to visually distinguish the `VisualRect` so that the user knows that
  562. * releasing a touch will result in a "selection" (analog to click).
  563. *
  564. * @abstract
  565. * touchableHandleActivePressIn: function,
  566. */
  567. /**
  568. * Invoked when the item is "active" (in that it is still eligible to become
  569. * a "select") but the touch has left the `PressRect`. Usually the mixer will
  570. * want to unhighlight the `VisualRect`. If the user (while pressing) moves
  571. * back into the `PressRect` `touchableHandleActivePressIn` will be invoked
  572. * again and the mixer should probably highlight the `VisualRect` again. This
  573. * event will not fire on an `touchEnd/mouseUp` event, only move events while
  574. * the user is depressing the mouse/touch.
  575. *
  576. * @abstract
  577. * touchableHandleActivePressOut: function
  578. */
  579. /**
  580. * Invoked when the item is "selected" - meaning the interaction ended by
  581. * letting up while the item was either in the state
  582. * `RESPONDER_ACTIVE_PRESS_IN` or `RESPONDER_INACTIVE_PRESS_IN`.
  583. *
  584. * @abstract
  585. * touchableHandlePress: function
  586. */
  587. /**
  588. * Invoked when the item is long pressed - meaning the interaction ended by
  589. * letting up while the item was in `RESPONDER_ACTIVE_LONG_PRESS_IN`. If
  590. * `touchableHandleLongPress` is *not* provided, `touchableHandlePress` will
  591. * be called as it normally is. If `touchableHandleLongPress` is provided, by
  592. * default any `touchableHandlePress` callback will not be invoked. To
  593. * override this default behavior, override `touchableLongPressCancelsPress`
  594. * to return false. As a result, `touchableHandlePress` will be called when
  595. * lifting up, even if `touchableHandleLongPress` has also been called.
  596. *
  597. * @abstract
  598. * touchableHandleLongPress: function
  599. */
  600. /**
  601. * Returns the number of millis to wait before triggering a highlight.
  602. *
  603. * @abstract
  604. * touchableGetHighlightDelayMS: function
  605. */
  606. /**
  607. * Returns the amount to extend the `HitRect` into the `PressRect`. Positive
  608. * numbers mean the size expands outwards.
  609. *
  610. * @abstract
  611. * touchableGetPressRectOffset: function
  612. */
  613. // ==== Internal Logic ====
  614. /**
  615. * Measures the `HitRect` node on activation. The Bounding rectangle is with
  616. * respect to viewport - not page, so adding the `pageXOffset/pageYOffset`
  617. * should result in points that are in the same coordinate system as an
  618. * event's `globalX/globalY` data values.
  619. *
  620. * - Consider caching this for the lifetime of the component, or possibly
  621. * being able to share this cache between any `ScrollMap` view.
  622. *
  623. * @sideeffects
  624. * @private
  625. */
  626. _remeasureMetricsOnActivation: function() {
  627. const responderID = this.state.touchable.responderID;
  628. if (responderID == null) {
  629. return;
  630. }
  631. if (typeof responderID === 'number') {
  632. UIManager.measure(responderID, this._handleQueryLayout);
  633. } else {
  634. responderID.measure(this._handleQueryLayout);
  635. }
  636. },
  637. _handleQueryLayout: function(
  638. l: number,
  639. t: number,
  640. w: number,
  641. h: number,
  642. globalX: number,
  643. globalY: number,
  644. ) {
  645. //don't do anything UIManager failed to measure node
  646. if (!l && !t && !w && !h && !globalX && !globalY) {
  647. return;
  648. }
  649. this.state.touchable.positionOnActivate &&
  650. Position.release(this.state.touchable.positionOnActivate);
  651. this.state.touchable.dimensionsOnActivate &&
  652. BoundingDimensions.release(this.state.touchable.dimensionsOnActivate);
  653. this.state.touchable.positionOnActivate = Position.getPooled(
  654. globalX,
  655. globalY,
  656. );
  657. this.state.touchable.dimensionsOnActivate = BoundingDimensions.getPooled(
  658. w,
  659. h,
  660. );
  661. },
  662. _handleDelay: function(e: PressEvent) {
  663. this.touchableDelayTimeout = null;
  664. this._receiveSignal(Signals.DELAY, e);
  665. },
  666. _handleLongDelay: function(e: PressEvent) {
  667. this.longPressDelayTimeout = null;
  668. const curState = this.state.touchable.touchState;
  669. if (
  670. curState === States.RESPONDER_ACTIVE_PRESS_IN ||
  671. curState === States.RESPONDER_ACTIVE_LONG_PRESS_IN
  672. ) {
  673. this._receiveSignal(Signals.LONG_PRESS_DETECTED, e);
  674. }
  675. },
  676. /**
  677. * Receives a state machine signal, performs side effects of the transition
  678. * and stores the new state. Validates the transition as well.
  679. *
  680. * @param {Signals} signal State machine signal.
  681. * @throws Error if invalid state transition or unrecognized signal.
  682. * @sideeffects
  683. */
  684. _receiveSignal: function(signal: Signal, e: PressEvent) {
  685. const responderID = this.state.touchable.responderID;
  686. const curState = this.state.touchable.touchState;
  687. const nextState = Transitions[curState] && Transitions[curState][signal];
  688. if (!responderID && signal === Signals.RESPONDER_RELEASE) {
  689. return;
  690. }
  691. if (!nextState) {
  692. throw new Error(
  693. 'Unrecognized signal `' +
  694. signal +
  695. '` or state `' +
  696. curState +
  697. '` for Touchable responder `' +
  698. typeof this.state.touchable.responderID ===
  699. 'number'
  700. ? this.state.touchable.responderID
  701. : 'host component' + '`',
  702. );
  703. }
  704. if (nextState === States.ERROR) {
  705. throw new Error(
  706. 'Touchable cannot transition from `' +
  707. curState +
  708. '` to `' +
  709. signal +
  710. '` for responder `' +
  711. typeof this.state.touchable.responderID ===
  712. 'number'
  713. ? this.state.touchable.responderID
  714. : '<<host component>>' + '`',
  715. );
  716. }
  717. if (curState !== nextState) {
  718. this._performSideEffectsForTransition(curState, nextState, signal, e);
  719. this.state.touchable.touchState = nextState;
  720. }
  721. },
  722. _cancelLongPressDelayTimeout: function() {
  723. this.longPressDelayTimeout && clearTimeout(this.longPressDelayTimeout);
  724. this.longPressDelayTimeout = null;
  725. },
  726. _isHighlight: function(state: State): boolean {
  727. return (
  728. state === States.RESPONDER_ACTIVE_PRESS_IN ||
  729. state === States.RESPONDER_ACTIVE_LONG_PRESS_IN
  730. );
  731. },
  732. _savePressInLocation: function(e: PressEvent) {
  733. const touch = extractSingleTouch(e.nativeEvent);
  734. const pageX = touch && touch.pageX;
  735. const pageY = touch && touch.pageY;
  736. const locationX = touch && touch.locationX;
  737. const locationY = touch && touch.locationY;
  738. this.pressInLocation = {pageX, pageY, locationX, locationY};
  739. },
  740. _getDistanceBetweenPoints: function(
  741. aX: number,
  742. aY: number,
  743. bX: number,
  744. bY: number,
  745. ): number {
  746. const deltaX = aX - bX;
  747. const deltaY = aY - bY;
  748. return Math.sqrt(deltaX * deltaX + deltaY * deltaY);
  749. },
  750. /**
  751. * Will perform a transition between touchable states, and identify any
  752. * highlighting or unhighlighting that must be performed for this particular
  753. * transition.
  754. *
  755. * @param {States} curState Current Touchable state.
  756. * @param {States} nextState Next Touchable state.
  757. * @param {Signal} signal Signal that triggered the transition.
  758. * @param {Event} e Native event.
  759. * @sideeffects
  760. */
  761. _performSideEffectsForTransition: function(
  762. curState: State,
  763. nextState: State,
  764. signal: Signal,
  765. e: PressEvent,
  766. ) {
  767. const curIsHighlight = this._isHighlight(curState);
  768. const newIsHighlight = this._isHighlight(nextState);
  769. const isFinalSignal =
  770. signal === Signals.RESPONDER_TERMINATED ||
  771. signal === Signals.RESPONDER_RELEASE;
  772. if (isFinalSignal) {
  773. this._cancelLongPressDelayTimeout();
  774. }
  775. const isInitialTransition =
  776. curState === States.NOT_RESPONDER &&
  777. nextState === States.RESPONDER_INACTIVE_PRESS_IN;
  778. const isActiveTransition = !IsActive[curState] && IsActive[nextState];
  779. if (isInitialTransition || isActiveTransition) {
  780. this._remeasureMetricsOnActivation();
  781. }
  782. if (IsPressingIn[curState] && signal === Signals.LONG_PRESS_DETECTED) {
  783. this.touchableHandleLongPress && this.touchableHandleLongPress(e);
  784. }
  785. if (newIsHighlight && !curIsHighlight) {
  786. this._startHighlight(e);
  787. } else if (!newIsHighlight && curIsHighlight) {
  788. this._endHighlight(e);
  789. }
  790. if (IsPressingIn[curState] && signal === Signals.RESPONDER_RELEASE) {
  791. const hasLongPressHandler = !!this.props.onLongPress;
  792. const pressIsLongButStillCallOnPress =
  793. IsLongPressingIn[curState] && // We *are* long pressing.. // But either has no long handler
  794. (!hasLongPressHandler || !this.touchableLongPressCancelsPress()); // or we're told to ignore it.
  795. const shouldInvokePress =
  796. !IsLongPressingIn[curState] || pressIsLongButStillCallOnPress;
  797. if (shouldInvokePress && this.touchableHandlePress) {
  798. if (!newIsHighlight && !curIsHighlight) {
  799. // we never highlighted because of delay, but we should highlight now
  800. this._startHighlight(e);
  801. this._endHighlight(e);
  802. }
  803. if (Platform.OS === 'android' && !this.props.touchSoundDisabled) {
  804. SoundManager.playTouchSound();
  805. }
  806. this.touchableHandlePress(e);
  807. }
  808. }
  809. this.touchableDelayTimeout && clearTimeout(this.touchableDelayTimeout);
  810. this.touchableDelayTimeout = null;
  811. },
  812. _startHighlight: function(e: PressEvent) {
  813. this._savePressInLocation(e);
  814. this.touchableHandleActivePressIn && this.touchableHandleActivePressIn(e);
  815. },
  816. _endHighlight: function(e: PressEvent) {
  817. if (this.touchableHandleActivePressOut) {
  818. if (
  819. this.touchableGetPressOutDelayMS &&
  820. this.touchableGetPressOutDelayMS()
  821. ) {
  822. this.pressOutDelayTimeout = setTimeout(() => {
  823. this.touchableHandleActivePressOut(e);
  824. }, this.touchableGetPressOutDelayMS());
  825. } else {
  826. this.touchableHandleActivePressOut(e);
  827. }
  828. }
  829. },
  830. withoutDefaultFocusAndBlur: ({}: $TEMPORARY$object<{||}>),
  831. };
  832. /**
  833. * Provide an optional version of the mixin where `touchableHandleFocus` and
  834. * `touchableHandleBlur` can be overridden. This allows appropriate defaults to
  835. * be set on TV platforms, without breaking existing implementations of
  836. * `Touchable`.
  837. */
  838. const {
  839. touchableHandleFocus,
  840. touchableHandleBlur,
  841. ...TouchableMixinWithoutDefaultFocusAndBlur
  842. } = TouchableMixin;
  843. TouchableMixin.withoutDefaultFocusAndBlur = TouchableMixinWithoutDefaultFocusAndBlur;
  844. const Touchable = {
  845. Mixin: TouchableMixin,
  846. TOUCH_TARGET_DEBUG: false, // Highlights all touchable targets. Toggle with Inspector.
  847. /**
  848. * Renders a debugging overlay to visualize touch target with hitSlop (might not work on Android).
  849. */
  850. renderDebugView: ({
  851. color,
  852. hitSlop,
  853. }: {
  854. color: string | number,
  855. hitSlop: EdgeInsetsProp,
  856. ...
  857. }): null | React.Node => {
  858. if (!Touchable.TOUCH_TARGET_DEBUG) {
  859. return null;
  860. }
  861. if (!__DEV__) {
  862. throw Error(
  863. 'Touchable.TOUCH_TARGET_DEBUG should not be enabled in prod!',
  864. );
  865. }
  866. const debugHitSlopStyle = {};
  867. hitSlop = hitSlop || {top: 0, bottom: 0, left: 0, right: 0};
  868. for (const key in hitSlop) {
  869. debugHitSlopStyle[key] = -hitSlop[key];
  870. }
  871. const normalizedColor = normalizeColor(color);
  872. if (typeof normalizedColor !== 'number') {
  873. return null;
  874. }
  875. const hexColor =
  876. '#' + ('00000000' + normalizedColor.toString(16)).substr(-8);
  877. return (
  878. <View
  879. pointerEvents="none"
  880. style={[
  881. styles.debug,
  882. /* $FlowFixMe(>=0.111.0 site=react_native_fb) This comment suppresses
  883. * an error found when Flow v0.111 was deployed. To see the error,
  884. * delete this comment and run Flow. */
  885. {
  886. borderColor: hexColor.slice(0, -2) + '55', // More opaque
  887. backgroundColor: hexColor.slice(0, -2) + '0F', // Less opaque
  888. ...debugHitSlopStyle,
  889. },
  890. ]}
  891. />
  892. );
  893. },
  894. };
  895. const styles = StyleSheet.create({
  896. debug: {
  897. position: 'absolute',
  898. borderWidth: 1,
  899. borderStyle: 'dashed',
  900. },
  901. });
  902. module.exports = Touchable;