react-native/Libraries/Components/ScrollView/ScrollView.js

475 lines
17 KiB
JavaScript

/**
* 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 ScrollView
* @flow
*/
'use strict';
var EdgeInsetsPropType = require('EdgeInsetsPropType');
var Platform = require('Platform');
var PointPropType = require('PointPropType');
var RCTScrollView = require('NativeModules').UIManager.RCTScrollView;
var React = require('React');
var ReactNativeViewAttributes = require('ReactNativeViewAttributes');
var RCTUIManager = require('NativeModules').UIManager;
var ScrollResponder = require('ScrollResponder');
var StyleSheet = require('StyleSheet');
var StyleSheetPropType = require('StyleSheetPropType');
var View = require('View');
var ViewStylePropTypes = require('ViewStylePropTypes');
var createReactNativeComponentClass = require('createReactNativeComponentClass');
var deepDiffer = require('deepDiffer');
var dismissKeyboard = require('dismissKeyboard');
var flattenStyle = require('flattenStyle');
var insetsDiffer = require('insetsDiffer');
var invariant = require('invariant');
var pointsDiffer = require('pointsDiffer');
var requireNativeComponent = require('requireNativeComponent');
var PropTypes = React.PropTypes;
var SCROLLVIEW = 'ScrollView';
var INNERVIEW = 'InnerScrollView';
/**
* Component that wraps platform ScrollView while providing
* integration with touch locking "responder" system.
*
* Keep in mind that ScrollViews must have a bounded height in order to work,
* since they contain unbounded-height children into a bounded container (via
* a scroll interaction). In order to bound the height of a ScrollView, either
* set the height of the view directly (discouraged) or make sure all parent
* views have bounded height. Forgetting to transfer `{flex: 1}` down the
* view stack can lead to errors here, which the element inspector makes
* easy to debug.
*
* Doesn't yet support other contained responders from blocking this scroll
* view from becoming the responder.
*/
var ScrollView = React.createClass({
propTypes: {
/**
* Controls whether iOS should automatically adjust the content inset
* for scroll views that are placed behind a navigation bar or
* tab bar/ toolbar. The default value is true.
* @platform ios
*/
automaticallyAdjustContentInsets: PropTypes.bool,
/**
* The amount by which the scroll view content is inset from the edges
* of the scroll view. Defaults to `{0, 0, 0, 0}`.
* @platform ios
*/
contentInset: EdgeInsetsPropType,
/**
* Used to manually set the starting scroll offset.
* The default value is `{x: 0, y: 0}`.
* @platform ios
*/
contentOffset: PointPropType,
/**
* When true, the scroll view bounces when it reaches the end of the
* content if the content is larger then the scroll view along the axis of
* the scroll direction. When false, it disables all bouncing even if
* the `alwaysBounce*` props are true. The default value is true.
* @platform ios
*/
bounces: PropTypes.bool,
/**
* When true, gestures can drive zoom past min/max and the zoom will animate
* to the min/max value at gesture end, otherwise the zoom will not exceed
* the limits.
* @platform ios
*/
bouncesZoom: PropTypes.bool,
/**
* When true, the scroll view bounces horizontally when it reaches the end
* even if the content is smaller than the scroll view itself. The default
* value is true when `horizontal={true}` and false otherwise.
* @platform ios
*/
alwaysBounceHorizontal: PropTypes.bool,
/**
* When true, the scroll view bounces vertically when it reaches the end
* even if the content is smaller than the scroll view itself. The default
* value is false when `horizontal={true}` and true otherwise.
* @platform ios
*/
alwaysBounceVertical: PropTypes.bool,
/**
* When true, the scroll view automatically centers the content when the
* content is smaller than the scroll view bounds; when the content is
* larger than the scroll view, this property has no effect. The default
* value is false.
* @platform ios
*/
centerContent: PropTypes.bool,
/**
* These styles will be applied to the scroll view content container which
* wraps all of the child views. Example:
*
* return (
* <ScrollView contentContainerStyle={styles.contentContainer}>
* </ScrollView>
* );
* ...
* var styles = StyleSheet.create({
* contentContainer: {
* paddingVertical: 20
* }
* });
*/
contentContainerStyle: StyleSheetPropType(ViewStylePropTypes),
/**
* A floating-point number that determines how quickly the scroll view
* decelerates after the user lifts their finger. Reasonable choices include
* - Normal: 0.998 (the default)
* - Fast: 0.9
* @platform ios
*/
decelerationRate: PropTypes.number,
/**
* When true, the scroll view's children are arranged horizontally in a row
* instead of vertically in a column. The default value is false.
*/
horizontal: PropTypes.bool,
/**
* When true, the ScrollView will try to lock to only vertical or horizontal
* scrolling while dragging. The default value is false.
* @platform ios
*/
directionalLockEnabled: PropTypes.bool,
/**
* When false, once tracking starts, won't try to drag if the touch moves.
* The default value is true.
* @platform ios
*/
canCancelContentTouches: PropTypes.bool,
/**
* Determines whether the keyboard gets dismissed in response to a drag.
* - 'none' (the default), drags do not dismiss the keyboard.
* - 'on-drag', the keyboard is dismissed when a drag begins.
* - 'interactive', the keyboard is dismissed interactively with the drag and moves in
* synchrony with the touch; dragging upwards cancels the dismissal.
* On android this is not supported and it will have the same behavior as 'none'.
*/
keyboardDismissMode: PropTypes.oneOf([
'none', // default
'interactive',
'on-drag',
]),
/**
* When false, tapping outside of the focused text input when the keyboard
* is up dismisses the keyboard. When true, the scroll view will not catch
* taps, and the keyboard will not dismiss automatically. The default value
* is false.
*/
keyboardShouldPersistTaps: PropTypes.bool,
/**
* The maximum allowed zoom scale. The default value is 1.0.
* @platform ios
*/
maximumZoomScale: PropTypes.number,
/**
* The minimum allowed zoom scale. The default value is 1.0.
* @platform ios
*/
minimumZoomScale: PropTypes.number,
/**
* Fires at most once per frame during scrolling. The frequency of the
* events can be contolled using the `scrollEventThrottle` prop.
*/
onScroll: PropTypes.func,
/**
* Called when a scrolling animation ends.
* @platform ios
*/
onScrollAnimationEnd: PropTypes.func,
/**
* When true, the scroll view stops on multiples of the scroll view's size
* when scrolling. This can be used for horizontal pagination. The default
* value is false.
* @platform ios
*/
pagingEnabled: PropTypes.bool,
/**
* When false, the content does not scroll.
* The default value is true.
* @platform ios
*/
scrollEnabled: PropTypes.bool,
/**
* This controls how often the scroll event will be fired while scrolling
* (in events per seconds). A higher number yields better accuracy for code
* that is tracking the scroll position, but can lead to scroll performance
* problems due to the volume of information being send over the bridge.
* The default value is zero, which means the scroll event will be sent
* only once each time the view is scrolled.
* @platform ios
*/
scrollEventThrottle: PropTypes.number,
/**
* The amount by which the scroll view indicators are inset from the edges
* of the scroll view. This should normally be set to the same value as
* the `contentInset`. Defaults to `{0, 0, 0, 0}`.
* @platform ios
*/
scrollIndicatorInsets: EdgeInsetsPropType,
/**
* When true, the scroll view scrolls to top when the status bar is tapped.
* The default value is true.
* @platform ios
*/
scrollsToTop: PropTypes.bool,
/**
* When true, shows a horizontal scroll indicator.
*/
showsHorizontalScrollIndicator: PropTypes.bool,
/**
* When true, shows a vertical scroll indicator.
*/
showsVerticalScrollIndicator: PropTypes.bool,
/**
* An array of child indices determining which children get docked to the
* top of the screen when scrolling. For example, passing
* `stickyHeaderIndices={[0]}` will cause the first child to be fixed to the
* top of the scroll view. This property is not supported in conjunction
* with `horizontal={true}`.
* @platform ios
*/
stickyHeaderIndices: PropTypes.arrayOf(PropTypes.number),
style: StyleSheetPropType(ViewStylePropTypes),
/**
* When set, causes the scroll view to stop at multiples of the value of
* `snapToInterval`. This can be used for paginating through children
* that have lengths smaller than the scroll view. Used in combination
* with `snapToAlignment`.
* @platform ios
*/
snapToInterval: PropTypes.number,
/**
* When `snapToInterval` is set, `snapToAlignment` will define the relationship
* of the the snapping to the scroll view.
* - `start` (the default) will align the snap at the left (horizontal) or top (vertical)
* - `center` will align the snap in the center
* - `end` will align the snap at the right (horizontal) or bottom (vertical)
* @platform ios
*/
snapToAlignment: PropTypes.oneOf([
'start', // default
'center',
'end',
]),
/**
* Experimental: When true, offscreen child views (whose `overflow` value is
* `hidden`) are removed from their native backing superview when offscreen.
* This can improve scrolling performance on long lists. The default value is
* false.
*/
removeClippedSubviews: PropTypes.bool,
/**
* The current scale of the scroll view content. The default value is 1.0.
* @platform ios
*/
zoomScale: PropTypes.number,
},
mixins: [ScrollResponder.Mixin],
getInitialState: function() {
return this.scrollResponderMixinGetInitialState();
},
setNativeProps: function(props: Object) {
this.refs[SCROLLVIEW].setNativeProps(props);
},
/**
* Returns a reference to the underlying scroll responder, which supports
* operations like `scrollTo`. All ScrollView-like components should
* implement this method so that they can be composed while providing access
* to the underlying scroll responder's methods.
*/
getScrollResponder: function(): ReactComponent {
return this;
},
getInnerViewNode: function(): any {
return React.findNodeHandle(this.refs[INNERVIEW]);
},
scrollTo: function(destY?: number, destX?: number) {
// $FlowFixMe - Don't know how to pass Mixin correctly. Postpone for now
this.getScrollResponder().scrollResponderScrollTo(destX || 0, destY || 0);
},
scrollWithoutAnimationTo: function(destY?: number, destX?: number) {
// $FlowFixMe - Don't know how to pass Mixin correctly. Postpone for now
this.getScrollResponder().scrollResponderScrollWithouthAnimationTo(
destX || 0,
destY || 0,
);
},
handleScroll: function(e: Event) {
if (__DEV__) {
if (this.props.onScroll && !this.props.scrollEventThrottle) {
console.log(
'You specified `onScroll` on a <ScrollView> but not ' +
'`scrollEventThrottle`. You will only receive one event. ' +
'Using `16` you get all the events but be aware that it may ' +
'cause frame drops, use a bigger number if you don\'t need as ' +
'much precision.'
);
}
}
if (Platform.OS === 'android') {
if (this.props.keyboardDismissMode === 'on-drag') {
dismissKeyboard();
}
}
this.scrollResponderHandleScroll(e);
},
render: function() {
var contentContainerStyle = [
this.props.horizontal && styles.contentContainerHorizontal,
this.props.contentContainerStyle,
];
if (__DEV__ && this.props.style) {
var style = flattenStyle(this.props.style);
var childLayoutProps = ['alignItems', 'justifyContent']
.filter((prop) => style && style[prop] !== undefined);
invariant(
childLayoutProps.length === 0,
'ScrollView child layout (' + JSON.stringify(childLayoutProps) +
') must by applied through the contentContainerStyle prop.'
);
}
var contentContainer =
<View
ref={INNERVIEW}
style={contentContainerStyle}
removeClippedSubviews={this.props.removeClippedSubviews}
collapsable={false}>
{this.props.children}
</View>;
var alwaysBounceHorizontal =
this.props.alwaysBounceHorizontal !== undefined ?
this.props.alwaysBounceHorizontal :
this.props.horizontal;
var alwaysBounceVertical =
this.props.alwaysBounceVertical !== undefined ?
this.props.alwaysBounceVertical :
!this.props.horizontal;
var props = {
...this.props,
alwaysBounceHorizontal,
alwaysBounceVertical,
style: ([styles.base, this.props.style]: ?Array<any>),
onTouchStart: this.scrollResponderHandleTouchStart,
onTouchMove: this.scrollResponderHandleTouchMove,
onTouchEnd: this.scrollResponderHandleTouchEnd,
onScrollBeginDrag: this.scrollResponderHandleScrollBeginDrag,
onScrollEndDrag: this.scrollResponderHandleScrollEndDrag,
onMomentumScrollBegin: this.scrollResponderHandleMomentumScrollBegin,
onMomentumScrollEnd: this.scrollResponderHandleMomentumScrollEnd,
onStartShouldSetResponder: this.scrollResponderHandleStartShouldSetResponder,
onStartShouldSetResponderCapture: this.scrollResponderHandleStartShouldSetResponderCapture,
onScrollShouldSetResponder: this.scrollResponderHandleScrollShouldSetResponder,
onScroll: this.handleScroll,
onResponderGrant: this.scrollResponderHandleResponderGrant,
onResponderTerminationRequest: this.scrollResponderHandleTerminationRequest,
onResponderTerminate: this.scrollResponderHandleTerminate,
onResponderRelease: this.scrollResponderHandleResponderRelease,
onResponderReject: this.scrollResponderHandleResponderReject,
};
var ScrollViewClass;
if (Platform.OS === 'ios') {
ScrollViewClass = RCTScrollView;
} else if (Platform.OS === 'android') {
if (this.props.horizontal) {
ScrollViewClass = AndroidHorizontalScrollView;
} else {
ScrollViewClass = AndroidScrollView;
}
}
invariant(
ScrollViewClass !== undefined,
'ScrollViewClass must not be undefined'
);
return (
<ScrollViewClass {...props} ref={SCROLLVIEW}>
{contentContainer}
</ScrollViewClass>
);
}
});
var styles = StyleSheet.create({
base: {
flex: 1,
},
contentContainerHorizontal: {
alignSelf: 'flex-start',
flexDirection: 'row',
},
});
var validAttributes = {
...ReactNativeViewAttributes.UIView,
alwaysBounceHorizontal: true,
alwaysBounceVertical: true,
automaticallyAdjustContentInsets: true,
bounces: true,
centerContent: true,
contentInset: {diff: insetsDiffer},
contentOffset: {diff: pointsDiffer},
decelerationRate: true,
horizontal: true,
keyboardDismissMode: true,
keyboardShouldPersistTaps: true,
maximumZoomScale: true,
minimumZoomScale: true,
pagingEnabled: true,
removeClippedSubviews: true,
scrollEnabled: true,
scrollIndicatorInsets: {diff: insetsDiffer},
scrollsToTop: true,
showsHorizontalScrollIndicator: true,
showsVerticalScrollIndicator: true,
snapToInterval: true,
snapToAlignment: true,
stickyHeaderIndices: {diff: deepDiffer},
scrollEventThrottle: true,
zoomScale: true,
};
if (Platform.OS === 'android') {
var AndroidScrollView = createReactNativeComponentClass({
validAttributes: validAttributes,
uiViewClassName: 'RCTScrollView',
});
var AndroidHorizontalScrollView = createReactNativeComponentClass({
validAttributes: validAttributes,
uiViewClassName: 'AndroidHorizontalScrollView',
});
} else if (Platform.OS === 'ios') {
var RCTScrollView = requireNativeComponent('RCTScrollView', ScrollView);
}
module.exports = ScrollView;