mirror of
https://github.com/zoriya/react-native-web.git
synced 2025-12-06 06:36:13 +00:00
[change] Port ScrollView to Class Component
Port ScrollView from legacy createReactClass to ES6 Class syntax.
This commit is contained in:
committed by
Nicolas Gallagher
parent
a33c322152
commit
2874b281ff
34
package-lock.json
generated
34
package-lock.json
generated
@@ -15424,11 +15424,11 @@
|
||||
"prop-types": "^15.6.0",
|
||||
"react": ">=17.0.2",
|
||||
"react-dom": ">=17.0.2",
|
||||
"react-native-web": "0.18.9"
|
||||
"react-native-web": "0.18.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-loader": "^8.2.5",
|
||||
"babel-plugin-react-native-web": "0.18.9",
|
||||
"babel-plugin-react-native-web": "0.18.10",
|
||||
"css-loader": "^6.7.1",
|
||||
"style-loader": "^3.3.1",
|
||||
"url-loader": "^4.1.1",
|
||||
@@ -15438,15 +15438,15 @@
|
||||
}
|
||||
},
|
||||
"packages/benchmarks/node_modules/babel-plugin-react-native-web": {
|
||||
"version": "0.18.9",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-react-native-web/-/babel-plugin-react-native-web-0.18.9.tgz",
|
||||
"integrity": "sha512-A9rrSfV98CFRS+ACgZorxaHH8gDrVyK2Nea8OHepY4Sv/Mf+vk8uvQq+tRUEBpHnUvd/qRDKIjFLbygecAt9VA==",
|
||||
"version": "0.18.10",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-react-native-web/-/babel-plugin-react-native-web-0.18.10.tgz",
|
||||
"integrity": "sha512-2UiwS6G7XKJvpo0X5OFkzGjHGFuNx9J+DgEG8TEmm+X5S0z6EB59W11RDEZghdKzsQzVbs1jB+2VHBuVgjMTiw==",
|
||||
"dev": true
|
||||
},
|
||||
"packages/benchmarks/node_modules/react-native-web": {
|
||||
"version": "0.18.9",
|
||||
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.18.9.tgz",
|
||||
"integrity": "sha512-BaV5Mpe7u9pN5vTRDW2g+MLh6PbPBJZpXRQM3Jr2cNv7hNa3sxCGh9T+NcW6wOFzf/+USrdrEPI1M9wNyr7vyA==",
|
||||
"version": "0.18.10",
|
||||
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.18.10.tgz",
|
||||
"integrity": "sha512-YV2gtZa1n7ulTGp+HcxH+KsAtaDPBI/dKd9oOQS31zyFHURjObLUVkKnGjkmlYAUReWfvmlU64GzyNwoZF9/tA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.6",
|
||||
"create-react-class": "^15.7.0",
|
||||
@@ -15470,7 +15470,6 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.6",
|
||||
"create-react-class": "^15.7.0",
|
||||
"fbjs": "^3.0.4",
|
||||
"inline-style-prefixer": "^6.0.1",
|
||||
"normalize-css-color": "^1.0.2",
|
||||
@@ -18209,14 +18208,14 @@
|
||||
"version": "file:packages/benchmarks",
|
||||
"requires": {
|
||||
"babel-loader": "^8.2.5",
|
||||
"babel-plugin-react-native-web": "0.18.9",
|
||||
"babel-plugin-react-native-web": "0.18.10",
|
||||
"classnames": "^2.3.1",
|
||||
"css-loader": "^6.7.1",
|
||||
"d3-scale-chromatic": "^3.0.0",
|
||||
"prop-types": "^15.6.0",
|
||||
"react": ">=17.0.2",
|
||||
"react-dom": ">=17.0.2",
|
||||
"react-native-web": "0.18.9",
|
||||
"react-native-web": "0.18.10",
|
||||
"style-loader": "^3.3.1",
|
||||
"url-loader": "^4.1.1",
|
||||
"webpack": "^5.76.0",
|
||||
@@ -18225,15 +18224,15 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"babel-plugin-react-native-web": {
|
||||
"version": "0.18.9",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-react-native-web/-/babel-plugin-react-native-web-0.18.9.tgz",
|
||||
"integrity": "sha512-A9rrSfV98CFRS+ACgZorxaHH8gDrVyK2Nea8OHepY4Sv/Mf+vk8uvQq+tRUEBpHnUvd/qRDKIjFLbygecAt9VA==",
|
||||
"version": "0.18.10",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-react-native-web/-/babel-plugin-react-native-web-0.18.10.tgz",
|
||||
"integrity": "sha512-2UiwS6G7XKJvpo0X5OFkzGjHGFuNx9J+DgEG8TEmm+X5S0z6EB59W11RDEZghdKzsQzVbs1jB+2VHBuVgjMTiw==",
|
||||
"dev": true
|
||||
},
|
||||
"react-native-web": {
|
||||
"version": "0.18.9",
|
||||
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.18.9.tgz",
|
||||
"integrity": "sha512-BaV5Mpe7u9pN5vTRDW2g+MLh6PbPBJZpXRQM3Jr2cNv7hNa3sxCGh9T+NcW6wOFzf/+USrdrEPI1M9wNyr7vyA==",
|
||||
"version": "0.18.10",
|
||||
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.18.10.tgz",
|
||||
"integrity": "sha512-YV2gtZa1n7ulTGp+HcxH+KsAtaDPBI/dKd9oOQS31zyFHURjObLUVkKnGjkmlYAUReWfvmlU64GzyNwoZF9/tA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.18.6",
|
||||
"create-react-class": "^15.7.0",
|
||||
@@ -23606,7 +23605,6 @@
|
||||
"version": "file:packages/react-native-web",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.18.6",
|
||||
"create-react-class": "^15.7.0",
|
||||
"fbjs": "^3.0.4",
|
||||
"inline-style-prefixer": "^6.0.1",
|
||||
"normalize-css-color": "^1.0.2",
|
||||
|
||||
@@ -13,11 +13,11 @@
|
||||
"prop-types": "^15.6.0",
|
||||
"react": ">=17.0.2",
|
||||
"react-dom": ">=17.0.2",
|
||||
"react-native-web": "0.18.9"
|
||||
"react-native-web": "0.18.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-loader": "^8.2.5",
|
||||
"babel-plugin-react-native-web": "0.18.9",
|
||||
"babel-plugin-react-native-web": "0.18.10",
|
||||
"css-loader": "^6.7.1",
|
||||
"style-loader": "^3.3.1",
|
||||
"url-loader": "^4.1.1",
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.6",
|
||||
"create-react-class": "^15.7.0",
|
||||
"fbjs": "^3.0.4",
|
||||
"inline-style-prefixer": "^6.0.1",
|
||||
"normalize-css-color": "^1.0.2",
|
||||
|
||||
@@ -10,15 +10,18 @@
|
||||
|
||||
import type { ViewProps, ViewStyle } from '../View/types';
|
||||
|
||||
import createReactClass from 'create-react-class';
|
||||
import Dimensions from '../Dimensions';
|
||||
import dismissKeyboard from '../../modules/dismissKeyboard';
|
||||
import invariant from 'fbjs/lib/invariant';
|
||||
import mergeRefs from '../../modules/mergeRefs';
|
||||
import ScrollResponder from '../../modules/ScrollResponder';
|
||||
import Platform from '../Platform';
|
||||
import ScrollViewBase from './ScrollViewBase';
|
||||
import StyleSheet from '../StyleSheet';
|
||||
import TextInputState from '../../modules/TextInputState';
|
||||
import UIManager from '../UIManager';
|
||||
import View from '../View';
|
||||
import React from 'react';
|
||||
import warning from 'fbjs/lib/warning';
|
||||
|
||||
type ScrollViewProps = {
|
||||
...ViewProps,
|
||||
@@ -35,19 +38,436 @@ type ScrollViewProps = {
|
||||
stickyHeaderIndices?: Array<number>
|
||||
};
|
||||
|
||||
type Event = Object;
|
||||
|
||||
const emptyObject = {};
|
||||
const IS_ANIMATING_TOUCH_START_THRESHOLD_MS = 16;
|
||||
|
||||
/* eslint-disable react/prefer-es6-class */
|
||||
const ScrollView = ((createReactClass({
|
||||
mixins: [ScrollResponder.Mixin],
|
||||
class ScrollView extends React.Component<ScrollViewProps> {
|
||||
_scrollNodeRef: any = null;
|
||||
_innerViewRef: any = null;
|
||||
|
||||
getInitialState() {
|
||||
return this.scrollResponderMixinGetInitialState();
|
||||
},
|
||||
/**
|
||||
* ------------------------------------------------------
|
||||
* START SCROLLRESPONDER
|
||||
* ------------------------------------------------------
|
||||
*/
|
||||
isTouching: boolean = false;
|
||||
lastMomentumScrollBeginTime: number = 0;
|
||||
lastMomentumScrollEndTime: number = 0;
|
||||
// Reset to false every time becomes responder. This is used to:
|
||||
// - Determine if the scroll view has been scrolled and therefore should
|
||||
// refuse to give up its responder lock.
|
||||
// - Determine if releasing should dismiss the keyboard when we are in
|
||||
// tap-to-dismiss mode (!this.props.keyboardShouldPersistTaps).
|
||||
observedScrollSinceBecomingResponder: boolean = false;
|
||||
becameResponderWhileAnimating: boolean = false;
|
||||
|
||||
flashScrollIndicators() {
|
||||
/**
|
||||
* Invoke this from an `onScroll` event.
|
||||
*/
|
||||
scrollResponderHandleScrollShouldSetResponder: boolean = () => {
|
||||
return this.isTouching;
|
||||
};
|
||||
|
||||
/**
|
||||
* Merely touch starting is not sufficient for a scroll view to become the
|
||||
* responder. Being the "responder" means that the very next touch move/end
|
||||
* event will result in an action/movement.
|
||||
*
|
||||
* Invoke this from an `onStartShouldSetResponder` event.
|
||||
*
|
||||
* `onStartShouldSetResponder` is used when the next move/end will trigger
|
||||
* some UI movement/action, but when you want to yield priority to views
|
||||
* nested inside of the view.
|
||||
*
|
||||
* There may be some cases where scroll views actually should return `true`
|
||||
* from `onStartShouldSetResponder`: Any time we are detecting a standard tap
|
||||
* that gives priority to nested views.
|
||||
*
|
||||
* - If a single tap on the scroll view triggers an action such as
|
||||
* recentering a map style view yet wants to give priority to interaction
|
||||
* views inside (such as dropped pins or labels), then we would return true
|
||||
* from this method when there is a single touch.
|
||||
*
|
||||
* - Similar to the previous case, if a two finger "tap" should trigger a
|
||||
* zoom, we would check the `touches` count, and if `>= 2`, we would return
|
||||
* true.
|
||||
*
|
||||
*/
|
||||
scrollResponderHandleStartShouldSetResponder(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* There are times when the scroll view wants to become the responder
|
||||
* (meaning respond to the next immediate `touchStart/touchEnd`), in a way
|
||||
* that *doesn't* give priority to nested views (hence the capture phase):
|
||||
*
|
||||
* - Currently animating.
|
||||
* - Tapping anywhere that is not the focused input, while the keyboard is
|
||||
* up (which should dismiss the keyboard).
|
||||
*
|
||||
* Invoke this from an `onStartShouldSetResponderCapture` event.
|
||||
*/
|
||||
scrollResponderHandleStartShouldSetResponderCapture: boolean = (e: Event) => {
|
||||
// First see if we want to eat taps while the keyboard is up
|
||||
// var currentlyFocusedTextInput = TextInputState.currentlyFocusedField();
|
||||
// if (!this.props.keyboardShouldPersistTaps &&
|
||||
// currentlyFocusedTextInput != null &&
|
||||
// e.target !== currentlyFocusedTextInput) {
|
||||
// return true;
|
||||
// }
|
||||
return this.scrollResponderIsAnimating();
|
||||
};
|
||||
|
||||
/**
|
||||
* Invoke this from an `onResponderReject` event.
|
||||
*
|
||||
* Some other element is not yielding its role as responder. Normally, we'd
|
||||
* just disable the `UIScrollView`, but a touch has already began on it, the
|
||||
* `UIScrollView` will not accept being disabled after that. The easiest
|
||||
* solution for now is to accept the limitation of disallowing this
|
||||
* altogether. To improve this, find a way to disable the `UIScrollView` after
|
||||
* a touch has already started.
|
||||
*/
|
||||
scrollResponderHandleResponderReject() {
|
||||
warning(false, "ScrollView doesn't take rejection well - scrolls anyway");
|
||||
}
|
||||
|
||||
/**
|
||||
* We will allow the scroll view to give up its lock iff it acquired the lock
|
||||
* during an animation. This is a very useful default that happens to satisfy
|
||||
* many common user experiences.
|
||||
*
|
||||
* - Stop a scroll on the left edge, then turn that into an outer view's
|
||||
* backswipe.
|
||||
* - Stop a scroll mid-bounce at the top, continue pulling to have the outer
|
||||
* view dismiss.
|
||||
* - However, without catching the scroll view mid-bounce (while it is
|
||||
* motionless), if you drag far enough for the scroll view to become
|
||||
* responder (and therefore drag the scroll view a bit), any backswipe
|
||||
* navigation of a swipe gesture higher in the view hierarchy, should be
|
||||
* rejected.
|
||||
*/
|
||||
scrollResponderHandleTerminationRequest: boolean = () => {
|
||||
return !this.observedScrollSinceBecomingResponder;
|
||||
};
|
||||
|
||||
/**
|
||||
* Invoke this from an `onTouchEnd` event.
|
||||
*
|
||||
* @param {SyntheticEvent} e Event.
|
||||
*/
|
||||
scrollResponderHandleTouchEnd = (e: Event) => {
|
||||
const nativeEvent = e.nativeEvent;
|
||||
this.isTouching = nativeEvent.touches.length !== 0;
|
||||
this.props.onTouchEnd && this.props.onTouchEnd(e);
|
||||
};
|
||||
|
||||
/**
|
||||
* Invoke this from an `onResponderRelease` event.
|
||||
*/
|
||||
scrollResponderHandleResponderRelease = (e: Event) => {
|
||||
this.props.onResponderRelease && this.props.onResponderRelease(e);
|
||||
|
||||
// By default scroll views will unfocus a textField
|
||||
// if another touch occurs outside of it
|
||||
const currentlyFocusedTextInput = TextInputState.currentlyFocusedField();
|
||||
if (
|
||||
!this.props.keyboardShouldPersistTaps &&
|
||||
currentlyFocusedTextInput != null &&
|
||||
e.target !== currentlyFocusedTextInput &&
|
||||
!this.observedScrollSinceBecomingResponder &&
|
||||
!this.becameResponderWhileAnimating
|
||||
) {
|
||||
this.props.onScrollResponderKeyboardDismissed &&
|
||||
this.props.onScrollResponderKeyboardDismissed(e);
|
||||
TextInputState.blurTextInput(currentlyFocusedTextInput);
|
||||
}
|
||||
};
|
||||
|
||||
scrollResponderHandleScroll = (e: Event) => {
|
||||
this.observedScrollSinceBecomingResponder = true;
|
||||
this.props.onScroll && this.props.onScroll(e);
|
||||
};
|
||||
|
||||
/**
|
||||
* Invoke this from an `onResponderGrant` event.
|
||||
*/
|
||||
scrollResponderHandleResponderGrant = (e: Event) => {
|
||||
this.observedScrollSinceBecomingResponder = false;
|
||||
this.props.onResponderGrant && this.props.onResponderGrant(e);
|
||||
this.becameResponderWhileAnimating = this.scrollResponderIsAnimating();
|
||||
};
|
||||
|
||||
/**
|
||||
* Unfortunately, `onScrollBeginDrag` also fires when *stopping* the scroll
|
||||
* animation, and there's not an easy way to distinguish a drag vs. stopping
|
||||
* momentum.
|
||||
*
|
||||
* Invoke this from an `onScrollBeginDrag` event.
|
||||
*/
|
||||
scrollResponderHandleScrollBeginDrag = (e: Event) => {
|
||||
this.props.onScrollBeginDrag && this.props.onScrollBeginDrag(e);
|
||||
};
|
||||
|
||||
/**
|
||||
* Invoke this from an `onScrollEndDrag` event.
|
||||
*/
|
||||
scrollResponderHandleScrollEndDrag = (e: Event) => {
|
||||
this.props.onScrollEndDrag && this.props.onScrollEndDrag(e);
|
||||
};
|
||||
|
||||
/**
|
||||
* Invoke this from an `onMomentumScrollBegin` event.
|
||||
*/
|
||||
scrollResponderHandleMomentumScrollBegin = (e: Event) => {
|
||||
this.lastMomentumScrollBeginTime = Date.now();
|
||||
this.props.onMomentumScrollBegin && this.props.onMomentumScrollBegin(e);
|
||||
};
|
||||
|
||||
/**
|
||||
* Invoke this from an `onMomentumScrollEnd` event.
|
||||
*/
|
||||
scrollResponderHandleMomentumScrollEnd = (e: Event) => {
|
||||
this.lastMomentumScrollEndTime = Date.now();
|
||||
this.props.onMomentumScrollEnd && this.props.onMomentumScrollEnd(e);
|
||||
};
|
||||
|
||||
/**
|
||||
* Invoke this from an `onTouchStart` event.
|
||||
*
|
||||
* Since we know that the `SimpleEventPlugin` occurs later in the plugin
|
||||
* order, after `ResponderEventPlugin`, we can detect that we were *not*
|
||||
* permitted to be the responder (presumably because a contained view became
|
||||
* responder). The `onResponderReject` won't fire in that case - it only
|
||||
* fires when a *current* responder rejects our request.
|
||||
*
|
||||
* @param {SyntheticEvent} e Touch Start event.
|
||||
*/
|
||||
scrollResponderHandleTouchStart = (e: Event) => {
|
||||
this.isTouching = true;
|
||||
this.props.onTouchStart && this.props.onTouchStart(e);
|
||||
};
|
||||
|
||||
/**
|
||||
* Invoke this from an `onTouchMove` event.
|
||||
*
|
||||
* Since we know that the `SimpleEventPlugin` occurs later in the plugin
|
||||
* order, after `ResponderEventPlugin`, we can detect that we were *not*
|
||||
* permitted to be the responder (presumably because a contained view became
|
||||
* responder). The `onResponderReject` won't fire in that case - it only
|
||||
* fires when a *current* responder rejects our request.
|
||||
*
|
||||
* @param {SyntheticEvent} e Touch Start event.
|
||||
*/
|
||||
scrollResponderHandleTouchMove = (e: Event) => {
|
||||
this.props.onTouchMove && this.props.onTouchMove(e);
|
||||
};
|
||||
|
||||
/**
|
||||
* A helper function for this class that lets us quickly determine if the
|
||||
* view is currently animating. This is particularly useful to know when
|
||||
* a touch has just started or ended.
|
||||
*/
|
||||
scrollResponderIsAnimating: boolean = () => {
|
||||
const now = Date.now();
|
||||
const timeSinceLastMomentumScrollEnd = now - this.lastMomentumScrollEndTime;
|
||||
const isAnimating =
|
||||
timeSinceLastMomentumScrollEnd < IS_ANIMATING_TOUCH_START_THRESHOLD_MS ||
|
||||
this.lastMomentumScrollEndTime < this.lastMomentumScrollBeginTime;
|
||||
return isAnimating;
|
||||
};
|
||||
|
||||
/**
|
||||
* A helper function to scroll to a specific point in the scrollview.
|
||||
* This is currently used to help focus on child textviews, but can also
|
||||
* be used to quickly scroll to any element we want to focus. Syntax:
|
||||
*
|
||||
* scrollResponderScrollTo(options: {x: number = 0; y: number = 0; animated: boolean = true})
|
||||
*
|
||||
* Note: The weird argument signature is due to the fact that, for historical reasons,
|
||||
* the function also accepts separate arguments as as alternative to the options object.
|
||||
* This is deprecated due to ambiguity (y before x), and SHOULD NOT BE USED.
|
||||
*/
|
||||
scrollResponderScrollTo = (
|
||||
x?: number | { x?: number, y?: number, animated?: boolean },
|
||||
y?: number,
|
||||
animated?: boolean
|
||||
) => {
|
||||
if (typeof x === 'number') {
|
||||
console.warn(
|
||||
'`scrollResponderScrollTo(x, y, animated)` is deprecated. Use `scrollResponderScrollTo({x: 5, y: 5, animated: true})` instead.'
|
||||
);
|
||||
} else {
|
||||
({ x, y, animated } = x || emptyObject);
|
||||
}
|
||||
const node = this.getScrollableNode();
|
||||
const left = x || 0;
|
||||
const top = y || 0;
|
||||
if (node != null) {
|
||||
if (typeof node.scroll === 'function') {
|
||||
node.scroll({ top, left, behavior: !animated ? 'auto' : 'smooth' });
|
||||
} else {
|
||||
node.scrollLeft = left;
|
||||
node.scrollTop = top;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A helper function to zoom to a specific rect in the scrollview. The argument has the shape
|
||||
* {x: number; y: number; width: number; height: number; animated: boolean = true}
|
||||
*
|
||||
* @platform ios
|
||||
*/
|
||||
scrollResponderZoomTo = (
|
||||
rect: {
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
animated?: boolean
|
||||
},
|
||||
animated?: boolean // deprecated, put this inside the rect argument instead
|
||||
) => {
|
||||
if (Platform.OS !== 'ios') {
|
||||
invariant('zoomToRect is not implemented');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Displays the scroll indicators momentarily.
|
||||
*/
|
||||
scrollResponderFlashScrollIndicators() {}
|
||||
|
||||
/**
|
||||
* This method should be used as the callback to onFocus in a TextInputs'
|
||||
* parent view. Note that any module using this mixin needs to return
|
||||
* the parent view's ref in getScrollViewRef() in order to use this method.
|
||||
* @param {any} nodeHandle The TextInput node handle
|
||||
* @param {number} additionalOffset The scroll view's top "contentInset".
|
||||
* Default is 0.
|
||||
* @param {bool} preventNegativeScrolling Whether to allow pulling the content
|
||||
* down to make it meet the keyboard's top. Default is false.
|
||||
*/
|
||||
scrollResponderScrollNativeHandleToKeyboard = (
|
||||
nodeHandle: any,
|
||||
additionalOffset?: number,
|
||||
preventNegativeScrollOffset?: boolean
|
||||
) => {
|
||||
this.additionalScrollOffset = additionalOffset || 0;
|
||||
this.preventNegativeScrollOffset = !!preventNegativeScrollOffset;
|
||||
UIManager.measureLayout(
|
||||
nodeHandle,
|
||||
this.getInnerViewNode(),
|
||||
this.scrollResponderTextInputFocusError,
|
||||
this.scrollResponderInputMeasureAndScrollToKeyboard
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* The calculations performed here assume the scroll view takes up the entire
|
||||
* screen - even if has some content inset. We then measure the offsets of the
|
||||
* keyboard, and compensate both for the scroll view's "contentInset".
|
||||
*
|
||||
* @param {number} left Position of input w.r.t. table view.
|
||||
* @param {number} top Position of input w.r.t. table view.
|
||||
* @param {number} width Width of the text input.
|
||||
* @param {number} height Height of the text input.
|
||||
*/
|
||||
scrollResponderInputMeasureAndScrollToKeyboard = (
|
||||
left: number,
|
||||
top: number,
|
||||
width: number,
|
||||
height: number
|
||||
) => {
|
||||
let keyboardScreenY = Dimensions.get('window').height;
|
||||
if (this.keyboardWillOpenTo) {
|
||||
keyboardScreenY = this.keyboardWillOpenTo.endCoordinates.screenY;
|
||||
}
|
||||
let scrollOffsetY =
|
||||
top - keyboardScreenY + height + this.additionalScrollOffset;
|
||||
|
||||
// By default, this can scroll with negative offset, pulling the content
|
||||
// down so that the target component's bottom meets the keyboard's top.
|
||||
// If requested otherwise, cap the offset at 0 minimum to avoid content
|
||||
// shifting down.
|
||||
if (this.preventNegativeScrollOffset) {
|
||||
scrollOffsetY = Math.max(0, scrollOffsetY);
|
||||
}
|
||||
this.scrollResponderScrollTo({ x: 0, y: scrollOffsetY, animated: true });
|
||||
|
||||
this.additionalOffset = 0;
|
||||
this.preventNegativeScrollOffset = false;
|
||||
};
|
||||
|
||||
scrollResponderTextInputFocusError(e: Event) {
|
||||
console.error('Error measuring text field: ', e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Warning, this may be called several times for a single keyboard opening.
|
||||
* It's best to store the information in this method and then take any action
|
||||
* at a later point (either in `keyboardDidShow` or other).
|
||||
*
|
||||
* Here's the order that events occur in:
|
||||
* - focus
|
||||
* - willShow {startCoordinates, endCoordinates} several times
|
||||
* - didShow several times
|
||||
* - blur
|
||||
* - willHide {startCoordinates, endCoordinates} several times
|
||||
* - didHide several times
|
||||
*
|
||||
* The `ScrollResponder` providesModule callbacks for each of these events.
|
||||
* Even though any user could have easily listened to keyboard events
|
||||
* themselves, using these `props` callbacks ensures that ordering of events
|
||||
* is consistent - and not dependent on the order that the keyboard events are
|
||||
* subscribed to. This matters when telling the scroll view to scroll to where
|
||||
* the keyboard is headed - the scroll responder better have been notified of
|
||||
* the keyboard destination before being instructed to scroll to where the
|
||||
* keyboard will be. Stick to the `ScrollResponder` callbacks, and everything
|
||||
* will work.
|
||||
*
|
||||
* WARNING: These callbacks will fire even if a keyboard is displayed in a
|
||||
* different navigation pane. Filter out the events to determine if they are
|
||||
* relevant to you. (For example, only if you receive these callbacks after
|
||||
* you had explicitly focused a node etc).
|
||||
*/
|
||||
scrollResponderKeyboardWillShow = (e: Event) => {
|
||||
this.keyboardWillOpenTo = e;
|
||||
this.props.onKeyboardWillShow && this.props.onKeyboardWillShow(e);
|
||||
};
|
||||
|
||||
scrollResponderKeyboardWillHide = (e: Event) => {
|
||||
this.keyboardWillOpenTo = null;
|
||||
this.props.onKeyboardWillHide && this.props.onKeyboardWillHide(e);
|
||||
};
|
||||
|
||||
scrollResponderKeyboardDidShow = (e: Event) => {
|
||||
// TODO(7693961): The event for DidShow is not available on iOS yet.
|
||||
// Use the one from WillShow and do not assign.
|
||||
if (e) {
|
||||
this.keyboardWillOpenTo = e;
|
||||
}
|
||||
this.props.onKeyboardDidShow && this.props.onKeyboardDidShow(e);
|
||||
};
|
||||
|
||||
scrollResponderKeyboardDidHide = (e: Event) => {
|
||||
this.keyboardWillOpenTo = null;
|
||||
this.props.onKeyboardDidHide && this.props.onKeyboardDidHide(e);
|
||||
};
|
||||
|
||||
/**
|
||||
* ------------------------------------------------------
|
||||
* END SCROLLRESPONDER
|
||||
* ------------------------------------------------------
|
||||
*/
|
||||
|
||||
flashScrollIndicators = () => {
|
||||
this.scrollResponderFlashScrollIndicators();
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a reference to the underlying scroll responder, which supports
|
||||
@@ -55,25 +475,25 @@ const ScrollView = ((createReactClass({
|
||||
* implement this method so that they can be composed while providing access
|
||||
* to the underlying scroll responder's methods.
|
||||
*/
|
||||
getScrollResponder(): ScrollView {
|
||||
getScrollResponder: ScrollView = () => {
|
||||
return this;
|
||||
},
|
||||
};
|
||||
|
||||
getScrollableNode(): any {
|
||||
getScrollableNode = () => {
|
||||
return this._scrollNodeRef;
|
||||
},
|
||||
};
|
||||
|
||||
getInnerViewRef(): any {
|
||||
getInnerViewRef = () => {
|
||||
return this._innerViewRef;
|
||||
},
|
||||
};
|
||||
|
||||
getInnerViewNode(): any {
|
||||
getInnerViewNode = () => {
|
||||
return this._innerViewRef;
|
||||
},
|
||||
};
|
||||
|
||||
getNativeScrollRef(): any {
|
||||
getNativeScrollRef = () => {
|
||||
return this._scrollNodeRef;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Scrolls to a given x, y offset, either immediately or with a smooth animation.
|
||||
@@ -85,11 +505,11 @@ const ScrollView = ((createReactClass({
|
||||
* the function also accepts separate arguments as as alternative to the options object.
|
||||
* This is deprecated due to ambiguity (y before x), and SHOULD NOT BE USED.
|
||||
*/
|
||||
scrollTo(
|
||||
scrollTo = (
|
||||
y?: number | { x?: number, y?: number, animated?: boolean },
|
||||
x?: number,
|
||||
animated?: boolean
|
||||
) {
|
||||
) => {
|
||||
if (typeof y === 'number') {
|
||||
console.warn(
|
||||
'`scrollTo(y, x, animated)` is deprecated. Use `scrollTo({x: 5, y: 5, animated: true})` instead.'
|
||||
@@ -98,12 +518,12 @@ const ScrollView = ((createReactClass({
|
||||
({ x, y, animated } = y || emptyObject);
|
||||
}
|
||||
|
||||
this.getScrollResponder().scrollResponderScrollTo({
|
||||
this.scrollResponderScrollTo({
|
||||
x: x || 0,
|
||||
y: y || 0,
|
||||
animated: animated !== false
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* If this is a vertical ScrollView scrolls to the bottom.
|
||||
@@ -113,17 +533,15 @@ const ScrollView = ((createReactClass({
|
||||
* `scrollToEnd({ animated: false })` for immediate scrolling.
|
||||
* If no options are passed, `animated` defaults to true.
|
||||
*/
|
||||
scrollToEnd(options?: { animated?: boolean }) {
|
||||
scrollToEnd = (options?: { animated?: boolean }) => {
|
||||
// Default to true
|
||||
const animated = (options && options.animated) !== false;
|
||||
const { horizontal } = this.props;
|
||||
const scrollResponder = this.getScrollResponder();
|
||||
const scrollResponderNode =
|
||||
scrollResponder.scrollResponderGetScrollableNode();
|
||||
const scrollResponderNode = this.getScrollableNode();
|
||||
const x = horizontal ? scrollResponderNode.scrollWidth : 0;
|
||||
const y = horizontal ? 0 : scrollResponderNode.scrollHeight;
|
||||
scrollResponder.scrollResponderScrollTo({ x, y, animated });
|
||||
},
|
||||
this.scrollResponderScrollTo({ x, y, animated });
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
@@ -251,14 +669,14 @@ const ScrollView = ((createReactClass({
|
||||
}
|
||||
|
||||
return scrollView;
|
||||
},
|
||||
}
|
||||
|
||||
_handleContentOnLayout(e: Object) {
|
||||
_handleContentOnLayout = (e: Object) => {
|
||||
const { width, height } = e.nativeEvent.layout;
|
||||
this.props.onContentSizeChange(width, height);
|
||||
},
|
||||
};
|
||||
|
||||
_handleScroll(e: Object) {
|
||||
_handleScroll = (e: Object) => {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (this.props.onScroll && this.props.scrollEventThrottle == null) {
|
||||
console.log(
|
||||
@@ -276,13 +694,13 @@ const ScrollView = ((createReactClass({
|
||||
}
|
||||
|
||||
this.scrollResponderHandleScroll(e);
|
||||
},
|
||||
};
|
||||
|
||||
_setInnerViewRef(node) {
|
||||
_setInnerViewRef = (node) => {
|
||||
this._innerViewRef = node;
|
||||
},
|
||||
};
|
||||
|
||||
_setScrollNodeRef(node) {
|
||||
_setScrollNodeRef = (node) => {
|
||||
this._scrollNodeRef = node;
|
||||
// ScrollView needs to add more methods to the hostNode in addition to those
|
||||
// added by `usePlatformMethods`. This is temporarily until an API like
|
||||
@@ -302,8 +720,8 @@ const ScrollView = ((createReactClass({
|
||||
}
|
||||
const ref = mergeRefs(this.props.forwardedRef);
|
||||
ref(node);
|
||||
}
|
||||
}): any): React.ComponentType<ScrollViewProps>);
|
||||
};
|
||||
}
|
||||
|
||||
const commonStyle = {
|
||||
flexGrow: 1,
|
||||
|
||||
@@ -1,546 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Nicolas Gallagher.
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import Dimensions from '../../exports/Dimensions';
|
||||
import invariant from 'fbjs/lib/invariant';
|
||||
import Platform from '../../exports/Platform';
|
||||
import TextInputState from '../TextInputState';
|
||||
import UIManager from '../../exports/UIManager';
|
||||
import warning from 'fbjs/lib/warning';
|
||||
|
||||
/**
|
||||
* Mixin that can be integrated in order to handle scrolling that plays well
|
||||
* with `ResponderEventPlugin`. Integrate with your platform specific scroll
|
||||
* views, or even your custom built (every-frame animating) scroll views so that
|
||||
* all of these systems play well with the `ResponderEventPlugin`.
|
||||
*
|
||||
* iOS scroll event timing nuances:
|
||||
* ===============================
|
||||
*
|
||||
*
|
||||
* Scrolling without bouncing, if you touch down:
|
||||
* -------------------------------
|
||||
*
|
||||
* 1. `onMomentumScrollBegin` (when animation begins after letting up)
|
||||
* ... physical touch starts ...
|
||||
* 2. `onTouchStartCapture` (when you press down to stop the scroll)
|
||||
* 3. `onTouchStart` (same, but bubble phase)
|
||||
* 4. `onResponderRelease` (when lifting up - you could pause forever before * lifting)
|
||||
* 5. `onMomentumScrollEnd`
|
||||
*
|
||||
*
|
||||
* Scrolling with bouncing, if you touch down:
|
||||
* -------------------------------
|
||||
*
|
||||
* 1. `onMomentumScrollBegin` (when animation begins after letting up)
|
||||
* ... bounce begins ...
|
||||
* ... some time elapses ...
|
||||
* ... physical touch during bounce ...
|
||||
* 2. `onMomentumScrollEnd` (Makes no sense why this occurs first during bounce)
|
||||
* 3. `onTouchStartCapture` (immediately after `onMomentumScrollEnd`)
|
||||
* 4. `onTouchStart` (same, but bubble phase)
|
||||
* 5. `onTouchEnd` (You could hold the touch start for a long time)
|
||||
* 6. `onMomentumScrollBegin` (When releasing the view starts bouncing back)
|
||||
*
|
||||
* So when we receive an `onTouchStart`, how can we tell if we are touching
|
||||
* *during* an animation (which then causes the animation to stop)? The only way
|
||||
* to tell is if the `touchStart` occurred immediately after the
|
||||
* `onMomentumScrollEnd`.
|
||||
*
|
||||
* This is abstracted out for you, so you can just call this.scrollResponderIsAnimating() if
|
||||
* necessary
|
||||
*
|
||||
* `ScrollResponder` also includes logic for blurring a currently focused input
|
||||
* if one is focused while scrolling. The `ScrollResponder` is a natural place
|
||||
* to put this logic since it can support not dismissing the keyboard while
|
||||
* scrolling, unless a recognized "tap"-like gesture has occurred.
|
||||
*
|
||||
* The public lifecycle API includes events for keyboard interaction, responder
|
||||
* interaction, and scrolling (among others). The keyboard callbacks
|
||||
* `onKeyboardWill/Did/*` are *global* events, but are invoked on scroll
|
||||
* responder's props so that you can guarantee that the scroll responder's
|
||||
* internal state has been updated accordingly (and deterministically) by
|
||||
* the time the props callbacks are invoke. Otherwise, you would always wonder
|
||||
* if the scroll responder is currently in a state where it recognizes new
|
||||
* keyboard positions etc. If coordinating scrolling with keyboard movement,
|
||||
* *always* use these hooks instead of listening to your own global keyboard
|
||||
* events.
|
||||
*
|
||||
* Public keyboard lifecycle API: (props callbacks)
|
||||
*
|
||||
* Standard Keyboard Appearance Sequence:
|
||||
*
|
||||
* this.props.onKeyboardWillShow
|
||||
* this.props.onKeyboardDidShow
|
||||
*
|
||||
* `onScrollResponderKeyboardDismissed` will be invoked if an appropriate
|
||||
* tap inside the scroll responder's scrollable region was responsible
|
||||
* for the dismissal of the keyboard. There are other reasons why the
|
||||
* keyboard could be dismissed.
|
||||
*
|
||||
* this.props.onScrollResponderKeyboardDismissed
|
||||
*
|
||||
* Standard Keyboard Hide Sequence:
|
||||
*
|
||||
* this.props.onKeyboardWillHide
|
||||
* this.props.onKeyboardDidHide
|
||||
*/
|
||||
|
||||
const emptyObject = {};
|
||||
|
||||
const IS_ANIMATING_TOUCH_START_THRESHOLD_MS = 16;
|
||||
|
||||
type State = {
|
||||
isTouching: boolean,
|
||||
lastMomentumScrollBeginTime: number,
|
||||
lastMomentumScrollEndTime: number,
|
||||
observedScrollSinceBecomingResponder: boolean,
|
||||
becameResponderWhileAnimating: boolean
|
||||
};
|
||||
type Event = Object;
|
||||
|
||||
const ScrollResponderMixin = {
|
||||
// mixins: [Subscribable.Mixin],
|
||||
scrollResponderMixinGetInitialState: function (): State {
|
||||
return {
|
||||
isTouching: false,
|
||||
lastMomentumScrollBeginTime: 0,
|
||||
lastMomentumScrollEndTime: 0,
|
||||
|
||||
// Reset to false every time becomes responder. This is used to:
|
||||
// - Determine if the scroll view has been scrolled and therefore should
|
||||
// refuse to give up its responder lock.
|
||||
// - Determine if releasing should dismiss the keyboard when we are in
|
||||
// tap-to-dismiss mode (!this.props.keyboardShouldPersistTaps).
|
||||
observedScrollSinceBecomingResponder: false,
|
||||
becameResponderWhileAnimating: false
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Invoke this from an `onScroll` event.
|
||||
*/
|
||||
scrollResponderHandleScrollShouldSetResponder: function (): boolean {
|
||||
return this.state.isTouching;
|
||||
},
|
||||
|
||||
/**
|
||||
* Merely touch starting is not sufficient for a scroll view to become the
|
||||
* responder. Being the "responder" means that the very next touch move/end
|
||||
* event will result in an action/movement.
|
||||
*
|
||||
* Invoke this from an `onStartShouldSetResponder` event.
|
||||
*
|
||||
* `onStartShouldSetResponder` is used when the next move/end will trigger
|
||||
* some UI movement/action, but when you want to yield priority to views
|
||||
* nested inside of the view.
|
||||
*
|
||||
* There may be some cases where scroll views actually should return `true`
|
||||
* from `onStartShouldSetResponder`: Any time we are detecting a standard tap
|
||||
* that gives priority to nested views.
|
||||
*
|
||||
* - If a single tap on the scroll view triggers an action such as
|
||||
* recentering a map style view yet wants to give priority to interaction
|
||||
* views inside (such as dropped pins or labels), then we would return true
|
||||
* from this method when there is a single touch.
|
||||
*
|
||||
* - Similar to the previous case, if a two finger "tap" should trigger a
|
||||
* zoom, we would check the `touches` count, and if `>= 2`, we would return
|
||||
* true.
|
||||
*
|
||||
*/
|
||||
scrollResponderHandleStartShouldSetResponder: function (): boolean {
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* There are times when the scroll view wants to become the responder
|
||||
* (meaning respond to the next immediate `touchStart/touchEnd`), in a way
|
||||
* that *doesn't* give priority to nested views (hence the capture phase):
|
||||
*
|
||||
* - Currently animating.
|
||||
* - Tapping anywhere that is not the focused input, while the keyboard is
|
||||
* up (which should dismiss the keyboard).
|
||||
*
|
||||
* Invoke this from an `onStartShouldSetResponderCapture` event.
|
||||
*/
|
||||
scrollResponderHandleStartShouldSetResponderCapture: function (
|
||||
e: Event
|
||||
): boolean {
|
||||
// First see if we want to eat taps while the keyboard is up
|
||||
// var currentlyFocusedTextInput = TextInputState.currentlyFocusedField();
|
||||
// if (!this.props.keyboardShouldPersistTaps &&
|
||||
// currentlyFocusedTextInput != null &&
|
||||
// e.target !== currentlyFocusedTextInput) {
|
||||
// return true;
|
||||
// }
|
||||
return this.scrollResponderIsAnimating();
|
||||
},
|
||||
|
||||
/**
|
||||
* Invoke this from an `onResponderReject` event.
|
||||
*
|
||||
* Some other element is not yielding its role as responder. Normally, we'd
|
||||
* just disable the `UIScrollView`, but a touch has already began on it, the
|
||||
* `UIScrollView` will not accept being disabled after that. The easiest
|
||||
* solution for now is to accept the limitation of disallowing this
|
||||
* altogether. To improve this, find a way to disable the `UIScrollView` after
|
||||
* a touch has already started.
|
||||
*/
|
||||
scrollResponderHandleResponderReject: function () {
|
||||
warning(false, "ScrollView doesn't take rejection well - scrolls anyway");
|
||||
},
|
||||
|
||||
/**
|
||||
* We will allow the scroll view to give up its lock iff it acquired the lock
|
||||
* during an animation. This is a very useful default that happens to satisfy
|
||||
* many common user experiences.
|
||||
*
|
||||
* - Stop a scroll on the left edge, then turn that into an outer view's
|
||||
* backswipe.
|
||||
* - Stop a scroll mid-bounce at the top, continue pulling to have the outer
|
||||
* view dismiss.
|
||||
* - However, without catching the scroll view mid-bounce (while it is
|
||||
* motionless), if you drag far enough for the scroll view to become
|
||||
* responder (and therefore drag the scroll view a bit), any backswipe
|
||||
* navigation of a swipe gesture higher in the view hierarchy, should be
|
||||
* rejected.
|
||||
*/
|
||||
scrollResponderHandleTerminationRequest: function (): boolean {
|
||||
return !this.state.observedScrollSinceBecomingResponder;
|
||||
},
|
||||
|
||||
/**
|
||||
* Invoke this from an `onTouchEnd` event.
|
||||
*
|
||||
* @param {SyntheticEvent} e Event.
|
||||
*/
|
||||
scrollResponderHandleTouchEnd: function (e: Event) {
|
||||
const nativeEvent = e.nativeEvent;
|
||||
this.state.isTouching = nativeEvent.touches.length !== 0;
|
||||
this.props.onTouchEnd && this.props.onTouchEnd(e);
|
||||
},
|
||||
|
||||
/**
|
||||
* Invoke this from an `onResponderRelease` event.
|
||||
*/
|
||||
scrollResponderHandleResponderRelease: function (e: Event) {
|
||||
this.props.onResponderRelease && this.props.onResponderRelease(e);
|
||||
|
||||
// By default scroll views will unfocus a textField
|
||||
// if another touch occurs outside of it
|
||||
const currentlyFocusedTextInput = TextInputState.currentlyFocusedField();
|
||||
if (
|
||||
!this.props.keyboardShouldPersistTaps &&
|
||||
currentlyFocusedTextInput != null &&
|
||||
e.target !== currentlyFocusedTextInput &&
|
||||
!this.state.observedScrollSinceBecomingResponder &&
|
||||
!this.state.becameResponderWhileAnimating
|
||||
) {
|
||||
this.props.onScrollResponderKeyboardDismissed &&
|
||||
this.props.onScrollResponderKeyboardDismissed(e);
|
||||
TextInputState.blurTextInput(currentlyFocusedTextInput);
|
||||
}
|
||||
},
|
||||
|
||||
scrollResponderHandleScroll: function (e: Event) {
|
||||
this.state.observedScrollSinceBecomingResponder = true;
|
||||
this.props.onScroll && this.props.onScroll(e);
|
||||
},
|
||||
|
||||
/**
|
||||
* Invoke this from an `onResponderGrant` event.
|
||||
*/
|
||||
scrollResponderHandleResponderGrant: function (e: Event) {
|
||||
this.state.observedScrollSinceBecomingResponder = false;
|
||||
this.props.onResponderGrant && this.props.onResponderGrant(e);
|
||||
this.state.becameResponderWhileAnimating =
|
||||
this.scrollResponderIsAnimating();
|
||||
},
|
||||
|
||||
/**
|
||||
* Unfortunately, `onScrollBeginDrag` also fires when *stopping* the scroll
|
||||
* animation, and there's not an easy way to distinguish a drag vs. stopping
|
||||
* momentum.
|
||||
*
|
||||
* Invoke this from an `onScrollBeginDrag` event.
|
||||
*/
|
||||
scrollResponderHandleScrollBeginDrag: function (e: Event) {
|
||||
this.props.onScrollBeginDrag && this.props.onScrollBeginDrag(e);
|
||||
},
|
||||
|
||||
/**
|
||||
* Invoke this from an `onScrollEndDrag` event.
|
||||
*/
|
||||
scrollResponderHandleScrollEndDrag: function (e: Event) {
|
||||
this.props.onScrollEndDrag && this.props.onScrollEndDrag(e);
|
||||
},
|
||||
|
||||
/**
|
||||
* Invoke this from an `onMomentumScrollBegin` event.
|
||||
*/
|
||||
scrollResponderHandleMomentumScrollBegin: function (e: Event) {
|
||||
this.state.lastMomentumScrollBeginTime = Date.now();
|
||||
this.props.onMomentumScrollBegin && this.props.onMomentumScrollBegin(e);
|
||||
},
|
||||
|
||||
/**
|
||||
* Invoke this from an `onMomentumScrollEnd` event.
|
||||
*/
|
||||
scrollResponderHandleMomentumScrollEnd: function (e: Event) {
|
||||
this.state.lastMomentumScrollEndTime = Date.now();
|
||||
this.props.onMomentumScrollEnd && this.props.onMomentumScrollEnd(e);
|
||||
},
|
||||
|
||||
/**
|
||||
* Invoke this from an `onTouchStart` event.
|
||||
*
|
||||
* Since we know that the `SimpleEventPlugin` occurs later in the plugin
|
||||
* order, after `ResponderEventPlugin`, we can detect that we were *not*
|
||||
* permitted to be the responder (presumably because a contained view became
|
||||
* responder). The `onResponderReject` won't fire in that case - it only
|
||||
* fires when a *current* responder rejects our request.
|
||||
*
|
||||
* @param {SyntheticEvent} e Touch Start event.
|
||||
*/
|
||||
scrollResponderHandleTouchStart: function (e: Event) {
|
||||
this.state.isTouching = true;
|
||||
this.props.onTouchStart && this.props.onTouchStart(e);
|
||||
},
|
||||
|
||||
/**
|
||||
* Invoke this from an `onTouchMove` event.
|
||||
*
|
||||
* Since we know that the `SimpleEventPlugin` occurs later in the plugin
|
||||
* order, after `ResponderEventPlugin`, we can detect that we were *not*
|
||||
* permitted to be the responder (presumably because a contained view became
|
||||
* responder). The `onResponderReject` won't fire in that case - it only
|
||||
* fires when a *current* responder rejects our request.
|
||||
*
|
||||
* @param {SyntheticEvent} e Touch Start event.
|
||||
*/
|
||||
scrollResponderHandleTouchMove: function (e: Event) {
|
||||
this.props.onTouchMove && this.props.onTouchMove(e);
|
||||
},
|
||||
|
||||
/**
|
||||
* A helper function for this class that lets us quickly determine if the
|
||||
* view is currently animating. This is particularly useful to know when
|
||||
* a touch has just started or ended.
|
||||
*/
|
||||
scrollResponderIsAnimating: function (): boolean {
|
||||
const now = Date.now();
|
||||
const timeSinceLastMomentumScrollEnd =
|
||||
now - this.state.lastMomentumScrollEndTime;
|
||||
const isAnimating =
|
||||
timeSinceLastMomentumScrollEnd < IS_ANIMATING_TOUCH_START_THRESHOLD_MS ||
|
||||
this.state.lastMomentumScrollEndTime <
|
||||
this.state.lastMomentumScrollBeginTime;
|
||||
return isAnimating;
|
||||
},
|
||||
|
||||
/**
|
||||
* A helper function to scroll to a specific point in the scrollview.
|
||||
* This is currently used to help focus on child textviews, but can also
|
||||
* be used to quickly scroll to any element we want to focus. Syntax:
|
||||
*
|
||||
* scrollResponderScrollTo(options: {x: number = 0; y: number = 0; animated: boolean = true})
|
||||
*
|
||||
* Note: The weird argument signature is due to the fact that, for historical reasons,
|
||||
* the function also accepts separate arguments as as alternative to the options object.
|
||||
* This is deprecated due to ambiguity (y before x), and SHOULD NOT BE USED.
|
||||
*/
|
||||
scrollResponderScrollTo: function (
|
||||
x?: number | { x?: number, y?: number, animated?: boolean },
|
||||
y?: number,
|
||||
animated?: boolean
|
||||
) {
|
||||
if (typeof x === 'number') {
|
||||
console.warn(
|
||||
'`scrollResponderScrollTo(x, y, animated)` is deprecated. Use `scrollResponderScrollTo({x: 5, y: 5, animated: true})` instead.'
|
||||
);
|
||||
} else {
|
||||
({ x, y, animated } = x || emptyObject);
|
||||
}
|
||||
const node = this.getScrollableNode();
|
||||
const left = x || 0;
|
||||
const top = y || 0;
|
||||
if (typeof node.scroll === 'function') {
|
||||
node.scroll({ top, left, behavior: !animated ? 'auto' : 'smooth' });
|
||||
} else {
|
||||
node.scrollLeft = left;
|
||||
node.scrollTop = top;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* A helper function to zoom to a specific rect in the scrollview. The argument has the shape
|
||||
* {x: number; y: number; width: number; height: number; animated: boolean = true}
|
||||
*
|
||||
* @platform ios
|
||||
*/
|
||||
scrollResponderZoomTo: function (
|
||||
rect: {
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
animated?: boolean
|
||||
},
|
||||
animated?: boolean // deprecated, put this inside the rect argument instead
|
||||
) {
|
||||
if (Platform.OS !== 'ios') {
|
||||
invariant('zoomToRect is not implemented');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Displays the scroll indicators momentarily.
|
||||
*/
|
||||
scrollResponderFlashScrollIndicators: function () {},
|
||||
|
||||
/**
|
||||
* This method should be used as the callback to onFocus in a TextInputs'
|
||||
* parent view. Note that any module using this mixin needs to return
|
||||
* the parent view's ref in getScrollViewRef() in order to use this method.
|
||||
* @param {any} nodeHandle The TextInput node handle
|
||||
* @param {number} additionalOffset The scroll view's top "contentInset".
|
||||
* Default is 0.
|
||||
* @param {bool} preventNegativeScrolling Whether to allow pulling the content
|
||||
* down to make it meet the keyboard's top. Default is false.
|
||||
*/
|
||||
scrollResponderScrollNativeHandleToKeyboard: function (
|
||||
nodeHandle: any,
|
||||
additionalOffset?: number,
|
||||
preventNegativeScrollOffset?: boolean
|
||||
) {
|
||||
this.additionalScrollOffset = additionalOffset || 0;
|
||||
this.preventNegativeScrollOffset = !!preventNegativeScrollOffset;
|
||||
UIManager.measureLayout(
|
||||
nodeHandle,
|
||||
this.getInnerViewNode(),
|
||||
this.scrollResponderTextInputFocusError,
|
||||
this.scrollResponderInputMeasureAndScrollToKeyboard
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* The calculations performed here assume the scroll view takes up the entire
|
||||
* screen - even if has some content inset. We then measure the offsets of the
|
||||
* keyboard, and compensate both for the scroll view's "contentInset".
|
||||
*
|
||||
* @param {number} left Position of input w.r.t. table view.
|
||||
* @param {number} top Position of input w.r.t. table view.
|
||||
* @param {number} width Width of the text input.
|
||||
* @param {number} height Height of the text input.
|
||||
*/
|
||||
scrollResponderInputMeasureAndScrollToKeyboard: function (
|
||||
left: number,
|
||||
top: number,
|
||||
width: number,
|
||||
height: number
|
||||
) {
|
||||
let keyboardScreenY = Dimensions.get('window').height;
|
||||
if (this.keyboardWillOpenTo) {
|
||||
keyboardScreenY = this.keyboardWillOpenTo.endCoordinates.screenY;
|
||||
}
|
||||
let scrollOffsetY =
|
||||
top - keyboardScreenY + height + this.additionalScrollOffset;
|
||||
|
||||
// By default, this can scroll with negative offset, pulling the content
|
||||
// down so that the target component's bottom meets the keyboard's top.
|
||||
// If requested otherwise, cap the offset at 0 minimum to avoid content
|
||||
// shifting down.
|
||||
if (this.preventNegativeScrollOffset) {
|
||||
scrollOffsetY = Math.max(0, scrollOffsetY);
|
||||
}
|
||||
this.scrollResponderScrollTo({ x: 0, y: scrollOffsetY, animated: true });
|
||||
|
||||
this.additionalOffset = 0;
|
||||
this.preventNegativeScrollOffset = false;
|
||||
},
|
||||
|
||||
scrollResponderTextInputFocusError: function (e: Event) {
|
||||
console.error('Error measuring text field: ', e);
|
||||
},
|
||||
|
||||
/**
|
||||
* `componentWillMount` is the closest thing to a standard "constructor" for
|
||||
* React components.
|
||||
*
|
||||
* The `keyboardWillShow` is called before input focus.
|
||||
*/
|
||||
UNSAFE_componentWillMount: function () {
|
||||
this.keyboardWillOpenTo = null;
|
||||
this.additionalScrollOffset = 0;
|
||||
// this.addListenerOn(RCTDeviceEventEmitter, 'keyboardWillShow', this.scrollResponderKeyboardWillShow);
|
||||
// this.addListenerOn(RCTDeviceEventEmitter, 'keyboardWillHide', this.scrollResponderKeyboardWillHide);
|
||||
// this.addListenerOn(RCTDeviceEventEmitter, 'keyboardDidShow', this.scrollResponderKeyboardDidShow);
|
||||
// this.addListenerOn(RCTDeviceEventEmitter, 'keyboardDidHide', this.scrollResponderKeyboardDidHide);
|
||||
},
|
||||
|
||||
/**
|
||||
* Warning, this may be called several times for a single keyboard opening.
|
||||
* It's best to store the information in this method and then take any action
|
||||
* at a later point (either in `keyboardDidShow` or other).
|
||||
*
|
||||
* Here's the order that events occur in:
|
||||
* - focus
|
||||
* - willShow {startCoordinates, endCoordinates} several times
|
||||
* - didShow several times
|
||||
* - blur
|
||||
* - willHide {startCoordinates, endCoordinates} several times
|
||||
* - didHide several times
|
||||
*
|
||||
* The `ScrollResponder` providesModule callbacks for each of these events.
|
||||
* Even though any user could have easily listened to keyboard events
|
||||
* themselves, using these `props` callbacks ensures that ordering of events
|
||||
* is consistent - and not dependent on the order that the keyboard events are
|
||||
* subscribed to. This matters when telling the scroll view to scroll to where
|
||||
* the keyboard is headed - the scroll responder better have been notified of
|
||||
* the keyboard destination before being instructed to scroll to where the
|
||||
* keyboard will be. Stick to the `ScrollResponder` callbacks, and everything
|
||||
* will work.
|
||||
*
|
||||
* WARNING: These callbacks will fire even if a keyboard is displayed in a
|
||||
* different navigation pane. Filter out the events to determine if they are
|
||||
* relevant to you. (For example, only if you receive these callbacks after
|
||||
* you had explicitly focused a node etc).
|
||||
*/
|
||||
scrollResponderKeyboardWillShow: function (e: Event) {
|
||||
this.keyboardWillOpenTo = e;
|
||||
this.props.onKeyboardWillShow && this.props.onKeyboardWillShow(e);
|
||||
},
|
||||
|
||||
scrollResponderKeyboardWillHide: function (e: Event) {
|
||||
this.keyboardWillOpenTo = null;
|
||||
this.props.onKeyboardWillHide && this.props.onKeyboardWillHide(e);
|
||||
},
|
||||
|
||||
scrollResponderKeyboardDidShow: function (e: Event) {
|
||||
// TODO(7693961): The event for DidShow is not available on iOS yet.
|
||||
// Use the one from WillShow and do not assign.
|
||||
if (e) {
|
||||
this.keyboardWillOpenTo = e;
|
||||
}
|
||||
this.props.onKeyboardDidShow && this.props.onKeyboardDidShow(e);
|
||||
},
|
||||
|
||||
scrollResponderKeyboardDidHide: function (e: Event) {
|
||||
this.keyboardWillOpenTo = null;
|
||||
this.props.onKeyboardDidHide && this.props.onKeyboardDidHide(e);
|
||||
}
|
||||
};
|
||||
|
||||
const ScrollResponder = {
|
||||
Mixin: ScrollResponderMixin
|
||||
};
|
||||
|
||||
export default ScrollResponder;
|
||||
Reference in New Issue
Block a user