[fix] ScrollView: based on ScrollResponder

This commit is contained in:
Nicolas Gallagher
2016-03-15 14:07:45 -07:00
parent 8d5ecb84d5
commit 190966f411
2 changed files with 288 additions and 98 deletions
@@ -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 (
<View
{...this.props}
onScroll={this._handleScroll}
onTouchMove={this._handlePreventableScrollEvent(this.props.onTouchMove)}
onWheel={this._handlePreventableScrollEvent(this.props.onWheel)}
/>
)
}
}
+193 -98
View File
@@ -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 <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.'
)
}
}
_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 = (
<View
{...contentSizeChangeProps}
children={this.props.children}
collapsable={false}
ref={INNERVIEW}
style={contentContainerStyle}
/>
)
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 },
<ScrollViewClass {...props} ref={SCROLLVIEW} style={styles.base}>
{contentContainer}
</ScrollViewClass>
)
}
return (
<View
onScroll={(e) => this._onScroll(e)}
onTouchMove={(e) => this._maybePreventScroll(e)}
onWheel={(e) => this._maybePreventScroll(e)}
style={[
styles.initial,
style
]}
>
{children ? (
<View
children={children}
style={[
styles.initialContentContainer,
contentContainerStyle,
horizontal && styles.row
]}
/>
) : null}
</View>
<ScrollViewClass {...props} ref={SCROLLVIEW} style={props.style}>
{contentContainer}
</ScrollViewClass>
)
}
}
})
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'
}
})