react-native-sliding-panel
Version:
Fork of rn-sliding-up-panel with custom maxOpacity
363 lines (301 loc) • 9.87 kB
JavaScript
import React from 'react'
import PropTypes from 'prop-types'
import {Animated, PanResponder, Platform} from 'react-native'
import ViewOverflow from 'react-native-view-overflow'
import testID from 'react-native-testid'
import clamp from 'clamp'
import FlickAnimation from './libs/FlickAnimation'
import {visibleHeight} from './libs/layout'
import styles from './libs/styles'
const deprecated = (condition, message) => condition && console.warn(message)
const MINIMUM_VELOCITY_THRESHOLD = 0.1
const MINIMUM_DISTANCE_THRESHOLD = 0.24
const DEFAULT_SLIDING_DURATION = 240
const AnimatedViewOverflow = Animated.createAnimatedComponent(ViewOverflow)
class SlidingUpPanel extends React.Component {
static propTypes = {
visible: PropTypes.bool.isRequired,
draggableRange: PropTypes.shape({
top: PropTypes.number,
bottom: PropTypes.number
}),
height: PropTypes.number,
onDrag: PropTypes.func,
onDragStart: PropTypes.func,
onDragEnd: PropTypes.func,
onRequestClose: PropTypes.func,
startCollapsed: PropTypes.bool,
allowMomentum: PropTypes.bool,
allowDragging: PropTypes.bool,
showBackdrop: PropTypes.bool,
maxOpacity: PropTypes.number,
contentStyle: PropTypes.any,
backdropTestID: PropTypes.any
}
static defaultProps = {
height: visibleHeight,
draggableRange: {top: visibleHeight, bottom: 0},
onDrag: () => {},
onDragStart: () => {},
onDragEnd: () => {},
onRequestClose: () => {},
allowMomentum: true,
allowDragging: true,
showBackdrop: true,
maxOpacity: 0.75
}
constructor(props) {
super(props)
this._onDrag = this._onDrag.bind(this)
this._renderContent = this._renderContent.bind(this)
this._renderBackdrop = this._renderBackdrop.bind(this)
this._isInsideDraggableRange = this._isInsideDraggableRange.bind(this)
this._triggerAnimation = this._triggerAnimation.bind(this)
this.transitionTo = this.transitionTo.bind(this)
this.state = {
visible: props.visible,
open: !props.startCollapsed
}
if (__DEV__) {
deprecated(
props.contentStyle,
'SlidingUpPanel#contentStyle is deprecated. ' +
'You should wrap your content inside a View.'
)
}
const {top, bottom} = props.draggableRange
const collapsedPosition = this.props.startCollapsed ? -bottom : -top
this._animatedValueY = this.state.visible ? collapsedPosition : -bottom
this._translateYAnimation = new Animated.Value(this._animatedValueY)
this._flick = new FlickAnimation(this._translateYAnimation, -top, -bottom)
this._panResponder = PanResponder.create({
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),
onPanResponderTerminationRequest: () => false
})
this._backdrop = null
this._isAtBottom = !props.visible
this._requestCloseTriggered = false
this._translateYAnimation.addListener(this._onDrag)
}
componentWillReceiveProps(nextProps) {
if (nextProps.visible && !this.props.visible) {
this._requestCloseTriggered = false
this.setState({visible: true}, () => {
this.transitionTo(-this.props.draggableRange.top)
})
return
}
const {bottom} = this.props.draggableRange
if (
!nextProps.visible &&
this.props.visible &&
-this._animatedValueY > bottom
) {
this._requestCloseTriggered = true
this.transitionTo({
toValue: -bottom,
onAnimationEnd: () => this.setState({visible: false})
})
return
}
if (
nextProps.draggableRange.top !== this.props.draggableRange.top ||
nextProps.draggableRange.bottom !== this.props.draggableRange.bottom
) {
const {top, bottom} = nextProps.draggableRange
this._flick = new FlickAnimation(this._translateYAnimation, -top, -bottom)
}
}
_onMoveShouldSetPanResponder(evt, gestureState) {
return (
this.props.allowDragging &&
this._isInsideDraggableRange() &&
Math.abs(gestureState.dy) > MINIMUM_DISTANCE_THRESHOLD
)
}
// eslint-disable-next-line no-unused-vars
_onPanResponderGrant(evt, gestureState) {
this._flick.stop()
this._translateYAnimation.setOffset(this._animatedValueY)
this._translateYAnimation.setValue(0)
this.props.onDragStart(-this._animatedValueY)
}
_onPanResponderMove(evt, gestureState) {
if (!this._isInsideDraggableRange()) {
return
}
this._translateYAnimation.setValue(gestureState.dy)
}
// Trigger when you release your finger
_onPanResponderRelease(evt, gestureState) {
const {top, bottom} = this.props.draggableRange
if (!this._isInsideDraggableRange()) {
return
}
this._translateYAnimation.flattenOffset()
const cancelFlick = this.props.onDragEnd(-this._animatedValueY)
if (!this.props.allowMomentum || cancelFlick) {
return
}
if (Math.abs(gestureState.vy) > MINIMUM_VELOCITY_THRESHOLD) {
this._flick.start(
{
velocity: gestureState.vy,
fromValue: this._animatedValueY
},
() => {
const halfRange = (top - bottom) / 2 + bottom
if (-this._animatedValueY > halfRange) {
this.transitionTo(-this.props.draggableRange.top)
} else {
this.transitionTo(-this.props.draggableRange.bottom)
}
}
)
}
const halfRange = (top - bottom) / 2 + bottom
if (-this._animatedValueY > halfRange) {
this.transitionTo(-this.props.draggableRange.top)
} else {
this.transitionTo(-this.props.draggableRange.bottom)
}
return
}
// eslint-disable-next-line no-unused-vars
_onPanResponderTerminate(evt, gestureState) {
//
}
_isInsideDraggableRange() {
const {top, bottom} = this.props.draggableRange
return this._animatedValueY >= -top && this._animatedValueY <= -bottom
}
_onDrag({value}) {
const {top, bottom} = this.props.draggableRange
if (value >= -bottom) {
this._isAtBottom = true
// if (this._backdrop != null) {
// this._backdrop.setNativeProps({pointerEvents: 'none'})
// }
if (!this._requestCloseTriggered) {
this.props.onRequestClose()
}
return
}
if (this._isAtBottom) {
this._isAtBottom = false
// if (this._backdrop != null) {
// this._backdrop.setNativeProps({pointerEvents: 'box-only'})
// }
}
this._animatedValueY = clamp(value, -top, -bottom)
this.props.onDrag(-this._animatedValueY)
}
transitionTo(mayBeValueOrOptions) {
if (
mayBeValueOrOptions === this.props.draggableRange.top ||
mayBeValueOrOptions === -this.props.draggableRange.top
) {
this.setState({open: true})
} else {
this.setState({open: false})
}
if (typeof mayBeValueOrOptions === 'object') {
return this._triggerAnimation(mayBeValueOrOptions)
}
return this._triggerAnimation({toValue: mayBeValueOrOptions})
}
_triggerAnimation(options = {}) {
const {
toValue,
easing,
onAnimationEnd = () => {},
duration = DEFAULT_SLIDING_DURATION
} = options
const animationConfig = {
duration,
easing,
toValue: -Math.abs(toValue),
delay: Platform.OS === 'android' ? 166.67 : undefined // to make it looks smooth on android
}
const animation = Animated.timing(
this._translateYAnimation,
animationConfig
)
animation.start(onAnimationEnd)
}
_renderBackdrop() {
if (!this.props.showBackdrop) {
return null
}
const {top, bottom} = this.props.draggableRange
const backdropOpacity = this._translateYAnimation.interpolate({
inputRange: [-top, -bottom],
outputRange: [this.props.maxOpacity, 0],
extrapolate: 'clamp'
})
return (
<AnimatedViewOverflow
key="backdrop"
pointerEvents="box-only"
ref={c => (this._backdrop = c)}
onTouchStart={() => this._flick.stop()}
onTouchEnd={() => this.props.onRequestClose()}
{...testID(this.props.backdropTestID)}
style={[
styles.backdrop,
{
opacity: backdropOpacity,
height: this.state.open ? visibleHeight : 1
}
]}
/>
)
}
_renderContent() {
const {top, bottom} = this.props.draggableRange
const height = this.props.height
const translateY = this._translateYAnimation.interpolate({
inputRange: [-top, -bottom],
outputRange: [-top, -bottom],
extrapolate: 'clamp'
})
const transform = {transform: [{translateY}]}
const animatedContainerStyles = [
styles.animatedContainer,
transform,
{height, top: visibleHeight, bottom: 0}
]
if (typeof this.props.children === 'function') {
return (
<AnimatedViewOverflow
accessible={false}
key="content"
pointerEvents="box-none"
style={animatedContainerStyles}>
{this.props.children(this._panResponder.panHandlers)}
</AnimatedViewOverflow>
)
}
return (
<AnimatedViewOverflow
accessible={false}
key="content"
pointerEvents="box-none"
style={animatedContainerStyles}
{...this._panResponder.panHandlers}>
{this.props.children}
</AnimatedViewOverflow>
)
}
render() {
if (!this.state.visible) {
return null
}
return [this._renderBackdrop(), this._renderContent()]
}
}
export default SlidingUpPanel