/** * Copyright (c) Nicolas Gallagher. * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @noflow */ import createReactClass from 'create-react-class'; import dismissKeyboard from '../../modules/dismissKeyboard'; import findNodeHandle from '../findNodeHandle'; import invariant from 'fbjs/lib/invariant'; import ScrollResponder from '../../modules/ScrollResponder'; import ScrollViewBase from './ScrollViewBase'; import StyleSheet from '../StyleSheet'; import View from '../View'; import ViewPropTypes from '../ViewPropTypes'; import React from 'react'; import { arrayOf, bool, element, func, number, oneOf } from 'prop-types'; const emptyObject = {}; /* eslint-disable react/prefer-es6-class, react/prop-types */ const ScrollView = createReactClass({ propTypes: { ...ViewPropTypes, contentContainerStyle: ViewPropTypes.style, horizontal: bool, keyboardDismissMode: oneOf(['none', 'interactive', 'on-drag']), onContentSizeChange: func, onScroll: func, pagingEnabled: bool, refreshControl: element, scrollEnabled: bool, scrollEventThrottle: number, stickyHeaderIndices: arrayOf(number), style: ViewPropTypes.style }, mixins: [ScrollResponder.Mixin], getInitialState() { return this.scrollResponderMixinGetInitialState(); }, flashScrollIndicators() { this.scrollResponderFlashScrollIndicators(); }, setNativeProps(props: Object) { if (this._scrollViewRef) { this._scrollViewRef.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(): ScrollView { return this; }, getScrollableNode(): any { return findNodeHandle(this._scrollViewRef); }, getInnerViewNode(): any { return findNodeHandle(this._innerViewRef); }, /** * Scrolls to a given x, y offset, either immediately or with a smooth animation. * Syntax: * * scrollTo(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. */ 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.' ); } else { ({ x, y, animated } = y || emptyObject); } this.getScrollResponder().scrollResponderScrollTo({ x: x || 0, y: y || 0, animated: animated !== false }); }, /** * If this is a vertical ScrollView scrolls to the bottom. * If this is a horizontal ScrollView scrolls to the right. * * Use `scrollToEnd({ animated: true })` for smooth animated scrolling, * `scrollToEnd({ animated: false })` for immediate scrolling. * If no options are passed, `animated` defaults to true. */ 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 x = horizontal ? scrollResponderNode.scrollWidth : 0; const y = horizontal ? 0 : scrollResponderNode.scrollHeight; scrollResponder.scrollResponderScrollTo({ x, y, animated }); }, /** * Deprecated, do not use. */ scrollWithoutAnimationTo(y: number = 0, x: number = 0) { console.warn('`scrollWithoutAnimationTo` is deprecated. Use `scrollTo` instead'); this.scrollTo({ x, y, animated: false }); }, render() { const { contentContainerStyle, horizontal, onContentSizeChange, refreshControl, stickyHeaderIndices, pagingEnabled, /* eslint-disable */ keyboardDismissMode, onScroll, /* eslint-enable */ ...other } = this.props; if (process.env.NODE_ENV !== 'production' && this.props.style) { const style = StyleSheet.flatten(this.props.style); const childLayoutProps = ['alignItems', 'justifyContent'].filter( prop => style && style[prop] !== undefined ); invariant( childLayoutProps.length === 0, `ScrollView child layout (${JSON.stringify(childLayoutProps)}) ` + 'must be applied through the contentContainerStyle prop.' ); } let contentSizeChangeProps = {}; if (onContentSizeChange) { contentSizeChangeProps = { onLayout: this._handleContentOnLayout }; } const hasStickyHeaderIndices = !horizontal && Array.isArray(stickyHeaderIndices); const children = hasStickyHeaderIndices || pagingEnabled ? React.Children.map(this.props.children, (child, i) => { const isSticky = hasStickyHeaderIndices && stickyHeaderIndices.indexOf(i) > -1; if (child != null && (isSticky || pagingEnabled)) { return ( <View style={StyleSheet.compose( isSticky && styles.stickyHeader, pagingEnabled && styles.pagingEnabledChild )} > {child} </View> ); } else { return child; } }) : this.props.children; const contentContainer = ( <View {...contentSizeChangeProps} children={children} collapsable={false} ref={this._setInnerViewRef} style={StyleSheet.compose( horizontal && styles.contentContainerHorizontal, contentContainerStyle )} /> ); const baseStyle = horizontal ? styles.baseHorizontal : styles.baseVertical; const pagingEnabledStyle = horizontal ? styles.pagingEnabledHorizontal : styles.pagingEnabledVertical; const props = { ...other, style: [baseStyle, pagingEnabled && pagingEnabledStyle, this.props.style], 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 }; const ScrollViewClass = ScrollViewBase; invariant(ScrollViewClass !== undefined, 'ScrollViewClass must not be undefined'); if (refreshControl) { return React.cloneElement( refreshControl, { style: props.style }, <ScrollViewClass {...props} ref={this._setScrollViewRef} style={baseStyle}> {contentContainer} </ScrollViewClass> ); } return ( <ScrollViewClass {...props} ref={this._setScrollViewRef}> {contentContainer} </ScrollViewClass> ); }, _handleContentOnLayout(e: Object) { const { width, height } = e.nativeEvent.layout; this.props.onContentSizeChange(width, height); }, _handleScroll(e: Object) { if (process.env.NODE_ENV !== 'production') { 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 (this.props.keyboardDismissMode === 'on-drag') { dismissKeyboard(); } this.scrollResponderHandleScroll(e); }, _setInnerViewRef(component) { this._innerViewRef = component; }, _setScrollViewRef(component) { this._scrollViewRef = component; } }); const commonStyle = { flexGrow: 1, flexShrink: 1, // Enable hardware compositing in modern browsers. // Creates a new layer with its own backing surface that can significantly // improve scroll performance. transform: [{ translateZ: 0 }], // iOS native scrolling WebkitOverflowScrolling: 'touch' }; const styles = StyleSheet.create({ baseVertical: { ...commonStyle, flexDirection: 'column', overflowX: 'hidden', overflowY: 'auto' }, baseHorizontal: { ...commonStyle, flexDirection: 'row', overflowX: 'auto', overflowY: 'hidden' }, contentContainerHorizontal: { flexDirection: 'row' }, stickyHeader: { position: 'sticky', top: 0, zIndex: 10 }, pagingEnabledHorizontal: { scrollSnapType: 'x mandatory' }, pagingEnabledVertical: { scrollSnapType: 'y mandatory' }, pagingEnabledChild: { scrollSnapAlign: 'start' } }); export default ScrollView;