Authored by Zhu-Arthur

备份更新 review by 陈林

/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ScrollResponder
* @flow
*/
'use strict';
const Dimensions = require('Dimensions');
// modify by yoho 2018/6/5
// const FrameRateLogger = require('FrameRateLogger');
const Keyboard = require('Keyboard');
const ReactNative = require('ReactNative');
const Platform = require('Platform');
// const ReactNative = require('react/lib/ReactNative');
// <eof> modify by yoho
const Subscribable = require('Subscribable');
const TextInputState = require('TextInputState');
const UIManager = require('UIManager');
const invariant = require('fbjs/lib/invariant');
const nullthrows = require('fbjs/lib/nullthrows');
const performanceNow = require('fbjs/lib/performanceNow');
const warning = require('fbjs/lib/warning');
const { ScrollViewManager } = require('NativeModules');
const { getInstanceFromNode } = require('ReactNativeComponentTree');
/**
* 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 IS_ANIMATING_TOUCH_START_THRESHOLD_MS = 16;
type State = {
isTouching: boolean,
lastMomentumScrollBeginTime: number,
lastMomentumScrollEndTime: number,
observedScrollSinceBecomingResponder: boolean,
becameResponderWhileAnimating: boolean,
};
type Event = Object;
// modify by yoho 2018/6/5
// function isTagInstanceOfTextInput(tag) {
// const instance = getInstanceFromNode(tag);
// return instance && instance.viewConfig && (
// instance.viewConfig.uiViewClassName === 'AndroidTextInput' ||
// instance.viewConfig.uiViewClassName === 'RCTMultilineTextInputView' ||
// instance.viewConfig.uiViewClassName === 'RCTSinglelineTextInputView'
// );
// }
// <eof> modify by yoho
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 !== 'always').
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 {
// modify by yoho 2018/6/5
// const currentlyFocusedTextInput = TextInputState.currentlyFocusedField();
//
// if (this.props.keyboardShouldPersistTaps === 'handled' &&
// currentlyFocusedTextInput != null &&
// e.target !== currentlyFocusedTextInput) {
// return true;
// }
// <eof> modify by yoho
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 a text 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
const currentlyFocusedTextInput = TextInputState.currentlyFocusedField();
// modify by yoho 2018/6/5
// const {keyboardShouldPersistTaps} = this.props;
// const keyboardNeverPersistTaps = !keyboardShouldPersistTaps ||
// keyboardShouldPersistTaps === 'never';
// if (keyboardNeverPersistTaps &&
// currentlyFocusedTextInput != null &&
// !isTagInstanceOfTextInput(e.target)) {
// return true;
// }
if (!this.props.keyboardShouldPersistTaps &&
currentlyFocusedTextInput != null &&
e.target !== currentlyFocusedTextInput) {
return true;
}
// <eof> modify by yoho
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() {
},
/**
* 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 `onTouchCancel` event.
*
* @param {SyntheticEvent} e Event.
*/
scrollResponderHandleTouchCancel: function(e: Event) {
this.state.isTouching = false;
this.props.onTouchCancel && this.props.onTouchCancel(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();
// modify by yoho 2018/6/5
// if (this.props.keyboardShouldPersistTaps !== true &&
// this.props.keyboardShouldPersistTaps !== 'always' &&
// currentlyFocusedTextInput != null &&
// e.target !== currentlyFocusedTextInput &&
// !this.state.observedScrollSinceBecomingResponder &&
// !this.state.becameResponderWhileAnimating) {
// this.props.onScrollResponderKeyboardDismissed &&
// this.props.onScrollResponderKeyboardDismissed(e);
// TextInputState.blurTextInput(currentlyFocusedTextInput);
// }
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);
}
// <eof> modify by yoho
},
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) {
// FrameRateLogger.beginScroll(); // TODO: track all scrolls after implementing onScrollEndAnimation // modify by yoho 2018/6/5
this.props.onScrollBeginDrag && this.props.onScrollBeginDrag(e);
},
/**
* Invoke this from an `onScrollEndDrag` event.
*/
scrollResponderHandleScrollEndDrag: function(e: Event) {
// modify by yoho 2018/6/5
// const {velocity} = e.nativeEvent;
// // - If we are animating, then this is a "drag" that is stopping the scrollview and momentum end
// // will fire.
// // - If velocity is non-zero, then the interaction will stop when momentum scroll ends or
// // another drag starts and ends.
// // - If we don't get velocity, better to stop the interaction twice than not stop it.
// if (!this.scrollResponderIsAnimating() &&
// (!velocity || velocity.x === 0 && velocity.y === 0)) {
// FrameRateLogger.endScroll();
// }
// <eof> modify by yoho
this.props.onScrollEndDrag && this.props.onScrollEndDrag(e);
},
/**
* Invoke this from an `onMomentumScrollBegin` event.
*/
scrollResponderHandleMomentumScrollBegin: function(e: Event) {
this.state.lastMomentumScrollBeginTime = performanceNow();
this.props.onMomentumScrollBegin && this.props.onMomentumScrollBegin(e);
},
/**
* Invoke this from an `onMomentumScrollEnd` event.
*/
scrollResponderHandleMomentumScrollEnd: function(e: Event) {
// FrameRateLogger.endScroll(); // modify by yoho 2018/6/5
this.state.lastMomentumScrollEndTime = performanceNow();
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 = performanceNow();
const timeSinceLastMomentumScrollEnd = now - this.state.lastMomentumScrollEndTime;
const isAnimating = timeSinceLastMomentumScrollEnd < IS_ANIMATING_TOUCH_START_THRESHOLD_MS ||
this.state.lastMomentumScrollEndTime < this.state.lastMomentumScrollBeginTime;
return isAnimating;
},
/**
* Returns the node that represents native view that can be scrolled.
* Components can pass what node to use by defining a `getScrollableNode`
* function otherwise `this` is used.
*/
scrollResponderGetScrollableNode: function(): any {
return this.getScrollableNode ?
this.getScrollableNode() :
ReactNative.findNodeHandle(this);
},
/**
* A helper function to scroll to a specific point in the ScrollView.
* This is currently used to help focus 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 || {});
}
UIManager.dispatchViewManagerCommand(
nullthrows(this.scrollResponderGetScrollableNode()),
UIManager.RCTScrollView.Commands.scrollTo,
[x || 0, y || 0, animated !== false],
);
},
// add by yoho 2018/6/5
/****
*设置刷新事件
*
*
**/
scrollResponderStartPullToRefresh: function() {
UIManager.dispatchViewManagerCommand(
this.scrollResponderGetScrollableNode(),
UIManager.RCTScrollView.Commands.startPullToRefresh,
[],
);
},
/****
*停止刷新事件
*
*
**/
scrollResponderStopPullToRefresh: function() {
UIManager.dispatchViewManagerCommand(
this.scrollResponderGetScrollableNode(),
UIManager.RCTScrollView.Commands.stopPullToRefresh,
[],
);
},
// <eof> add by yoho
/**
* Scrolls to the end of the ScrollView, either immediately or with a smooth
* animation.
*
* Example:
*
* `scrollResponderScrollToEnd({animated: true})`
*/
scrollResponderScrollToEnd: function(
options?: { animated?: boolean },
) {
// Default to true
const animated = (options && options.animated) !== false;
UIManager.dispatchViewManagerCommand(
this.scrollResponderGetScrollableNode(),
UIManager.RCTScrollView.Commands.scrollToEnd,
[animated],
);
},
/**
* Deprecated, do not use.
*/
scrollResponderScrollWithoutAnimationTo: function(offsetX: number, offsetY: number) {
console.warn('`scrollResponderScrollWithoutAnimationTo` is deprecated. Use `scrollResponderScrollTo` instead');
this.scrollResponderScrollTo({x: offsetX, y: offsetY, animated: false});
},
/**
* 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
) {
// modify by yoho 2018/6/5
// invariant(ScrollViewManager && ScrollViewManager.zoomToRect, 'zoomToRect is not implemented');
// if ('animated' in rect) {
// animated = rect.animated;
// delete rect.animated;
// } else if (typeof animated !== 'undefined') {
// console.warn('`scrollResponderZoomTo` `animated` argument is deprecated. Use `options.animated` instead');
// }
// ScrollViewManager.zoomToRect(this.scrollResponderGetScrollableNode(), rect, animated !== false);
if (Platform.OS === 'android') {
invariant('zoomToRect is not implemented');
} else {
if ('animated' in rect) {
var { animated, ...rect } = rect;
} else if (typeof animated !== 'undefined') {
console.warn('`scrollResponderZoomTo` `animated` argument is deprecated. Use `options.animated` instead');
}
ScrollViewManager.zoomToRect(this.scrollResponderGetScrollableNode(), rect, animated !== false);
}
// <eof> modify by yoho
},
/**
* Displays the scroll indicators momentarily.
*/
scrollResponderFlashScrollIndicators: function() {
UIManager.dispatchViewManagerCommand(
this.scrollResponderGetScrollableNode(),
UIManager.RCTScrollView.Commands.flashScrollIndicators,
[]
);
},
/**
* 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 bottom "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?: bool) {
this.additionalScrollOffset = additionalOffset || 0;
this.preventNegativeScrollOffset = !!preventNegativeScrollOffset;
UIManager.measureLayout(
nodeHandle,
ReactNative.findNodeHandle(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() {
// modify by yoho 2018/6/5
// const {keyboardShouldPersistTaps} = this.props;
// warning(
// typeof keyboardShouldPersistTaps !== 'boolean',
// `'keyboardShouldPersistTaps={${keyboardShouldPersistTaps}}' is deprecated. `
// + `Use 'keyboardShouldPersistTaps="${keyboardShouldPersistTaps ? 'always' : 'never'}"' instead`
// );
// <eof> modify by yoho
this.keyboardWillOpenTo = null;
this.additionalScrollOffset = 0;
this.addListenerOn(Keyboard, 'keyboardWillShow', this.scrollResponderKeyboardWillShow);
this.addListenerOn(Keyboard, 'keyboardWillHide', this.scrollResponderKeyboardWillHide);
this.addListenerOn(Keyboard, 'keyboardDidShow', this.scrollResponderKeyboardDidShow);
this.addListenerOn(Keyboard, '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,
};
module.exports = ScrollResponder;
... ...
... ... @@ -193,7 +193,7 @@ export type Props<SectionT> = RequiredProps<SectionT> &
const defaultProps = {
...VirtualizedSectionList.defaultProps,
stickySectionHeadersEnabled: Platform.OS === 'ios',
stickySectionHeadersEnabled: true,
};
type DefaultProps = typeof defaultProps;
... ...
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule WebView
*/
'use strict';
var EdgeInsetsPropType = require('EdgeInsetsPropType');
var ActivityIndicator = require('ActivityIndicator');
var React = require('React');
var PropTypes = require('prop-types');
var ReactNative = require('ReactNative');
var StyleSheet = require('StyleSheet');
var UIManager = require('UIManager');
var View = require('View');
var ViewPropTypes = require('ViewPropTypes');
var deprecatedPropType = require('deprecatedPropType');
var keyMirror = require('fbjs/lib/keyMirror');
var requireNativeComponent = require('requireNativeComponent');
var resolveAssetSource = require('resolveAssetSource');
var RCT_WEBVIEW_REF = 'webview';
var WebViewState = keyMirror({
IDLE: null,
LOADING: null,
ERROR: null,
});
//add by yoho
type Event = Object;
// <eof> add by yoho
var defaultRenderLoading = () => (
<View style={styles.loadingView}>
<ActivityIndicator
style={styles.loadingProgressBar}
/>
</View>
);
/**
* Renders a native WebView.
*/
class WebView extends React.Component {
static get extraNativeComponentConfig() {
return {
nativeOnly: {
messagingEnabled: PropTypes.bool,
},
};
}
static propTypes = {
...ViewPropTypes,
renderError: PropTypes.func,
renderLoading: PropTypes.func,
onLoad: PropTypes.func,
onLoadEnd: PropTypes.func,
onLoadStart: PropTypes.func,
onError: PropTypes.func,
automaticallyAdjustContentInsets: PropTypes.bool,
contentInset: EdgeInsetsPropType,
onNavigationStateChange: PropTypes.func,
onMessage: PropTypes.func,
onContentSizeChange: PropTypes.func,
startInLoadingState: PropTypes.bool, // force WebView to show loadingView on first load
style: ViewPropTypes.style,
onShouldStartLoadWithRequest: PropTypes.func, //add by yoho
html: deprecatedPropType(
PropTypes.string,
'Use the `source` prop instead.'
),
url: deprecatedPropType(
PropTypes.string,
'Use the `source` prop instead.'
),
/**
* Loads static html or a uri (with optional headers) in the WebView.
*/
source: PropTypes.oneOfType([
PropTypes.shape({
/*
* The URI to load in the WebView. Can be a local or remote file.
*/
uri: PropTypes.string,
/*
* The HTTP Method to use. Defaults to GET if not specified.
* NOTE: On Android, only GET and POST are supported.
*/
method: PropTypes.oneOf(['GET', 'POST']),
/*
* Additional HTTP headers to send with the request.
* NOTE: On Android, this can only be used with GET requests.
*/
headers: PropTypes.object,
/*
* The HTTP body to send with the request. This must be a valid
* UTF-8 string, and will be sent exactly as specified, with no
* additional encoding (e.g. URL-escaping or base64) applied.
* NOTE: On Android, this can only be used with POST requests.
*/
body: PropTypes.string,
}),
PropTypes.shape({
/*
* A static HTML page to display in the WebView.
*/
html: PropTypes.string,
/*
* The base URL to be used for any relative links in the HTML.
*/
baseUrl: PropTypes.string,
}),
/*
* Used internally by packager.
*/
PropTypes.number,
]),
/**
* Used on Android only, JS is enabled by default for WebView on iOS
* @platform android
*/
javaScriptEnabled: PropTypes.bool,
/**
* Used on Android Lollipop and above only, third party cookies are enabled
* by default for WebView on Android Kitkat and below and on iOS
* @platform android
*/
thirdPartyCookiesEnabled: PropTypes.bool,
/**
* Used on Android only, controls whether DOM Storage is enabled or not
* @platform android
*/
domStorageEnabled: PropTypes.bool,
/**
* Sets the JS to be injected when the webpage loads.
*/
injectedJavaScript: PropTypes.string,
/**
* Sets whether the webpage scales to fit the view and the user can change the scale.
*/
scalesPageToFit: PropTypes.bool,
/**
* Sets the user-agent for this WebView. The user-agent can also be set in native using
* WebViewConfig. This prop will overwrite that config.
*/
userAgent: PropTypes.string,
/**
* Used to locate this view in end-to-end tests.
*/
testID: PropTypes.string,
/**
* Determines whether HTML5 audio & videos require the user to tap before they can
* start playing. The default value is `false`.
*/
mediaPlaybackRequiresUserAction: PropTypes.bool,
/**
* Boolean that sets whether JavaScript running in the context of a file
* scheme URL should be allowed to access content from any origin.
* Including accessing content from other file scheme URLs
* @platform android
*/
allowUniversalAccessFromFileURLs: PropTypes.bool,
/**
* Function that accepts a string that will be passed to the WebView and
* executed immediately as JavaScript.
*/
injectJavaScript: PropTypes.func,
/**
* Specifies the mixed content mode. i.e WebView will allow a secure origin to load content from any other origin.
*
* Possible values for `mixedContentMode` are:
*
* - `'never'` (default) - WebView will not allow a secure origin to load content from an insecure origin.
* - `'always'` - WebView will allow a secure origin to load content from any other origin, even if that origin is insecure.
* - `'compatibility'` - WebView will attempt to be compatible with the approach of a modern web browser with regard to mixed content.
* @platform android
*/
mixedContentMode: PropTypes.oneOf([
'never',
'always',
'compatibility'
]),
/**
* Used on Android only, controls whether form autocomplete data should be saved
* @platform android
*/
saveFormDataDisabled: PropTypes.bool,
/**
* Override the native component used to render the WebView. Enables a custom native
* WebView which uses the same JavaScript as the original WebView.
*/
nativeConfig: PropTypes.shape({
/*
* The native component used to render the WebView.
*/
component: PropTypes.any,
/*
* Set props directly on the native component WebView. Enables custom props which the
* original WebView doesn't pass through.
*/
props: PropTypes.object,
/*
* Set the ViewManager to use for communcation with the native side.
* @platform ios
*/
viewManager: PropTypes.object,
}),
/*
* Used on Android only, controls whether the given list of URL prefixes should
* make {@link com.facebook.react.views.webview.ReactWebViewClient} to launch a
* default activity intent for those URL instead of loading it within the webview.
* Use this to list URLs that WebView cannot handle, e.g. a PDF url.
* @platform android
*/
urlPrefixesForDefaultIntent: PropTypes.arrayOf(PropTypes.string),
};
static defaultProps = {
javaScriptEnabled : true,
thirdPartyCookiesEnabled: true,
scalesPageToFit: true,
saveFormDataDisabled: false
};
state = {
viewState: WebViewState.IDLE,
lastErrorEvent: null,
startInLoadingState: true,
};
UNSAFE_componentWillMount() {
if (this.props.startInLoadingState) {
this.setState({viewState: WebViewState.LOADING});
}
}
render() {
var otherView = null;
if (this.state.viewState === WebViewState.LOADING) {
otherView = (this.props.renderLoading || defaultRenderLoading)();
} else if (this.state.viewState === WebViewState.ERROR) {
var errorEvent = this.state.lastErrorEvent;
otherView = this.props.renderError && this.props.renderError(
errorEvent.domain,
errorEvent.code,
errorEvent.description);
} else if (this.state.viewState !== WebViewState.IDLE) {
console.error('RCTWebView invalid state encountered: ' + this.state.loading);
}
var webViewStyles = [styles.container, this.props.style];
if (this.state.viewState === WebViewState.LOADING ||
this.state.viewState === WebViewState.ERROR) {
// if we're in either LOADING or ERROR states, don't show the webView
webViewStyles.push(styles.hidden);
}
var source = this.props.source || {};
if (this.props.html) {
source.html = this.props.html;
} else if (this.props.url) {
source.uri = this.props.url;
}
if (source.method === 'POST' && source.headers) {
console.warn('WebView: `source.headers` is not supported when using POST.');
} else if (source.method === 'GET' && source.body) {
console.warn('WebView: `source.body` is not supported when using GET.');
}
const nativeConfig = this.props.nativeConfig || {};
let NativeWebView = nativeConfig.component || RCTWebView;
// add by yoho 2018/6/5
var onShouldOverrideUrlLoading = this.props.onShouldStartLoadWithRequest
&& ((event: Event) => {
var shouldOverride = !this.props.onShouldStartLoadWithRequest(event.nativeEvent);
UIManager.dispatchViewManagerCommandSync(
this.getWebViewHandle(),
UIManager.RCTWebView.Commands.shouldOverrideWithResult,
[shouldOverride]);
});
// <eof> add by yoho
var webView =
<NativeWebView
ref={RCT_WEBVIEW_REF}
key="webViewKey"
style={webViewStyles}
source={resolveAssetSource(source)}
scalesPageToFit={this.props.scalesPageToFit}
injectedJavaScript={this.props.injectedJavaScript}
userAgent={this.props.userAgent}
javaScriptEnabled={this.props.javaScriptEnabled}
thirdPartyCookiesEnabled={this.props.thirdPartyCookiesEnabled}
domStorageEnabled={this.props.domStorageEnabled}
messagingEnabled={typeof this.props.onMessage === 'function'}
onMessage={this.onMessage}
contentInset={this.props.contentInset}
automaticallyAdjustContentInsets={this.props.automaticallyAdjustContentInsets}
onContentSizeChange={this.props.onContentSizeChange}
onLoadingStart={this.onLoadingStart}
onLoadingFinish={this.onLoadingFinish}
onLoadingError={this.onLoadingError}
testID={this.props.testID}
mediaPlaybackRequiresUserAction={this.props.mediaPlaybackRequiresUserAction}
allowUniversalAccessFromFileURLs={this.props.allowUniversalAccessFromFileURLs}
mixedContentMode={this.props.mixedContentMode}
saveFormDataDisabled={this.props.saveFormDataDisabled}
onShouldOverrideUrlLoading={onShouldOverrideUrlLoading}
urlPrefixesForDefaultIntent={this.props.urlPrefixesForDefaultIntent}
{...nativeConfig.props}
/>;
return (
<View style={styles.container}>
{webView}
{otherView}
</View>
);
}
goForward = () => {
UIManager.dispatchViewManagerCommand(
this.getWebViewHandle(),
UIManager.RCTWebView.Commands.goForward,
null
);
};
goBack = () => {
UIManager.dispatchViewManagerCommand(
this.getWebViewHandle(),
UIManager.RCTWebView.Commands.goBack,
null
);
};
reload = () => {
this.setState({
viewState: WebViewState.LOADING
});
UIManager.dispatchViewManagerCommand(
this.getWebViewHandle(),
UIManager.RCTWebView.Commands.reload,
null
);
};
stopLoading = () => {
UIManager.dispatchViewManagerCommand(
this.getWebViewHandle(),
UIManager.RCTWebView.Commands.stopLoading,
null
);
};
postMessage = (data) => {
UIManager.dispatchViewManagerCommand(
this.getWebViewHandle(),
UIManager.RCTWebView.Commands.postMessage,
[String(data)]
);
};
/**
* Injects a javascript string into the referenced WebView. Deliberately does not
* return a response because using eval() to return a response breaks this method
* on pages with a Content Security Policy that disallows eval(). If you need that
* functionality, look into postMessage/onMessage.
*/
injectJavaScript = (data) => {
UIManager.dispatchViewManagerCommand(
this.getWebViewHandle(),
UIManager.RCTWebView.Commands.injectJavaScript,
[data]
);
};
/**
* We return an event with a bunch of fields including:
* url, title, loading, canGoBack, canGoForward
*/
updateNavigationState = (event) => {
if (this.props.onNavigationStateChange) {
this.props.onNavigationStateChange(event.nativeEvent);
}
};
getWebViewHandle = () => {
return ReactNative.findNodeHandle(this.refs[RCT_WEBVIEW_REF]);
};
onLoadingStart = (event) => {
var onLoadStart = this.props.onLoadStart;
onLoadStart && onLoadStart(event);
this.updateNavigationState(event);
};
onLoadingError = (event) => {
event.persist(); // persist this event because we need to store it
var {onError, onLoadEnd} = this.props;
onError && onError(event);
onLoadEnd && onLoadEnd(event);
console.warn('Encountered an error loading page', event.nativeEvent);
this.setState({
lastErrorEvent: event.nativeEvent,
viewState: WebViewState.ERROR
});
};
onLoadingFinish = (event) => {
var {onLoad, onLoadEnd} = this.props;
onLoad && onLoad(event);
onLoadEnd && onLoadEnd(event);
this.setState({
viewState: WebViewState.IDLE,
});
this.updateNavigationState(event);
};
onMessage = (event: Event) => {
var {onMessage} = this.props;
onMessage && onMessage(event);
}
}
var RCTWebView = requireNativeComponent('RCTWebView', WebView, WebView.extraNativeComponentConfig);
var styles = StyleSheet.create({
container: {
flex: 1,
},
hidden: {
height: 0,
flex: 0, // disable 'flex:1' when hiding a View
},
loadingView: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
loadingProgressBar: {
height: 20,
},
});
module.exports = WebView;
... ...