Skip to content

Instantly share code, notes, and snippets.

@brentvatne
Created July 20, 2015 19:24
Show Gist options
  • Save brentvatne/db1cf9fa16f7fbc514cb to your computer and use it in GitHub Desktop.
Save brentvatne/db1cf9fa16f7fbc514cb to your computer and use it in GitHub Desktop.
/* @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