UNPKG

react-native-smart-scroll-view

Version:

Handles keyboard events and auto adjusts content to be visible above keyboard on focus. Further scrolling features available.

296 lines (255 loc) 8.84 kB
import React, { Component, PropTypes, } from 'react'; import ReactNative, { View, StyleSheet, ScrollView, Keyboard, Dimensions, LayoutAnimation, Platform } from 'react-native'; const { height: screenHeight } = Dimensions.get('window'); const animations = { layout: { easeInEaseOut: { duration: 250, create: { type: LayoutAnimation.Types.easeInEaseOut, property: LayoutAnimation.Properties.scaleXY }, update: { delay: 0, type: LayoutAnimation.Types.easeInEaseOut } } } }; class SmartScrollView extends Component { constructor(){ super(); this.state = { scrollPosition : 0, } this._refCreator = this._refCreator.bind(this); this._focusNode = this._focusNode.bind(this); this._keyboardWillHide = this._keyboardWillHide.bind(this); this._keyboardWillShow = this._keyboardWillShow.bind(this); this._updateScrollPosition = this._updateScrollPosition.bind(this); } componentDidMount() { if (this.props.forceFocusField !== this.state.focusedField){ this._focusField('input_' + this.props.forceFocusField) } this._listeners = [ Keyboard.addListener(Platform.OS == 'IOS' ? 'keyboardWillShow' : 'keyboardDidShow', this._keyboardWillShow), Keyboard.addListener(Platform.OS == 'IOS' ? 'keyboardWillHide' : 'keyboardDidHide', this._keyboardWillHide), ]; } componentWillUpdate(props, state) { if (state.keyboardUp !== this.state.keyboardUp) { LayoutAnimation.configureNext(animations.layout.easeInEaseOut) } } componentWillReceiveProps(props) { if (props.forceFocusField !== undefined && props.forceFocusField !== this.state.focusedField){ this._focusField('input_' + props.forceFocusField) } } componentWillUnmount() { this._listeners.forEach((listener) => listener.remove()); } _findScrollWindowHeight(keyboardHeight){ const {x, y, width, height} = this._layout const spaceBelow = screenHeight - y - height; return height - Math.max(keyboardHeight - spaceBelow, 0); } _keyboardWillShow(e) { const scrollWindowHeight = this._findScrollWindowHeight(e.endCoordinates.height) this.setState({ scrollWindowHeight, keyBoardUp: true }) } _keyboardWillHide() { this.setState({ keyBoardUp: false }); this._smartScroll && this._smartScroll.scrollTo({y: 0}); } _refCreator () { const refs = arguments; return component => Object.keys(refs).forEach(i => this[refs[i]] = component); } _focusField (ref) { const node = this[ref]; const {type} = node.props.smartScrollOptions; switch(type) { case 'text': this[ref].focus(); break; case 'custom': this._focusNode(ref); } } _focusNode (ref) { const { scrollPosition, scrollWindowHeight, } = this.state; const { scrollPadding, onRefFocus } = this.props; const num = ReactNative.findNodeHandle(this._smartScroll); const strippedBackRef = ref.slice('input_'.length); setTimeout(() => { onRefFocus(strippedBackRef); this.setState({focusedField: strippedBackRef}) this[ref].measureLayout(num, (X,Y,W,H) => { const py = Y - scrollPosition; if ( py + H > scrollWindowHeight ){ const nextScrollPosition = (Y + H) - scrollWindowHeight + scrollPadding; this._smartScroll.scrollTo({y: nextScrollPosition}); this.setState({scrollPosition: nextScrollPosition }) } else if ( py < 0 ) { const nextScrollPosition = Y - scrollPadding; this._smartScroll.scrollTo({y: nextScrollPosition}) this.setState({ scrollPosition: nextScrollPosition}) } }); }, 0); } _updateScrollPosition (event) { this.setState({ scrollPosition: event.nativeEvent.contentOffset.y }); } render () { const { children: scrollChildren, contentContainerStyle, scrollContainerStyle, zoomScale, showsVerticalScrollIndicator, contentInset, onScroll } = this.props; let inputIndex = 0; const smartClone = (element, i) => { const { smartScrollOptions } = element.props; let smartProps = { key: i }; if (smartScrollOptions.type !== undefined) { const ref = 'input_' + inputIndex; smartProps.ref = this._refCreator(ref, smartScrollOptions.scrollRef && 'input_' + smartScrollOptions.scrollRef); if (smartScrollOptions.type === 'text') { smartProps.onFocus = () => { smartProps.onFocus = element.props.onFocus && element.props.onFocus(); this._focusNode(ref) }; if (smartScrollOptions.moveToNext === true) { const nextRef = 'input_' + (inputIndex+1); const focusNextField = () => this._focusField(nextRef) if(typeof(element.props.returnKeyType) === 'undefined'){ smartProps.returnKeyType = 'next' } smartProps.blurOnSubmit = false; smartProps.onSubmitEditing = smartScrollOptions.onSubmitEditing ? () => smartScrollOptions.onSubmitEditing(focusNextField) : focusNextField } } inputIndex += 1 } return React.cloneElement(element, smartProps) } function recursivelyCheckAndAdd(children, i) { return React.Children.map(children, (child, j) => { if (child && child.props !== undefined) { if (child.props.smartScrollOptions !== undefined) { return smartClone(child, ''+i+j); } else if (child.props.children !== undefined) { return React.cloneElement(child, {key: i}, recursivelyCheckAndAdd(child.props.children, ''+i+j)); } else { return React.cloneElement(child, {key: i}); } } else { return child } }) } const content = recursivelyCheckAndAdd(scrollChildren, '0'); return ( <View ref = { component => this._container=component } style = {scrollContainerStyle} onLayout={(e) => this._layout = e.nativeEvent.layout} > <View style = {this.state.keyBoardUp ? { height: this.state.scrollWindowHeight } : styles.flex1} > <ScrollView ref = { component => this._smartScroll=component } automaticallyAdjustContentInsets = { false } scrollsToTop = { false } style = { styles.flex1 } onScroll = { (event) => { this._updateScrollPosition(event) onScroll(event) }} scrollEventThrottle = { 16 } contentContainerStyle = { contentContainerStyle } contentInset = { contentInset } zoomScale = { zoomScale } showsVerticalScrollIndicator = { showsVerticalScrollIndicator } keyboardShouldPersistTaps = { true } bounces = { false } > {content} </ScrollView> </View> </View> ); } } const styles = StyleSheet.create({ flex1: { flex: 1 } }); SmartScrollView.propTypes = { forceFocusField: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), scrollContainerStyle: View.propTypes.style, contentContainerStyle: View.propTypes.style, zoomScale: PropTypes.number, showsVerticalScrollIndicator: PropTypes.bool, contentInset: PropTypes.object, onScroll: PropTypes.func, onRefFocus: PropTypes.func, }; SmartScrollView.defaultProps = { scrollContainerStyle: styles.flex1, scrollPadding: 5, zoomScale: 1, showsVerticalScrollIndicator: true, contentInset: {top: 0, left: 0, bottom: 0, right: 0}, onScroll: () => {}, onRefFocus: () => {} }; export default SmartScrollView; // import dismissKeyboard from 'dismissKeyboard'; // this._scrollTap = this._scrollTap.bind(this); // lastTap: 0 // _scrollTap () { // const {lastTap} = this.state; // const currentTap = new Date().getTime(); // console.log("tap") // // if (currentTap - lastTap < 500) { // dismissKeyboard() // } // // this.setState({ // lastTap: currentTap // }) // }