UNPKG

rn-sliding-up-panel

Version:

Draggable sliding up panel implemented in React Native

235 lines (189 loc) 5.97 kB
/* @flow */ import React from 'react' import {Modal, View, TouchableWithoutFeedback, Animated, PanResponder, Platform} from 'react-native' import {visibleHeight} from './libs/layout' import FlickAnimation from './libs/FlickAnimation' import styles from './libs/styles' type AnimateConfig = { duration?: number, delay?: number, easing?: (t: number) => number }; const VMAX = 1.67 class SlidingUpPanel extends React.Component { static propsTypes = { height: React.PropTypes.number, initialPosition: React.PropTypes.number, disableDragging: React.PropTypes.bool, onShow: React.PropTypes.func, onDrag: React.PropTypes.func, onHide: React.PropTypes.func, contentContainerStyle: React.PropTypes.any }; static defaultProps = { disableDragging: false, height: visibleHeight, onShow: () => {}, onHide: () => {}, onDrag: () => {} }; _panResponder: any; _animatedValueY = 0; _translateYAnimation = new Animated.Value(0); _flick = new FlickAnimation(this._translateYAnimation); state = {visible: false}; componentWillMount() { this._translateYAnimation.addListener(this._onDrag) this._panResponder = PanResponder.create({ onStartShouldSetPanResponder: this._onStartShouldSetPanResponder.bind(this), onMoveShouldSetPanResponder: this._onMoveShouldSetPanResponder.bind(this), onPanResponderGrant: this._onPanResponderGrant.bind(this), onPanResponderMove: this._onPanResponderMove.bind(this), onPanResponderRelease: this._onPanResponderRelease.bind(this), onPanResponderTerminate: this._onPanResponderTerminate.bind(this) }) } componentWillUnmount() { this._translateYAnimation.removeListener(this._onDrag) } // eslint-disable-next-line no-unused-vars _onStartShouldSetPanResponder(evt, gestureState) { this._flick.stop() return !this.props.disableDragging } _onMoveShouldSetPanResponder(evt, gestureState) { this._flick.stop() if (this.props.disableDragging) { return false } if (this._animatedValueY <= -this.props.height) { return gestureState.dy > 1 } return Math.abs(gestureState.dy) > 1 } // eslint-disable-next-line no-unused-vars _onPanResponderGrant(evt, gestureState) { this._translateYAnimation.setOffset(this._animatedValueY) this._translateYAnimation.setValue(0) } _onPanResponderMove(evt, gestureState) { if ( this._animatedValueY + gestureState.dy <= -this.props.height ) { return } this._translateYAnimation.setValue(gestureState.dy) } _onPanResponderRelease(evt, gestureState) { if ( this._animatedValueY <= -this.props.height && gestureState.dy <= 0 ) { return } this._translateYAnimation.flattenOffset() const velocity = gestureState.vy if (this._animatedValueY >= -this.props.height / 2) { this.hide() return } // Predict if the panel closes in 20 frames const _delta = 325 * velocity const _nextValueY = this._animatedValueY + _delta if ( (_nextValueY >= -this.props.height / 2 && gestureState.vy > 0) || velocity >= VMAX ) { this.hide() return } if (Math.abs(gestureState.vy) > 0.1) { this._flick.start({velocity, fromValue: this._animatedValueY}) } return } // eslint-disable-next-line no-unused-vars _onPanResponderTerminate(evt, gestureState) { // } _onDrag = ({value}: {value: number}): void => { this._animatedValueY = value this.props.onDrag(value) if (this._animatedValueY >= 0 && this.state.visible) { this.setState({visible: false}) } } _startShowAnimation = (config: AnimateConfig = {}): void => { const animationConfig = { duration: 260, ...config, toValue: -(this.props.initialPosition || this.props.height) } Animated.timing( this._translateYAnimation, animationConfig ).start(() => { if (__DEV__) { console.log('shown') } this.props.onShow() }) } render(): ?React.Element<any> { const translateY = this._translateYAnimation.interpolate({ inputRange: [-this.props.height, 0], outputRange: [-this.props.height, 0], extrapolate: 'clamp' }) const backdropOpacity = this._translateYAnimation.interpolate({ inputRange: [-visibleHeight, 0], outputRange: [0.75, 0], extrapolate: 'clamp' }) const transform = {transform: [{translateY}]} const animatedContainerStyles = [ styles.animatedContainer, {height: this.props.height}, transform ] return ( <Modal transparent animationType='fade' onRequestClose={this.hide} visible={this.state.visible}> <View style={styles.container}> <TouchableWithoutFeedback onPressIn={() => this._flick.stop()} onPress={this.hide}> <Animated.View style={[styles.backdrop, {opacity: backdropOpacity}]} /> </TouchableWithoutFeedback> <Animated.View {...this._panResponder.panHandlers} style={animatedContainerStyles}> <View style={this.props.contentContainerStyle}>{this.props.children}</View> </Animated.View> </View> </Modal> ) } show = (config: AnimateConfig = {}): void => { if (Platform.OS === 'android') { // to make it looks smooth on android config.delay = config.delay || 166.67 } this.setState({visible: true}, () => this._startShowAnimation(config)) } hide = (config: AnimateConfig = {}): void => { this._translateYAnimation.setOffset(0) Animated.timing( this._translateYAnimation, {duration: 260, ...config, toValue: 0} ).start(() => { if (__DEV__) { console.log('hidden') } this.setState({visible: false}) this.props.onHide() }) } } export default SlidingUpPanel