Created
July 20, 2015 19:24
-
-
Save brentvatne/db1cf9fa16f7fbc514cb to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* @flow */ | |
import React from 'react-native'; | |
import ReactInstanceMap from 'ReactInstanceMap'; | |
import { Animated, Component, ScrollView, PropTypes, TextInput, NativeModules, Dimensions, } from 'react-native'; | |
import KeyboardEvents from 'react-native-keyboardevents'; | |
var { UIManager, } = NativeModules; | |
var FOCUS_AUTO = 'auto'; | |
var FOCUS_TOP = 'top'; | |
var FOCUS_CENTER = 'center'; | |
var FOCUS_NONE = 'none'; | |
var UNFOCUS_RETURN = 'return'; | |
var UNFOCUS_NONE = 'none'; | |
// TODO: This is an arbirtrary number to space out the | |
// view from the top of the screen, an offset should be exposed as | |
// a prop | |
var ARBITRARY_TOP_FOCUS_MARGIN = 15; | |
var ARBITRARY_AUTO_TOP_THRESHOLD = 100; | |
var InteractiveScrollView = React.createClass({ | |
scrollView: {}, | |
/* Not sure why this is producing a flow error :/ */ | |
focusedNode: null, | |
unfocusedScrollPosition: 0, | |
getChildContext():Object { | |
return { | |
focusNode: this.focusNode.bind(this), | |
unfocusNode: this.unfocusNode.bind(this), | |
} | |
}, | |
childContextTypes: { | |
focusNode: React.PropTypes.func, | |
unfocusNode: React.PropTypes.func, | |
}, | |
getInitialState():Object { | |
return { | |
/* TODO: Detect if keyboard is already open? Is this necessary? */ | |
keyboardHeight: 0, | |
viewportHeight: 0, | |
} | |
}, | |
componentWillMount() { | |
KeyboardEvents.Emitter.on(KeyboardEvents.KeyboardWillShowEvent, (frames) => { | |
this.setState({keyboardHeight: frames.end.height}); | |
}); | |
KeyboardEvents.Emitter.on(KeyboardEvents.KeyboardWillHideEvent, (frames) => { | |
this.setState({keyboardHeight: 0}); | |
}); | |
}, | |
availableHeight():number { | |
return this.state.viewportHeight - this.state.keyboardHeight; | |
}, | |
scrollToCenterForMeasurements(leftOffset:number, topOffset:number, width:number, height:number) { | |
/* requestAnimationFrame so we wait until keyboard offset has been set */ | |
requestAnimationFrame(() => { | |
var centerScrollPosition = | |
topOffset - (this.availableHeight() / 2) + height / 2 | |
- ARBITRARY_TOP_FOCUS_MARGIN; // TODO: Poor substitute for taking status bar height into focus | |
var MAX_SCROLL = Infinity; // TODO: Replace MAX_SCROLL with something more reasonable, eg: the height of the ScrollView | |
var MIN_SCROLL = 0; | |
/* TODO: replace this with the clamp function */ | |
if (centerScrollPosition < MIN_SCROLL) { | |
centerScrollPosition = MIN_SCROLL; | |
} else if (centerScrollPosition > MAX_SCROLL) { | |
centerScrollPosition = MAX_SCROLL; | |
} | |
this.scrollView.scrollTo(centerScrollPosition, 0); | |
}); | |
}, | |
scrollToTopForMeasurements(leftOffset:number, topOffset:number, width:number, height:number) { | |
requestAnimationFrame(() => { | |
var topScrollPosition = topOffset - ARBITRARY_TOP_FOCUS_MARGIN; | |
this.scrollView.scrollTo(topScrollPosition, 0); // VERIFY: Assume x doesn't scroll? | |
}); | |
}, | |
focusNode(node:any, focus:string) { | |
var targetHandle = React.findNodeHandle(node); | |
var scrollViewHandle = React.findNodeHandle(this.scrollView); | |
this.focusedNode = node; | |
if (focus === FOCUS_AUTO) { | |
var successCallback = (leftOffset, topOffset, width, height) => { | |
if (height >= (this.availableHeight() - ARBITRARY_AUTO_TOP_THRESHOLD)) { | |
this.scrollToTopForMeasurements(leftOffset, topOffset, width, height); | |
} else { | |
this.scrollToCenterForMeasurements(leftOffset, topOffset, width, height); | |
} | |
} | |
} else if (focus === FOCUS_CENTER) { | |
var successCallback = this.scrollToCenterForMeasurements; | |
} else if (focus === FOCUS_TOP) { | |
var successCallback = this.scrollToTopForMeasurements; | |
} | |
UIManager.measureLayout( | |
targetHandle, | |
scrollViewHandle, | |
() => { /* handle error */ }, | |
successCallback, | |
); | |
}, | |
unfocusNode(node:any, scrollUnfocus:string) { | |
this.focusedNode = null; | |
if (scrollUnfocus === UNFOCUS_RETURN) { | |
this.scrollView.scrollTo(this.unfocusedScrollPosition); | |
} | |
}, | |
_onLayout(e:any) { | |
var { y, height, } = e.nativeEvent.layout; | |
// Doesn't make sense to consider y values less than 0 here, I think | |
if (y < 0) { y = 0; } | |
this.setState({viewportHeight: height - y}); | |
}, | |
_onScroll(e:any) { | |
var { contentOffset, } = e.nativeEvent; | |
if (!this.focusedNode) { | |
this.unfocusedScrollPosition = contentOffset.y; | |
} | |
}, | |
render() { | |
return ( | |
<ScrollView {...this.props} ref={(view) => this.scrollView = view} | |
onLayout={this._onLayout} | |
scrollEventThrottle={20} | |
onScroll={this._onScroll}> | |
{this.props.children} | |
</ScrollView> | |
) | |
} | |
}); | |
InteractiveScrollView.TextInput = React.createClass({ | |
getDefaultProps() { | |
return { | |
scrollFocus: FOCUS_AUTO, | |
scrollUnfocus: UNFOCUS_NONE, | |
} | |
}, | |
propTypes: { | |
scrollFocus: PropTypes.oneOf([ | |
FOCUS_AUTO, FOCUS_TOP, FOCUS_CENTER, FOCUS_NONE, | |
]), | |
scrollUnfocus: PropTypes.oneOf([ | |
UNFOCUS_RETURN, UNFOCUS_NONE, | |
]) | |
}, | |
contextTypes: { | |
focusNode: PropTypes.func.isRequired, | |
unfocusNode: PropTypes.func.isRequired, | |
}, | |
_onFocus() { | |
// Workaround for this.context until that works in 0.13 | |
var context = ReactInstanceMap.get(this)._context; | |
var { scrollFocus, } = this.props; | |
if ([FOCUS_AUTO, FOCUS_CENTER, FOCUS_TOP].indexOf(scrollFocus) >= 0) { | |
context.focusNode(this, scrollFocus); | |
} else { | |
if (scrollFocus !== FOCUS_NONE) { | |
console.warn( | |
`You set scrollFocus to ${scrollFocus}, which is not a permitted value` | |
); | |
} | |
} | |
}, | |
_onBlur() { | |
// Workaround for this.context until that works in 0.13 | |
var context = ReactInstanceMap.get(this)._context; | |
context.unfocusNode(this, this.props.scrollUnfocus); | |
}, | |
render() { | |
/* TODO: Wrap this._onFocus and onBlur so I don't clobber those passed in */ | |
return <TextInput {...this.props} onFocus={this._onFocus} onBlur={this._onBlur} /> | |
} | |
}); | |
module.exports = InteractiveScrollView; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment