ScrollResponder.js 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773
  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. * @format
  8. * @flow
  9. */
  10. 'use strict';
  11. const React = require('react');
  12. const Dimensions = require('../Utilities/Dimensions');
  13. const FrameRateLogger = require('../Interaction/FrameRateLogger');
  14. const Keyboard = require('./Keyboard/Keyboard');
  15. const ReactNative = require('../Renderer/shims/ReactNative');
  16. const TextInputState = require('./TextInput/TextInputState');
  17. const UIManager = require('../ReactNative/UIManager');
  18. const Platform = require('../Utilities/Platform');
  19. import Commands from './ScrollView/ScrollViewCommands';
  20. const invariant = require('invariant');
  21. const performanceNow = require('fbjs/lib/performanceNow');
  22. import type {PressEvent, ScrollEvent} from '../Types/CoreEventTypes';
  23. import typeof ScrollView from './ScrollView/ScrollView';
  24. import type {Props as ScrollViewProps} from './ScrollView/ScrollView';
  25. import type {KeyboardEvent} from './Keyboard/Keyboard';
  26. import type EmitterSubscription from '../vendor/emitter/EmitterSubscription';
  27. import type {HostComponent} from '../Renderer/shims/ReactNativeTypes';
  28. /**
  29. * Mixin that can be integrated in order to handle scrolling that plays well
  30. * with `ResponderEventPlugin`. Integrate with your platform specific scroll
  31. * views, or even your custom built (every-frame animating) scroll views so that
  32. * all of these systems play well with the `ResponderEventPlugin`.
  33. *
  34. * iOS scroll event timing nuances:
  35. * ===============================
  36. *
  37. *
  38. * Scrolling without bouncing, if you touch down:
  39. * -------------------------------
  40. *
  41. * 1. `onMomentumScrollBegin` (when animation begins after letting up)
  42. * ... physical touch starts ...
  43. * 2. `onTouchStartCapture` (when you press down to stop the scroll)
  44. * 3. `onTouchStart` (same, but bubble phase)
  45. * 4. `onResponderRelease` (when lifting up - you could pause forever before * lifting)
  46. * 5. `onMomentumScrollEnd`
  47. *
  48. *
  49. * Scrolling with bouncing, if you touch down:
  50. * -------------------------------
  51. *
  52. * 1. `onMomentumScrollBegin` (when animation begins after letting up)
  53. * ... bounce begins ...
  54. * ... some time elapses ...
  55. * ... physical touch during bounce ...
  56. * 2. `onMomentumScrollEnd` (Makes no sense why this occurs first during bounce)
  57. * 3. `onTouchStartCapture` (immediately after `onMomentumScrollEnd`)
  58. * 4. `onTouchStart` (same, but bubble phase)
  59. * 5. `onTouchEnd` (You could hold the touch start for a long time)
  60. * 6. `onMomentumScrollBegin` (When releasing the view starts bouncing back)
  61. *
  62. * So when we receive an `onTouchStart`, how can we tell if we are touching
  63. * *during* an animation (which then causes the animation to stop)? The only way
  64. * to tell is if the `touchStart` occurred immediately after the
  65. * `onMomentumScrollEnd`.
  66. *
  67. * This is abstracted out for you, so you can just call this.scrollResponderIsAnimating() if
  68. * necessary
  69. *
  70. * `ScrollResponder` also includes logic for blurring a currently focused input
  71. * if one is focused while scrolling. The `ScrollResponder` is a natural place
  72. * to put this logic since it can support not dismissing the keyboard while
  73. * scrolling, unless a recognized "tap"-like gesture has occurred.
  74. *
  75. * The public lifecycle API includes events for keyboard interaction, responder
  76. * interaction, and scrolling (among others). The keyboard callbacks
  77. * `onKeyboardWill/Did/*` are *global* events, but are invoked on scroll
  78. * responder's props so that you can guarantee that the scroll responder's
  79. * internal state has been updated accordingly (and deterministically) by
  80. * the time the props callbacks are invoke. Otherwise, you would always wonder
  81. * if the scroll responder is currently in a state where it recognizes new
  82. * keyboard positions etc. If coordinating scrolling with keyboard movement,
  83. * *always* use these hooks instead of listening to your own global keyboard
  84. * events.
  85. *
  86. * Public keyboard lifecycle API: (props callbacks)
  87. *
  88. * Standard Keyboard Appearance Sequence:
  89. *
  90. * this.props.onKeyboardWillShow
  91. * this.props.onKeyboardDidShow
  92. *
  93. * `onScrollResponderKeyboardDismissed` will be invoked if an appropriate
  94. * tap inside the scroll responder's scrollable region was responsible
  95. * for the dismissal of the keyboard. There are other reasons why the
  96. * keyboard could be dismissed.
  97. *
  98. * this.props.onScrollResponderKeyboardDismissed
  99. *
  100. * Standard Keyboard Hide Sequence:
  101. *
  102. * this.props.onKeyboardWillHide
  103. * this.props.onKeyboardDidHide
  104. */
  105. const IS_ANIMATING_TOUCH_START_THRESHOLD_MS = 16;
  106. export type State = {|
  107. isTouching: boolean,
  108. lastMomentumScrollBeginTime: number,
  109. lastMomentumScrollEndTime: number,
  110. observedScrollSinceBecomingResponder: boolean,
  111. becameResponderWhileAnimating: boolean,
  112. |};
  113. const ScrollResponderMixin = {
  114. _subscriptionKeyboardWillShow: (null: ?EmitterSubscription),
  115. _subscriptionKeyboardWillHide: (null: ?EmitterSubscription),
  116. _subscriptionKeyboardDidShow: (null: ?EmitterSubscription),
  117. _subscriptionKeyboardDidHide: (null: ?EmitterSubscription),
  118. scrollResponderMixinGetInitialState: function(): State {
  119. return {
  120. isTouching: false,
  121. lastMomentumScrollBeginTime: 0,
  122. lastMomentumScrollEndTime: 0,
  123. // Reset to false every time becomes responder. This is used to:
  124. // - Determine if the scroll view has been scrolled and therefore should
  125. // refuse to give up its responder lock.
  126. // - Determine if releasing should dismiss the keyboard when we are in
  127. // tap-to-dismiss mode (this.props.keyboardShouldPersistTaps !== 'always').
  128. observedScrollSinceBecomingResponder: false,
  129. becameResponderWhileAnimating: false,
  130. };
  131. },
  132. /**
  133. * Invoke this from an `onScroll` event.
  134. */
  135. scrollResponderHandleScrollShouldSetResponder: function(): boolean {
  136. // Allow any event touch pass through if the default pan responder is disabled
  137. if (this.props.disableScrollViewPanResponder === true) {
  138. return false;
  139. }
  140. return this.state.isTouching;
  141. },
  142. /**
  143. * Merely touch starting is not sufficient for a scroll view to become the
  144. * responder. Being the "responder" means that the very next touch move/end
  145. * event will result in an action/movement.
  146. *
  147. * Invoke this from an `onStartShouldSetResponder` event.
  148. *
  149. * `onStartShouldSetResponder` is used when the next move/end will trigger
  150. * some UI movement/action, but when you want to yield priority to views
  151. * nested inside of the view.
  152. *
  153. * There may be some cases where scroll views actually should return `true`
  154. * from `onStartShouldSetResponder`: Any time we are detecting a standard tap
  155. * that gives priority to nested views.
  156. *
  157. * - If a single tap on the scroll view triggers an action such as
  158. * recentering a map style view yet wants to give priority to interaction
  159. * views inside (such as dropped pins or labels), then we would return true
  160. * from this method when there is a single touch.
  161. *
  162. * - Similar to the previous case, if a two finger "tap" should trigger a
  163. * zoom, we would check the `touches` count, and if `>= 2`, we would return
  164. * true.
  165. *
  166. */
  167. scrollResponderHandleStartShouldSetResponder: function(
  168. e: PressEvent,
  169. ): boolean {
  170. // Allow any event touch pass through if the default pan responder is disabled
  171. if (this.props.disableScrollViewPanResponder === true) {
  172. return false;
  173. }
  174. const currentlyFocusedInput = TextInputState.currentlyFocusedInput();
  175. if (
  176. this.props.keyboardShouldPersistTaps === 'handled' &&
  177. currentlyFocusedInput != null &&
  178. e.target !== currentlyFocusedInput
  179. ) {
  180. return true;
  181. }
  182. return false;
  183. },
  184. /**
  185. * There are times when the scroll view wants to become the responder
  186. * (meaning respond to the next immediate `touchStart/touchEnd`), in a way
  187. * that *doesn't* give priority to nested views (hence the capture phase):
  188. *
  189. * - Currently animating.
  190. * - Tapping anywhere that is not a text input, while the keyboard is
  191. * up (which should dismiss the keyboard).
  192. *
  193. * Invoke this from an `onStartShouldSetResponderCapture` event.
  194. */
  195. scrollResponderHandleStartShouldSetResponderCapture: function(
  196. e: PressEvent,
  197. ): boolean {
  198. // The scroll view should receive taps instead of its descendants if:
  199. // * it is already animating/decelerating
  200. if (this.scrollResponderIsAnimating()) {
  201. return true;
  202. }
  203. // Allow any event touch pass through if the default pan responder is disabled
  204. if (this.props.disableScrollViewPanResponder === true) {
  205. return false;
  206. }
  207. // * the keyboard is up, keyboardShouldPersistTaps is 'never' (the default),
  208. // and a new touch starts with a non-textinput target (in which case the
  209. // first tap should be sent to the scroll view and dismiss the keyboard,
  210. // then the second tap goes to the actual interior view)
  211. const currentlyFocusedTextInput = TextInputState.currentlyFocusedInput();
  212. const {keyboardShouldPersistTaps} = this.props;
  213. const keyboardNeverPersistTaps =
  214. !keyboardShouldPersistTaps || keyboardShouldPersistTaps === 'never';
  215. if (typeof e.target === 'number') {
  216. if (__DEV__) {
  217. console.error(
  218. 'Did not expect event target to be a number. Should have been a native component',
  219. );
  220. }
  221. return false;
  222. }
  223. if (
  224. keyboardNeverPersistTaps &&
  225. currentlyFocusedTextInput != null &&
  226. e.target != null &&
  227. !TextInputState.isTextInput(e.target)
  228. ) {
  229. return true;
  230. }
  231. return false;
  232. },
  233. /**
  234. * Invoke this from an `onResponderReject` event.
  235. *
  236. * Some other element is not yielding its role as responder. Normally, we'd
  237. * just disable the `UIScrollView`, but a touch has already began on it, the
  238. * `UIScrollView` will not accept being disabled after that. The easiest
  239. * solution for now is to accept the limitation of disallowing this
  240. * altogether. To improve this, find a way to disable the `UIScrollView` after
  241. * a touch has already started.
  242. */
  243. scrollResponderHandleResponderReject: function() {},
  244. /**
  245. * We will allow the scroll view to give up its lock iff it acquired the lock
  246. * during an animation. This is a very useful default that happens to satisfy
  247. * many common user experiences.
  248. *
  249. * - Stop a scroll on the left edge, then turn that into an outer view's
  250. * backswipe.
  251. * - Stop a scroll mid-bounce at the top, continue pulling to have the outer
  252. * view dismiss.
  253. * - However, without catching the scroll view mid-bounce (while it is
  254. * motionless), if you drag far enough for the scroll view to become
  255. * responder (and therefore drag the scroll view a bit), any backswipe
  256. * navigation of a swipe gesture higher in the view hierarchy, should be
  257. * rejected.
  258. */
  259. scrollResponderHandleTerminationRequest: function(): boolean {
  260. return !this.state.observedScrollSinceBecomingResponder;
  261. },
  262. /**
  263. * Invoke this from an `onTouchEnd` event.
  264. *
  265. * @param {PressEvent} e Event.
  266. */
  267. scrollResponderHandleTouchEnd: function(e: PressEvent) {
  268. const nativeEvent = e.nativeEvent;
  269. this.state.isTouching = nativeEvent.touches.length !== 0;
  270. this.props.onTouchEnd && this.props.onTouchEnd(e);
  271. },
  272. /**
  273. * Invoke this from an `onTouchCancel` event.
  274. *
  275. * @param {PressEvent} e Event.
  276. */
  277. scrollResponderHandleTouchCancel: function(e: PressEvent) {
  278. this.state.isTouching = false;
  279. this.props.onTouchCancel && this.props.onTouchCancel(e);
  280. },
  281. /**
  282. * Invoke this from an `onResponderRelease` event.
  283. */
  284. scrollResponderHandleResponderRelease: function(e: PressEvent) {
  285. this.props.onResponderRelease && this.props.onResponderRelease(e);
  286. if (typeof e.target === 'number') {
  287. if (__DEV__) {
  288. console.error(
  289. 'Did not expect event target to be a number. Should have been a native component',
  290. );
  291. }
  292. return;
  293. }
  294. // By default scroll views will unfocus a textField
  295. // if another touch occurs outside of it
  296. const currentlyFocusedTextInput = TextInputState.currentlyFocusedInput();
  297. if (
  298. this.props.keyboardShouldPersistTaps !== true &&
  299. this.props.keyboardShouldPersistTaps !== 'always' &&
  300. currentlyFocusedTextInput != null &&
  301. e.target !== currentlyFocusedTextInput &&
  302. !this.state.observedScrollSinceBecomingResponder &&
  303. !this.state.becameResponderWhileAnimating
  304. ) {
  305. this.props.onScrollResponderKeyboardDismissed &&
  306. this.props.onScrollResponderKeyboardDismissed(e);
  307. TextInputState.blurTextInput(currentlyFocusedTextInput);
  308. }
  309. },
  310. scrollResponderHandleScroll: function(e: ScrollEvent) {
  311. (this: any).state.observedScrollSinceBecomingResponder = true;
  312. (this: any).props.onScroll && (this: any).props.onScroll(e);
  313. },
  314. /**
  315. * Invoke this from an `onResponderGrant` event.
  316. */
  317. scrollResponderHandleResponderGrant: function(e: ScrollEvent) {
  318. this.state.observedScrollSinceBecomingResponder = false;
  319. this.props.onResponderGrant && this.props.onResponderGrant(e);
  320. this.state.becameResponderWhileAnimating = this.scrollResponderIsAnimating();
  321. },
  322. /**
  323. * Unfortunately, `onScrollBeginDrag` also fires when *stopping* the scroll
  324. * animation, and there's not an easy way to distinguish a drag vs. stopping
  325. * momentum.
  326. *
  327. * Invoke this from an `onScrollBeginDrag` event.
  328. */
  329. scrollResponderHandleScrollBeginDrag: function(e: ScrollEvent) {
  330. FrameRateLogger.beginScroll(); // TODO: track all scrolls after implementing onScrollEndAnimation
  331. this.props.onScrollBeginDrag && this.props.onScrollBeginDrag(e);
  332. },
  333. /**
  334. * Invoke this from an `onScrollEndDrag` event.
  335. */
  336. scrollResponderHandleScrollEndDrag: function(e: ScrollEvent) {
  337. const {velocity} = e.nativeEvent;
  338. // - If we are animating, then this is a "drag" that is stopping the scrollview and momentum end
  339. // will fire.
  340. // - If velocity is non-zero, then the interaction will stop when momentum scroll ends or
  341. // another drag starts and ends.
  342. // - If we don't get velocity, better to stop the interaction twice than not stop it.
  343. if (
  344. !this.scrollResponderIsAnimating() &&
  345. (!velocity || (velocity.x === 0 && velocity.y === 0))
  346. ) {
  347. FrameRateLogger.endScroll();
  348. }
  349. this.props.onScrollEndDrag && this.props.onScrollEndDrag(e);
  350. },
  351. /**
  352. * Invoke this from an `onMomentumScrollBegin` event.
  353. */
  354. scrollResponderHandleMomentumScrollBegin: function(e: ScrollEvent) {
  355. this.state.lastMomentumScrollBeginTime = performanceNow();
  356. this.props.onMomentumScrollBegin && this.props.onMomentumScrollBegin(e);
  357. },
  358. /**
  359. * Invoke this from an `onMomentumScrollEnd` event.
  360. */
  361. scrollResponderHandleMomentumScrollEnd: function(e: ScrollEvent) {
  362. FrameRateLogger.endScroll();
  363. this.state.lastMomentumScrollEndTime = performanceNow();
  364. this.props.onMomentumScrollEnd && this.props.onMomentumScrollEnd(e);
  365. },
  366. /**
  367. * Invoke this from an `onTouchStart` event.
  368. *
  369. * Since we know that the `SimpleEventPlugin` occurs later in the plugin
  370. * order, after `ResponderEventPlugin`, we can detect that we were *not*
  371. * permitted to be the responder (presumably because a contained view became
  372. * responder). The `onResponderReject` won't fire in that case - it only
  373. * fires when a *current* responder rejects our request.
  374. *
  375. * @param {PressEvent} e Touch Start event.
  376. */
  377. scrollResponderHandleTouchStart: function(e: PressEvent) {
  378. this.state.isTouching = true;
  379. this.props.onTouchStart && this.props.onTouchStart(e);
  380. },
  381. /**
  382. * Invoke this from an `onTouchMove` event.
  383. *
  384. * Since we know that the `SimpleEventPlugin` occurs later in the plugin
  385. * order, after `ResponderEventPlugin`, we can detect that we were *not*
  386. * permitted to be the responder (presumably because a contained view became
  387. * responder). The `onResponderReject` won't fire in that case - it only
  388. * fires when a *current* responder rejects our request.
  389. *
  390. * @param {PressEvent} e Touch Start event.
  391. */
  392. scrollResponderHandleTouchMove: function(e: PressEvent) {
  393. this.props.onTouchMove && this.props.onTouchMove(e);
  394. },
  395. /**
  396. * A helper function for this class that lets us quickly determine if the
  397. * view is currently animating. This is particularly useful to know when
  398. * a touch has just started or ended.
  399. */
  400. scrollResponderIsAnimating: function(): boolean {
  401. const now = performanceNow();
  402. const timeSinceLastMomentumScrollEnd =
  403. now - this.state.lastMomentumScrollEndTime;
  404. const isAnimating =
  405. timeSinceLastMomentumScrollEnd < IS_ANIMATING_TOUCH_START_THRESHOLD_MS ||
  406. this.state.lastMomentumScrollEndTime <
  407. this.state.lastMomentumScrollBeginTime;
  408. return isAnimating;
  409. },
  410. /**
  411. * Returns the node that represents native view that can be scrolled.
  412. * Components can pass what node to use by defining a `getScrollableNode`
  413. * function otherwise `this` is used.
  414. */
  415. scrollResponderGetScrollableNode: function(): ?number {
  416. return this.getScrollableNode
  417. ? this.getScrollableNode()
  418. : ReactNative.findNodeHandle(this);
  419. },
  420. /**
  421. * A helper function to scroll to a specific point in the ScrollView.
  422. * This is currently used to help focus child TextViews, but can also
  423. * be used to quickly scroll to any element we want to focus. Syntax:
  424. *
  425. * `scrollResponderScrollTo(options: {x: number = 0; y: number = 0; animated: boolean = true})`
  426. *
  427. * Note: The weird argument signature is due to the fact that, for historical reasons,
  428. * the function also accepts separate arguments as as alternative to the options object.
  429. * This is deprecated due to ambiguity (y before x), and SHOULD NOT BE USED.
  430. */
  431. scrollResponderScrollTo: function(
  432. x?:
  433. | number
  434. | {
  435. x?: number,
  436. y?: number,
  437. animated?: boolean,
  438. ...
  439. },
  440. y?: number,
  441. animated?: boolean,
  442. ) {
  443. if (typeof x === 'number') {
  444. console.warn(
  445. '`scrollResponderScrollTo(x, y, animated)` is deprecated. Use `scrollResponderScrollTo({x: 5, y: 5, animated: true})` instead.',
  446. );
  447. } else {
  448. ({x, y, animated} = x || {});
  449. }
  450. const that: React.ElementRef<ScrollView> = (this: any);
  451. invariant(
  452. that.getNativeScrollRef != null,
  453. 'Expected scrollTo to be called on a scrollViewRef. If this exception occurs it is likely a bug in React Native',
  454. );
  455. const nativeScrollRef = that.getNativeScrollRef();
  456. if (nativeScrollRef == null) {
  457. return;
  458. }
  459. Commands.scrollTo(nativeScrollRef, x || 0, y || 0, animated !== false);
  460. },
  461. /**
  462. * Scrolls to the end of the ScrollView, either immediately or with a smooth
  463. * animation.
  464. *
  465. * Example:
  466. *
  467. * `scrollResponderScrollToEnd({animated: true})`
  468. */
  469. scrollResponderScrollToEnd: function(options?: {animated?: boolean, ...}) {
  470. // Default to true
  471. const animated = (options && options.animated) !== false;
  472. const that: React.ElementRef<ScrollView> = (this: any);
  473. invariant(
  474. that.getNativeScrollRef != null,
  475. 'Expected scrollToEnd to be called on a scrollViewRef. If this exception occurs it is likely a bug in React Native',
  476. );
  477. const nativeScrollRef = that.getNativeScrollRef();
  478. if (nativeScrollRef == null) {
  479. return;
  480. }
  481. Commands.scrollToEnd(nativeScrollRef, animated);
  482. },
  483. /**
  484. * A helper function to zoom to a specific rect in the scrollview. The argument has the shape
  485. * {x: number; y: number; width: number; height: number; animated: boolean = true}
  486. *
  487. * @platform ios
  488. */
  489. scrollResponderZoomTo: function(
  490. rect: {|
  491. x: number,
  492. y: number,
  493. width: number,
  494. height: number,
  495. animated?: boolean,
  496. |},
  497. animated?: boolean, // deprecated, put this inside the rect argument instead
  498. ) {
  499. invariant(Platform.OS === 'ios', 'zoomToRect is not implemented');
  500. if ('animated' in rect) {
  501. animated = rect.animated;
  502. delete rect.animated;
  503. } else if (typeof animated !== 'undefined') {
  504. console.warn(
  505. '`scrollResponderZoomTo` `animated` argument is deprecated. Use `options.animated` instead',
  506. );
  507. }
  508. const that: React.ElementRef<ScrollView> = this;
  509. invariant(
  510. that.getNativeScrollRef != null,
  511. 'Expected zoomToRect to be called on a scrollViewRef. If this exception occurs it is likely a bug in React Native',
  512. );
  513. const nativeScrollRef = that.getNativeScrollRef();
  514. if (nativeScrollRef == null) {
  515. return;
  516. }
  517. Commands.zoomToRect(nativeScrollRef, rect, animated !== false);
  518. },
  519. /**
  520. * Displays the scroll indicators momentarily.
  521. */
  522. scrollResponderFlashScrollIndicators: function() {
  523. const that: React.ElementRef<ScrollView> = (this: any);
  524. invariant(
  525. that.getNativeScrollRef != null,
  526. 'Expected flashScrollIndicators to be called on a scrollViewRef. If this exception occurs it is likely a bug in React Native',
  527. );
  528. const nativeScrollRef = that.getNativeScrollRef();
  529. if (nativeScrollRef == null) {
  530. return;
  531. }
  532. Commands.flashScrollIndicators(nativeScrollRef);
  533. },
  534. /**
  535. * This method should be used as the callback to onFocus in a TextInputs'
  536. * parent view. Note that any module using this mixin needs to return
  537. * the parent view's ref in getScrollViewRef() in order to use this method.
  538. * @param {number} nodeHandle The TextInput node handle
  539. * @param {number} additionalOffset The scroll view's bottom "contentInset".
  540. * Default is 0.
  541. * @param {bool} preventNegativeScrolling Whether to allow pulling the content
  542. * down to make it meet the keyboard's top. Default is false.
  543. */
  544. scrollResponderScrollNativeHandleToKeyboard: function<T>(
  545. nodeHandle: number | React.ElementRef<HostComponent<T>>,
  546. additionalOffset?: number,
  547. preventNegativeScrollOffset?: boolean,
  548. ) {
  549. this.additionalScrollOffset = additionalOffset || 0;
  550. this.preventNegativeScrollOffset = !!preventNegativeScrollOffset;
  551. if (typeof nodeHandle === 'number') {
  552. UIManager.measureLayout(
  553. nodeHandle,
  554. ReactNative.findNodeHandle(this.getInnerViewNode()),
  555. this.scrollResponderTextInputFocusError,
  556. this.scrollResponderInputMeasureAndScrollToKeyboard,
  557. );
  558. } else {
  559. const innerRef = this.getInnerViewRef();
  560. if (innerRef == null) {
  561. return;
  562. }
  563. nodeHandle.measureLayout(
  564. innerRef,
  565. this.scrollResponderInputMeasureAndScrollToKeyboard,
  566. this.scrollResponderTextInputFocusError,
  567. );
  568. }
  569. },
  570. /**
  571. * The calculations performed here assume the scroll view takes up the entire
  572. * screen - even if has some content inset. We then measure the offsets of the
  573. * keyboard, and compensate both for the scroll view's "contentInset".
  574. *
  575. * @param {number} left Position of input w.r.t. table view.
  576. * @param {number} top Position of input w.r.t. table view.
  577. * @param {number} width Width of the text input.
  578. * @param {number} height Height of the text input.
  579. */
  580. scrollResponderInputMeasureAndScrollToKeyboard: function(
  581. left: number,
  582. top: number,
  583. width: number,
  584. height: number,
  585. ) {
  586. let keyboardScreenY = Dimensions.get('window').height;
  587. if (this.keyboardWillOpenTo) {
  588. keyboardScreenY = this.keyboardWillOpenTo.endCoordinates.screenY;
  589. }
  590. let scrollOffsetY =
  591. top - keyboardScreenY + height + this.additionalScrollOffset;
  592. // By default, this can scroll with negative offset, pulling the content
  593. // down so that the target component's bottom meets the keyboard's top.
  594. // If requested otherwise, cap the offset at 0 minimum to avoid content
  595. // shifting down.
  596. if (this.preventNegativeScrollOffset) {
  597. scrollOffsetY = Math.max(0, scrollOffsetY);
  598. }
  599. this.scrollResponderScrollTo({x: 0, y: scrollOffsetY, animated: true});
  600. this.additionalOffset = 0;
  601. this.preventNegativeScrollOffset = false;
  602. },
  603. scrollResponderTextInputFocusError: function(msg: string) {
  604. console.error('Error measuring text field: ', msg);
  605. },
  606. /**
  607. * `componentWillMount` is the closest thing to a standard "constructor" for
  608. * React components.
  609. *
  610. * The `keyboardWillShow` is called before input focus.
  611. */
  612. UNSAFE_componentWillMount: function() {
  613. const {keyboardShouldPersistTaps} = ((this: any).props: ScrollViewProps);
  614. if (typeof keyboardShouldPersistTaps === 'boolean') {
  615. console.warn(
  616. `'keyboardShouldPersistTaps={${
  617. keyboardShouldPersistTaps === true ? 'true' : 'false'
  618. }}' is deprecated. ` +
  619. `Use 'keyboardShouldPersistTaps="${
  620. keyboardShouldPersistTaps ? 'always' : 'never'
  621. }"' instead`,
  622. );
  623. }
  624. (this: any).keyboardWillOpenTo = null;
  625. (this: any).additionalScrollOffset = 0;
  626. this._subscriptionKeyboardWillShow = Keyboard.addListener(
  627. 'keyboardWillShow',
  628. this.scrollResponderKeyboardWillShow,
  629. );
  630. this._subscriptionKeyboardWillHide = Keyboard.addListener(
  631. 'keyboardWillHide',
  632. this.scrollResponderKeyboardWillHide,
  633. );
  634. this._subscriptionKeyboardDidShow = Keyboard.addListener(
  635. 'keyboardDidShow',
  636. this.scrollResponderKeyboardDidShow,
  637. );
  638. this._subscriptionKeyboardDidHide = Keyboard.addListener(
  639. 'keyboardDidHide',
  640. this.scrollResponderKeyboardDidHide,
  641. );
  642. },
  643. componentWillUnmount: function() {
  644. if (this._subscriptionKeyboardWillShow != null) {
  645. this._subscriptionKeyboardWillShow.remove();
  646. }
  647. if (this._subscriptionKeyboardWillHide != null) {
  648. this._subscriptionKeyboardWillHide.remove();
  649. }
  650. if (this._subscriptionKeyboardDidShow != null) {
  651. this._subscriptionKeyboardDidShow.remove();
  652. }
  653. if (this._subscriptionKeyboardDidHide != null) {
  654. this._subscriptionKeyboardDidHide.remove();
  655. }
  656. },
  657. /**
  658. * Warning, this may be called several times for a single keyboard opening.
  659. * It's best to store the information in this method and then take any action
  660. * at a later point (either in `keyboardDidShow` or other).
  661. *
  662. * Here's the order that events occur in:
  663. * - focus
  664. * - willShow {startCoordinates, endCoordinates} several times
  665. * - didShow several times
  666. * - blur
  667. * - willHide {startCoordinates, endCoordinates} several times
  668. * - didHide several times
  669. *
  670. * The `ScrollResponder` module callbacks for each of these events.
  671. * Even though any user could have easily listened to keyboard events
  672. * themselves, using these `props` callbacks ensures that ordering of events
  673. * is consistent - and not dependent on the order that the keyboard events are
  674. * subscribed to. This matters when telling the scroll view to scroll to where
  675. * the keyboard is headed - the scroll responder better have been notified of
  676. * the keyboard destination before being instructed to scroll to where the
  677. * keyboard will be. Stick to the `ScrollResponder` callbacks, and everything
  678. * will work.
  679. *
  680. * WARNING: These callbacks will fire even if a keyboard is displayed in a
  681. * different navigation pane. Filter out the events to determine if they are
  682. * relevant to you. (For example, only if you receive these callbacks after
  683. * you had explicitly focused a node etc).
  684. */
  685. scrollResponderKeyboardWillShow: function(e: KeyboardEvent) {
  686. this.keyboardWillOpenTo = e;
  687. this.props.onKeyboardWillShow && this.props.onKeyboardWillShow(e);
  688. },
  689. scrollResponderKeyboardWillHide: function(e: KeyboardEvent) {
  690. this.keyboardWillOpenTo = null;
  691. this.props.onKeyboardWillHide && this.props.onKeyboardWillHide(e);
  692. },
  693. scrollResponderKeyboardDidShow: function(e: KeyboardEvent) {
  694. // TODO(7693961): The event for DidShow is not available on iOS yet.
  695. // Use the one from WillShow and do not assign.
  696. if (e) {
  697. this.keyboardWillOpenTo = e;
  698. }
  699. this.props.onKeyboardDidShow && this.props.onKeyboardDidShow(e);
  700. },
  701. scrollResponderKeyboardDidHide: function(e: KeyboardEvent) {
  702. this.keyboardWillOpenTo = null;
  703. this.props.onKeyboardDidHide && this.props.onKeyboardDidHide(e);
  704. },
  705. };
  706. const ScrollResponder = {
  707. Mixin: ScrollResponderMixin,
  708. };
  709. module.exports = ScrollResponder;