Created
February 19, 2018 08:33
-
-
Save blackneck/a4cbe54f6b78459f180c961b7bf98ad2 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, { Component } from 'react'; | |
import { | |
Animated, | |
Dimensions, | |
Keyboard, | |
PanResponder, | |
StyleSheet, | |
TouchableWithoutFeedback, | |
View, | |
I18nManager, | |
} from 'react-native'; | |
const MIN_SWIPE_DISTANCE = 3; | |
const DEVICE_WIDTH = parseFloat(Dimensions.get('window').width); | |
const THRESHOLD = DEVICE_WIDTH / 2; | |
const VX_MAX = 0.1; | |
const IDLE = 'Idle'; | |
const DRAGGING = 'Dragging'; | |
const SETTLING = 'Settling'; | |
export type PropType = { | |
children: any, | |
drawerType?: 'overlay' | 'push-screen', | |
drawerBackgroundColor?: string, | |
drawerLockMode?: 'unlocked' | 'locked-closed' | 'locked-open', | |
drawerPosition: 'left' | 'right', | |
drawerWidth: number, | |
keyboardDismissMode?: 'none' | 'on-drag', | |
onDrawerClose?: Function, | |
onDrawerOpen?: Function, | |
onDrawerSlide?: Function, | |
onDrawerStateChanged?: Function, | |
renderNavigationView: () => any, | |
statusBarBackgroundColor?: string, | |
useNativeAnimations?: boolean, | |
}; | |
export type StateType = { | |
accessibilityViewIsModal: boolean, | |
drawerShown: boolean, | |
openValue: any, | |
}; | |
export type EventType = { | |
stopPropagation: Function, | |
}; | |
export type PanResponderEventType = { | |
dx: number, | |
dy: number, | |
moveX: number, | |
moveY: number, | |
vx: number, | |
vy: number, | |
}; | |
export type DrawerMovementOptionType = { | |
velocity?: number, | |
}; | |
export default class DrawerLayout extends Component { | |
props: PropType; | |
state: StateType; | |
_lastOpenValue: number; | |
_panResponder: any; | |
_isClosing: boolean; | |
_closingAnchorValue: number; | |
static defaultProps = { | |
drawerWidth: 0, | |
drawerPosition: 'left', | |
useNativeAnimations: false, | |
}; | |
static positions = { | |
Left: 'left', | |
Right: 'right', | |
}; | |
constructor(props: PropType, context: any) { | |
super(props, context); | |
this.state = { | |
accessibilityViewIsModal: false, | |
drawerShown: false, | |
openValue: new Animated.Value(0), | |
}; | |
} | |
getDrawerPosition() { | |
const { drawerPosition } = this.props; | |
const rtl = I18nManager.isRTL; | |
return rtl | |
? drawerPosition === 'left' ? 'right' : 'left' // invert it | |
: drawerPosition; | |
} | |
componentWillMount() { | |
const { openValue } = this.state; | |
openValue.addListener(({ value }) => { | |
const drawerShown = value > 0; | |
const accessibilityViewIsModal = drawerShown; | |
if (drawerShown !== this.state.drawerShown) { | |
this.setState({ drawerShown, accessibilityViewIsModal }); | |
} | |
if (this.props.keyboardDismissMode === 'on-drag') { | |
Keyboard.dismiss(); | |
} | |
this._lastOpenValue = value; | |
if (this.props.onDrawerSlide) { | |
this.props.onDrawerSlide({ nativeEvent: { offset: value } }); | |
} | |
}); | |
this._panResponder = PanResponder.create({ | |
onMoveShouldSetPanResponder: this._shouldSetPanResponder, | |
onPanResponderGrant: this._panResponderGrant, | |
onPanResponderMove: this._panResponderMove, | |
onPanResponderTerminationRequest: () => false, | |
onPanResponderRelease: this._panResponderRelease, | |
onPanResponderTerminate: () => {}, | |
}); | |
} | |
render() { | |
const { accessibilityViewIsModal, drawerShown, openValue } = this.state; | |
const { | |
drawerBackgroundColor, | |
drawerWidth, | |
drawerPosition, | |
drawerType | |
} = this.props; | |
/** | |
* We need to use the "original" drawer position here | |
* as RTL turns position left and right on its own | |
**/ | |
const dynamicDrawerStyles = { | |
backgroundColor: drawerBackgroundColor, | |
width: drawerWidth, | |
left: drawerPosition === 'left' ? 0 : null, | |
right: drawerPosition === 'right' ? 0 : null, | |
}; | |
/* Drawer styles */ | |
let outputRange; | |
let mainViewPosition; | |
if (this.getDrawerPosition() === 'left') { | |
outputRange = [-drawerWidth, 0]; | |
mainViewPosition = [0, drawerWidth]; | |
} else { | |
outputRange = [drawerWidth, 0]; | |
mainViewPosition = [0, -drawerWidth]; | |
} | |
const drawerTranslateX = openValue.interpolate({ | |
inputRange: [0, 1], | |
outputRange, | |
extrapolate: 'clamp', | |
}); | |
const mainViewTranslateX = openValue.interpolate({ | |
inputRange: [0, 1], | |
outputRange: mainViewPosition, | |
extrapolate: 'clamp', | |
}); | |
const animatedDrawerStyles = { | |
transform: [{ translateX: drawerTranslateX }], | |
}; | |
const animatedMainStyles = { | |
transform: [{ translateX: mainViewTranslateX }], | |
}; | |
/* Overlay styles */ | |
const overlayOpacity = openValue.interpolate({ | |
inputRange: [0, 1], | |
outputRange: [0, 0.7], | |
extrapolate: 'clamp', | |
}); | |
const animatedOverlayStyles = { opacity: drawerType === 'push-screen' ? 0 : overlayOpacity }; | |
const pointerEvents = drawerShown ? 'auto' : 'none'; | |
return ( | |
<View | |
style={{ flex: 1, backgroundColor: 'transparent' }} | |
{...this._panResponder.panHandlers} | |
> | |
<Animated.View style={[styles.main, drawerType === 'push-screen' && animatedMainStyles]}> | |
{this.props.children} | |
</Animated.View> | |
<TouchableWithoutFeedback | |
pointerEvents={pointerEvents} | |
onPress={this._onOverlayClick} | |
> | |
<Animated.View | |
pointerEvents={pointerEvents} | |
style={[styles.overlay, animatedOverlayStyles]} | |
/> | |
</TouchableWithoutFeedback> | |
<Animated.View | |
accessibilityViewIsModal={accessibilityViewIsModal} | |
style={[ | |
styles.drawer, | |
dynamicDrawerStyles, | |
animatedDrawerStyles, | |
]} | |
> | |
{this.props.renderNavigationView()} | |
</Animated.View> | |
</View> | |
); | |
} | |
_onOverlayClick = (e: EventType) => { | |
e.stopPropagation(); | |
if (!this._isLockedClosed() && !this._isLockedOpen()) { | |
this.closeDrawer(); | |
} | |
}; | |
_emitStateChanged = (newState: string) => { | |
if (this.props.onDrawerStateChanged) { | |
this.props.onDrawerStateChanged(newState); | |
} | |
}; | |
openDrawer = (options: DrawerMovementOptionType = {}) => { | |
this._emitStateChanged(SETTLING); | |
Animated.spring(this.state.openValue, { | |
toValue: 1, | |
bounciness: 0, | |
restSpeedThreshold: 0.1, | |
useNativeDriver: this.props.useNativeAnimations, | |
...options, | |
}).start(() => { | |
if (this.props.onDrawerOpen) { | |
this.props.onDrawerOpen(); | |
} | |
this._emitStateChanged(IDLE); | |
}); | |
}; | |
closeDrawer = (options: DrawerMovementOptionType = {}) => { | |
this._emitStateChanged(SETTLING); | |
Animated.spring(this.state.openValue, { | |
toValue: 0, | |
bounciness: 0, | |
restSpeedThreshold: 1, | |
useNativeDriver: this.props.useNativeAnimations, | |
...options, | |
}).start(() => { | |
if (this.props.onDrawerClose) { | |
this.props.onDrawerClose(); | |
} | |
this._emitStateChanged(IDLE); | |
}); | |
}; | |
_handleDrawerOpen = () => { | |
if (this.props.onDrawerOpen) { | |
this.props.onDrawerOpen(); | |
} | |
}; | |
_handleDrawerClose = () => { | |
if (this.props.onDrawerClose) { | |
this.props.onDrawerClose(); | |
} | |
}; | |
_shouldSetPanResponder = ( | |
e: EventType, | |
{ moveX, dx, dy }: PanResponderEventType, | |
) => { | |
if (!dx || !dy || Math.abs(dx) < MIN_SWIPE_DISTANCE) { | |
return false; | |
} | |
if (this._isLockedClosed() || this._isLockedOpen()) { | |
return false; | |
} | |
if (this.getDrawerPosition() === 'left') { | |
const overlayArea = DEVICE_WIDTH - | |
(DEVICE_WIDTH - this.props.drawerWidth); | |
if (this._lastOpenValue === 1) { | |
if ( | |
(dx < 0 && Math.abs(dx) > Math.abs(dy) * 3) || | |
moveX > overlayArea | |
) { | |
this._isClosing = true; | |
this._closingAnchorValue = this._getOpenValueForX(moveX); | |
return true; | |
} | |
} else { | |
if (moveX <= 35 && dx > 0) { | |
this._isClosing = false; | |
return true; | |
} | |
return false; | |
} | |
} else { | |
const overlayArea = DEVICE_WIDTH - this.props.drawerWidth; | |
if (this._lastOpenValue === 1) { | |
if ( | |
(dx > 0 && Math.abs(dx) > Math.abs(dy) * 3) || | |
moveX < overlayArea | |
) { | |
this._isClosing = true; | |
this._closingAnchorValue = this._getOpenValueForX(moveX); | |
return true; | |
} | |
} else { | |
if (moveX >= DEVICE_WIDTH - 35 && dx < 0) { | |
this._isClosing = false; | |
return true; | |
} | |
return false; | |
} | |
} | |
}; | |
_panResponderGrant = () => { | |
this._emitStateChanged(DRAGGING); | |
}; | |
_panResponderMove = (e: EventType, { moveX }: PanResponderEventType) => { | |
let openValue = this._getOpenValueForX(moveX); | |
if (this._isClosing) { | |
openValue = 1 - (this._closingAnchorValue - openValue); | |
} | |
if (openValue > 1) { | |
openValue = 1; | |
} else if (openValue < 0) { | |
openValue = 0; | |
} | |
this.state.openValue.setValue(openValue); | |
}; | |
_panResponderRelease = ( | |
e: EventType, | |
{ moveX, vx }: PanResponderEventType, | |
) => { | |
const previouslyOpen = this._isClosing; | |
const isWithinVelocityThreshold = vx < VX_MAX && vx > -VX_MAX; | |
if (this.getDrawerPosition() === 'left') { | |
if ( | |
(vx > 0 && moveX > THRESHOLD) || | |
vx >= VX_MAX || | |
(isWithinVelocityThreshold && | |
previouslyOpen && | |
moveX > THRESHOLD) | |
) { | |
this.openDrawer({ velocity: vx }); | |
} else if ( | |
(vx < 0 && moveX < THRESHOLD) || | |
vx < -VX_MAX || | |
(isWithinVelocityThreshold && !previouslyOpen) | |
) { | |
this.closeDrawer({ velocity: vx }); | |
} else if (previouslyOpen) { | |
this.openDrawer(); | |
} else { | |
this.closeDrawer(); | |
} | |
} else { | |
if ( | |
(vx < 0 && moveX < THRESHOLD) || | |
vx <= -VX_MAX || | |
(isWithinVelocityThreshold && | |
previouslyOpen && | |
moveX < THRESHOLD) | |
) { | |
this.openDrawer({ velocity: (-1) * vx }); | |
} else if ( | |
(vx > 0 && moveX > THRESHOLD) || | |
vx > VX_MAX || | |
(isWithinVelocityThreshold && !previouslyOpen) | |
) { | |
this.closeDrawer({ velocity: (-1) * vx }); | |
} else if (previouslyOpen) { | |
this.openDrawer(); | |
} else { | |
this.closeDrawer(); | |
} | |
} | |
}; | |
_isLockedClosed = () => { | |
return this.props.drawerLockMode === 'locked-closed' && | |
!this.state.drawerShown; | |
}; | |
_isLockedOpen = () => { | |
return this.props.drawerLockMode === 'locked-open' && | |
this.state.drawerShown; | |
}; | |
_getOpenValueForX(x: number): number { | |
const { drawerWidth } = this.props; | |
if (this.getDrawerPosition() === 'left') { | |
return x / drawerWidth; | |
} | |
// position === 'right' | |
return (DEVICE_WIDTH - x) / drawerWidth; | |
} | |
} | |
const styles = StyleSheet.create({ | |
drawer: { | |
position: 'absolute', | |
top: 0, | |
bottom: 0, | |
zIndex: 1001, | |
}, | |
main: { | |
flex: 1, | |
zIndex: 0, | |
}, | |
overlay: { | |
backgroundColor: '#000', | |
position: 'absolute', | |
top: 0, | |
left: 0, | |
bottom: 0, | |
right: 0, | |
zIndex: 1000, | |
}, | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment