/** * 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 RCTScrollViewManager = require('NativeModules').ScrollViewManager; 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 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: { ...View.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 ( * * * ); * ... * 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, /** * When defined, displays a UIRefreshControl. * Invoked with a function to stop refreshing when the UIRefreshControl is animating. * * ``` * (endRefreshing) => { * endRefreshing(); * } * ``` * * @platform ios */ onRefreshStart: PropTypes.func, }, mixins: [ScrollResponder.Mixin], getInitialState: function() { return this.scrollResponderMixinGetInitialState(); }, setNativeProps: function(props: Object) { this.refs[SCROLLVIEW].setNativeProps(props); }, endRefreshing: function() { RCTScrollViewManager.endRefreshing( React.findNodeHandle(this) ); }, /** * 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 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 = {this.props.children} ; 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), 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 onRefreshStart = this.props.onRefreshStart; // this is necessary because if we set it on props, even when empty, // it'll trigger the default pull-to-refresh behaviour on native. props.onRefreshStart = onRefreshStart ? function() { onRefreshStart && onRefreshStart(this.endRefreshing); }.bind(this) : null; 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 ( {contentContainer} ); } }); 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 = requireNativeComponent('RCTScrollView', ScrollView); var AndroidHorizontalScrollView = requireNativeComponent( 'AndroidHorizontalScrollView', ScrollView ); } else if (Platform.OS === 'ios') { var RCTScrollView = requireNativeComponent('RCTScrollView', ScrollView); } module.exports = ScrollView;