diff --git a/src/components/ScrollView/ScrollViewBase.js b/src/components/ScrollView/ScrollViewBase.js
new file mode 100644
index 00000000..46a976ff
--- /dev/null
+++ b/src/components/ScrollView/ScrollViewBase.js
@@ -0,0 +1,95 @@
+/**
+ * Copyright (c) 2016-present, Nicolas Gallagher.
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * @flow
+ */
+
+import debounce from 'lodash.debounce'
+import React, { Component, PropTypes } from 'react'
+import View from '../View'
+
+/**
+ * Encapsulates the Web-specific scroll throttling and disabling logic
+ */
+export default class ScrollViewBase extends Component {
+ static propTypes = {
+ ...View.propTypes,
+ onScroll: PropTypes.func,
+ onTouchMove: PropTypes.func,
+ onWheel: PropTypes.func,
+ scrollEnabled: PropTypes.bool,
+ scrollEventThrottle: PropTypes.number
+ };
+
+ static defaultProps = {
+ scrollEnabled: true
+ };
+
+ constructor(props) {
+ super(props)
+ this._debouncedOnScrollEnd = debounce(this._handleScrollEnd, 100)
+ this._handlePreventableScrollEvent = this._handlePreventableScrollEvent.bind(this)
+ this._handleScroll = this._handleScroll.bind(this)
+ this._state = { isScrolling: false }
+ }
+
+ _handlePreventableScrollEvent(handler) {
+ return (e) => {
+ if (!this.props.scrollEnabled) {
+ e.preventDefault()
+ } else {
+ if (handler) handler(e)
+ }
+ }
+ }
+
+ _handleScroll(e) {
+ const { scrollEventThrottle } = this.props
+ // A scroll happened, so the scroll bumps the debounce.
+ this._debouncedOnScrollEnd(e)
+ if (this._state.isScrolling) {
+ // Scroll last tick may have changed, check if we need to notify
+ if (this._shouldEmitScrollEvent(this._state.scrollLastTick, scrollEventThrottle)) {
+ this._handleScrollTick(e)
+ }
+ } else {
+ // Weren't scrolling, so we must have just started
+ this._handleScrollStart(e)
+ }
+ }
+
+ _handleScrollStart(e) {
+ this._state.isScrolling = true
+ this._state.scrollLastTick = Date.now()
+ }
+
+ _handleScrollTick(e) {
+ const { onScroll } = this.props
+ this._state.scrollLastTick = Date.now()
+ if (onScroll) onScroll(e)
+ }
+
+ _handleScrollEnd(e) {
+ const { onScroll } = this.props
+ this._state.isScrolling = false
+ if (onScroll) onScroll(e)
+ }
+
+ _shouldEmitScrollEvent(lastTick, eventThrottle) {
+ const timeSinceLastTick = Date.now() - lastTick
+ return (eventThrottle > 0 && timeSinceLastTick >= (1000 / eventThrottle))
+ }
+
+ render() {
+ return (
+
+ )
+ }
+}
diff --git a/src/components/ScrollView/index.js b/src/components/ScrollView/index.js
index 226c2eed..9091e2d7 100644
--- a/src/components/ScrollView/index.js
+++ b/src/components/ScrollView/index.js
@@ -1,130 +1,225 @@
-import { NativeMethodsDecorator } from '../../modules/NativeMethodsMixin'
-import debounce from 'lodash.debounce'
-import React, { Component, PropTypes } from 'react'
-import StyleSheet from '../../apis/StyleSheet'
-import View from '../View'
+/**
+ * Copyright (c) 2016-present, Nicolas Gallagher.
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * @flow
+ */
-@NativeMethodsDecorator
-export default class ScrollView extends Component {
- static propTypes = {
- children: PropTypes.any,
- contentContainerStyle: View.propTypes.style,
+import dismissKeyboard from '../../modules/dismissKeyboard'
+import invariant from 'fbjs/lib/invariant'
+import React, { Component, PropTypes } from 'react'
+import ReactDOM from 'react-dom'
+import ScrollResponder from '../../modules/ScrollResponder'
+import ScrollViewBase from './ScrollViewBase'
+import StyleSheet from '../../apis/StyleSheet'
+import StyleSheetPropType from '../../apis/StyleSheet/StyleSheetPropType'
+import View from '../View'
+import ViewStylePropTypes from '../View/ViewStylePropTypes'
+
+const INNERVIEW = 'InnerScrollView'
+const SCROLLVIEW = 'ScrollView'
+
+const ScrollView = React.createClass({
+ propTypes: {
+ ...View.propTypes,
+ children: View.propTypes.children,
+ contentContainerStyle: StyleSheetPropType(ViewStylePropTypes),
horizontal: PropTypes.bool,
+ keyboardDismissMode: PropTypes.oneOf([ 'none', 'interactive', 'on-drag' ]),
+ onContentSizeChange: PropTypes.func,
onScroll: PropTypes.func,
+ refreshControl: PropTypes.element,
scrollEnabled: PropTypes.bool,
scrollEventThrottle: PropTypes.number,
- style: View.propTypes.style
- };
+ style: StyleSheetPropType(ViewStylePropTypes)
+ },
- static defaultProps = {
- contentContainerStyle: {},
- horizontal: false,
- scrollEnabled: true,
- scrollEventThrottle: 0,
- style: {}
- };
+ mixins: [ScrollResponder.Mixin],
- constructor(...args) {
- super(...args)
- this._debouncedOnScrollEnd = debounce(this._onScrollEnd, 100)
- this.state = {
- isScrolling: false
- }
- }
+ getInitialState() {
+ return this.scrollResponderMixinGetInitialState()
+ },
- _onScroll(e) {
- const { scrollEventThrottle } = this.props
- const { isScrolling, scrollLastTick } = this.state
+ setNativeProps(props: Object) {
+ this.refs[SCROLLVIEW].setNativeProps(props)
+ },
- // A scroll happened, so the scroll bumps the debounce.
- this._debouncedOnScrollEnd(e)
+ /**
+ * 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(): Component {
+ return this
+ },
- if (isScrolling) {
- // Scroll last tick may have changed, check if we need to notify
- if (this._shouldEmitScrollEvent(scrollLastTick, scrollEventThrottle)) {
- this._onScrollTick(e)
- }
+ getScrollableNode(): any {
+ return ReactDOM.findDOMNode(this.refs[SCROLLVIEW])
+ },
+
+ getInnerViewNode(): any {
+ return ReactDOM.findDOMNode(this.refs[INNERVIEW])
+ },
+
+ /**
+ * 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 {
- // Weren't scrolling, so we must have just started
- this._onScrollStart(e)
+ ({x, y, animated} = y || {})
}
- }
- _onScrollStart() {
- this.setState({
- isScrolling: true,
- scrollLastTick: Date.now()
- })
- }
+ this.getScrollResponder().scrollResponderScrollTo({x: x || 0, y: y || 0, animated: animated !== false})
+ },
- _onScrollTick(e) {
- const { onScroll } = this.props
- this.setState({
- scrollLastTick: Date.now()
- })
- if (onScroll) onScroll(e)
- }
+ /**
+ * 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})
+ },
- _onScrollEnd(e) {
- const { onScroll } = this.props
- this.setState({
- isScrolling: false
- })
- if (onScroll) onScroll(e)
- }
+ handleScroll(e: Object) {
+ if (process.env.NODE_ENV !== 'production') {
+ 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.'
+ )
+ }
+ }
- _shouldEmitScrollEvent(lastTick, eventThrottle) {
- const timeSinceLastTick = Date.now() - lastTick
- return (eventThrottle > 0 && timeSinceLastTick >= (1000 / eventThrottle))
- }
+ if (this.props.keyboardDismissMode === 'on-drag') {
+ dismissKeyboard()
+ }
- _maybePreventScroll(e) {
- const { scrollEnabled } = this.props
- if (!scrollEnabled) e.preventDefault()
- }
+ this.scrollResponderHandleScroll(e)
+ },
+
+ _handleContentOnLayout(e: Object) {
+ const { width, height } = e.nativeEvent.layout
+ this.props.onContentSizeChange && this.props.onContentSizeChange(width, height)
+ },
render() {
- const {
- children,
- contentContainerStyle,
- horizontal,
- style
- } = this.props
+ const scrollViewStyle = [
+ styles.base,
+ this.props.horizontal && styles.baseHorizontal
+ ]
+
+ const contentContainerStyle = [
+ styles.contentContainer,
+ this.props.horizontal && styles.contentContainerHorizontal,
+ this.props.contentContainerStyle
+ ]
+
+ 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 (this.props.onContentSizeChange) {
+ contentSizeChangeProps = {
+ onLayout: this._handleContentOnLayout
+ }
+ }
+
+ const contentContainer = (
+
+ )
+
+ const props = {
+ ...this.props,
+ style: [scrollViewStyle, 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'
+ )
+
+ var refreshControl = this.props.refreshControl
+ if (refreshControl) {
+ return React.cloneElement(
+ refreshControl,
+ { style: props.style },
+
+ {contentContainer}
+
+ )
+ }
return (
- this._onScroll(e)}
- onTouchMove={(e) => this._maybePreventScroll(e)}
- onWheel={(e) => this._maybePreventScroll(e)}
- style={[
- styles.initial,
- style
- ]}
- >
- {children ? (
-
- ) : null}
-
+
+ {contentContainer}
+
)
}
-}
+})
const styles = StyleSheet.create({
- initial: {
+ base: {
flex: 1,
- overflow: 'auto'
+ overflowX: 'hidden',
+ overflowY: 'auto'
},
- initialContentContainer: {
+ baseHorizontal: {
+ overflowX: 'auto',
+ overflowY: 'hidden'
+ },
+ contentContainer: {
flex: 1
},
- row: {
+ contentContainerHorizontal: {
flexDirection: 'row'
}
})