diff --git a/packages/drawer/.eslintrc b/packages/drawer/.eslintrc
index 448eedc9..20448bc9 100644
--- a/packages/drawer/.eslintrc
+++ b/packages/drawer/.eslintrc
@@ -10,5 +10,6 @@
"rules": {
"import/no-unresolved": "off",
+ "eslint-comments/no-unlimited-disable": "off",
}
}
diff --git a/packages/drawer/example/src/GestureInteraction.js b/packages/drawer/example/src/GestureInteraction.js
index 24fe1906..7f5f3669 100644
--- a/packages/drawer/example/src/GestureInteraction.js
+++ b/packages/drawer/example/src/GestureInteraction.js
@@ -2,7 +2,10 @@ import React from 'react';
import { Button, WebView, View } from 'react-native';
import { MapView } from 'expo';
import { withNavigation } from 'react-navigation';
-import { createDrawerNavigator } from 'react-navigation-drawer';
+import {
+ createDrawerNavigator,
+ DrawerGestureContext,
+} from 'react-navigation-drawer';
import { NativeViewGestureHandler } from 'react-native-gesture-handler';
@withNavigation
@@ -41,9 +44,13 @@ class ContainerWithButtons extends React.Component {
const MapScreen = () => (
-
-
-
+
+ {ref => (
+
+
+
+ )}
+
);
@@ -53,12 +60,16 @@ MapScreen.navigationOptions = {
const WebViewScreen = () => (
-
-
-
+
+ {ref => (
+
+
+
+ )}
+
);
@@ -72,8 +83,8 @@ const DrawerExample = createDrawerNavigator(
Web: WebViewScreen,
},
{
- edgeWidth: 100,
- minSwipeDistance: 1,
+ edgeWidth: 70,
+ minSwipeDistance: 3,
contentOptions: {
activeTintColor: '#e91e63',
},
diff --git a/packages/drawer/src/index.js b/packages/drawer/src/index.js
index 03eecc18..0f0ca88f 100644
--- a/packages/drawer/src/index.js
+++ b/packages/drawer/src/index.js
@@ -30,4 +30,8 @@ module.exports = {
get DrawerView() {
return require('./views/DrawerView').default;
},
+
+ get DrawerGestureContext() {
+ return require('./utils/DrawerGestureContext').default;
+ },
};
diff --git a/packages/drawer/src/navigators/__tests__/__snapshots__/createDrawerNavigator-test.js.snap b/packages/drawer/src/navigators/__tests__/__snapshots__/createDrawerNavigator-test.js.snap
index fad653ae..acdf0796 100644
--- a/packages/drawer/src/navigators/__tests__/__snapshots__/createDrawerNavigator-test.js.snap
+++ b/packages/drawer/src/navigators/__tests__/__snapshots__/createDrawerNavigator-test.js.snap
@@ -8,6 +8,11 @@ exports[`createDrawerNavigator renders successfully 1`] = `
drawerType="front"
drawerWidth={320}
edgeWidth={undefined}
+ gestureRef={
+ Object {
+ "current": null,
+ }
+ }
hideStatusBar={false}
minSwipeDistance={undefined}
onDrawerClose={[Function]}
diff --git a/packages/drawer/src/utils/DrawerGestureContext.js b/packages/drawer/src/utils/DrawerGestureContext.js
new file mode 100644
index 00000000..51b82c7d
--- /dev/null
+++ b/packages/drawer/src/utils/DrawerGestureContext.js
@@ -0,0 +1,3 @@
+import React from 'react';
+
+export default React.createContext(null);
diff --git a/packages/drawer/src/views/DrawerLayout.js b/packages/drawer/src/views/DrawerLayout.js
index 00489083..b8a07181 100644
--- a/packages/drawer/src/views/DrawerLayout.js
+++ b/packages/drawer/src/views/DrawerLayout.js
@@ -1,2 +1,492 @@
-import { DrawerLayout } from 'react-native-gesture-handler';
-export default DrawerLayout;
+/* eslint-disable */
+
+// @flow
+
+// #########################################################
+// This is vendored from react-native-gesture-handler!
+// #########################################################
+
+// This component is based on RN's DrawerLayoutAndroid API
+//
+// It perhaps deserves to be put in a separate repo, but since it relies
+// on react-native-gesture-handler library which isn't very popular at the
+// moment I decided to keep it here for the time being. It will allow us
+// to move faster and fix issues that may arise in gesture handler library
+// that could be found when using the drawer component
+
+import React, { Component } from 'react';
+import { Animated, StyleSheet, View, Keyboard, StatusBar } from 'react-native';
+import invariant from '../utils/invariant';
+import { AnimatedEvent } from 'react-native/Libraries/Animated/src/AnimatedEvent';
+
+import {
+ PanGestureHandler,
+ TapGestureHandler,
+ State,
+} from 'react-native-gesture-handler';
+
+const DRAG_TOSS = 0.05;
+
+const IDLE = 'Idle';
+const DRAGGING = 'Dragging';
+const SETTLING = 'Settling';
+
+export type PropType = {
+ children: any,
+ drawerBackgroundColor?: string,
+ drawerPosition: 'left' | 'right',
+ drawerWidth: number,
+ keyboardDismissMode?: 'none' | 'on-drag',
+ onDrawerClose?: Function,
+ onDrawerOpen?: Function,
+ onDrawerStateChanged?: Function,
+ renderNavigationView: (progressAnimatedValue: any) => any,
+ useNativeAnimations: boolean,
+
+ // brand new properties
+ drawerType: 'front' | 'back' | 'slide',
+ edgeWidth: number,
+ minSwipeDistance: number,
+ hideStatusBar?: boolean,
+ statusBarAnimation?: 'slide' | 'none' | 'fade',
+ overlayColor: string,
+ contentContainerStyle?: any,
+
+ // Properties not yet supported
+ // onDrawerSlide?: Function
+ // drawerLockMode?: 'unlocked' | 'locked-closed' | 'locked-open',
+};
+
+export type StateType = {
+ drawerShown: boolean,
+ dragX: any,
+ touchX: any,
+ drawerTranslation: any,
+ containerWidth: number,
+};
+
+export type EventType = {
+ stopPropagation: Function,
+};
+
+export type DrawerMovementOptionType = {
+ velocity?: number,
+};
+
+export default class DrawerLayout extends Component {
+ static defaultProps = {
+ drawerWidth: 200,
+ drawerPosition: 'left',
+ useNativeAnimations: true,
+ drawerType: 'front',
+ edgeWidth: 20,
+ minSwipeDistance: 3,
+ overlayColor: 'black',
+ };
+
+ static positions = {
+ Left: 'left',
+ Right: 'right',
+ };
+ _openValue: ?Animated.Interpolation;
+ _onGestureEvent: ?AnimatedEvent;
+
+ constructor(props: PropType, context: any) {
+ super(props, context);
+
+ const dragX = new Animated.Value(0);
+ const touchX = new Animated.Value(0);
+ const drawerTranslation = new Animated.Value(0);
+
+ this.state = {
+ dragX,
+ touchX,
+ drawerTranslation,
+ drawerShown: false,
+ containerWidth: 0,
+ };
+
+ this._updateAnimatedEvent(props, this.state);
+ }
+
+ componentWillUpdate(props: PropType, state: StateType) {
+ if (
+ this.props.drawerPosition !== props.drawerPosition ||
+ this.props.drawerWidth !== props.drawerWidth ||
+ this.props.drawerType !== props.drawerType ||
+ this.state.containerWidth !== state.containerWidth
+ ) {
+ this._updateAnimatedEvent(props, state);
+ }
+ }
+
+ _updateAnimatedEvent = (props: PropType, state: StateType) => {
+ // Event definition is based on
+ const { drawerPosition, drawerWidth, drawerType } = props;
+ const {
+ dragX: dragXValue,
+ touchX: touchXValue,
+ drawerTranslation,
+ containerWidth,
+ } = state;
+
+ let dragX = dragXValue;
+ let touchX = touchXValue;
+
+ if (drawerPosition !== 'left') {
+ // Most of the code is written in a way to handle left-side drawer.
+ // In order to handle right-side drawer the only thing we need to
+ // do is to reverse events coming from gesture handler in a way they
+ // emulate left-side drawer gestures. E.g. dragX is simply -dragX, and
+ // touchX is calulcated by subtracing real touchX from the width of the
+ // container (such that when touch happens at the right edge the value
+ // is simply 0)
+ dragX = Animated.multiply(new Animated.Value(-1), dragXValue);
+ touchX = Animated.add(
+ new Animated.Value(containerWidth),
+ Animated.multiply(new Animated.Value(-1), touchXValue)
+ );
+ touchXValue.setValue(containerWidth);
+ } else {
+ touchXValue.setValue(0);
+ }
+
+ // While closing the drawer when user starts gesture outside of its area (in greyed
+ // out part of the window), we want the drawer to follow only once finger reaches the
+ // edge of the drawer.
+ // E.g. on the diagram below drawer is illustrate by X signs and the greyed out area by
+ // dots. The touch gesture starts at '*' and moves left, touch path is indicated by
+ // an arrow pointing left
+ // 1) +---------------+ 2) +---------------+ 3) +---------------+ 4) +---------------+
+ // |XXXXXXXX|......| |XXXXXXXX|......| |XXXXXXXX|......| |XXXXX|.........|
+ // |XXXXXXXX|......| |XXXXXXXX|......| |XXXXXXXX|......| |XXXXX|.........|
+ // |XXXXXXXX|......| |XXXXXXXX|......| |XXXXXXXX|......| |XXXXX|.........|
+ // |XXXXXXXX|......| |XXXXXXXX|.<-*..| |XXXXXXXX|<--*..| |XXXXX|<-----*..|
+ // |XXXXXXXX|......| |XXXXXXXX|......| |XXXXXXXX|......| |XXXXX|.........|
+ // |XXXXXXXX|......| |XXXXXXXX|......| |XXXXXXXX|......| |XXXXX|.........|
+ // |XXXXXXXX|......| |XXXXXXXX|......| |XXXXXXXX|......| |XXXXX|.........|
+ // +---------------+ +---------------+ +---------------+ +---------------+
+ //
+ // For the above to work properly we define animated value that will keep start position
+ // of the gesture. Then we use that value to calculate how much we need to subtract from
+ // the dragX. If the gesture started on the greyed out area we take the distance from the
+ // edge of the drawer to the start position. Otherwise we don't subtract at all and the
+ // drawer be pulled back as soon as you start the pan.
+ //
+ // This is used only when drawerType is "front"
+ //
+ let translationX = dragX;
+ if (drawerType === 'front') {
+ const startPositionX = Animated.add(
+ touchX,
+ Animated.multiply(new Animated.Value(-1), dragX)
+ );
+
+ const dragOffsetFromOnStartPosition = startPositionX.interpolate({
+ inputRange: [drawerWidth - 1, drawerWidth, drawerWidth + 1],
+ outputRange: [0, 0, 1],
+ });
+ translationX = Animated.add(dragX, dragOffsetFromOnStartPosition);
+ }
+
+ this._openValue = Animated.add(translationX, drawerTranslation).interpolate(
+ {
+ inputRange: [0, drawerWidth],
+ outputRange: [0, 1],
+ extrapolate: 'clamp',
+ }
+ );
+
+ this._onGestureEvent = Animated.event(
+ [{ nativeEvent: { translationX: dragXValue, x: touchXValue } }],
+ { useNativeDriver: props.useNativeAnimations }
+ );
+ };
+
+ _handleContainerLayout = ({ nativeEvent }) => {
+ this.setState({ containerWidth: nativeEvent.layout.width });
+ };
+
+ _emitStateChanged = (newState: string, drawerWillShow: boolean) => {
+ this.props.onDrawerStateChanged &&
+ this.props.onDrawerStateChanged(newState, drawerWillShow);
+ };
+
+ _openingHandlerStateChange = ({ nativeEvent }) => {
+ if (nativeEvent.oldState === State.ACTIVE) {
+ this._handleRelease(nativeEvent);
+ } else if (nativeEvent.state === State.ACTIVE) {
+ this._emitStateChanged(DRAGGING, false);
+ if (this.props.keyboardDismissMode === 'on-drag') {
+ Keyboard.dismiss();
+ }
+ if (this.props.hideStatusBar) {
+ StatusBar.setHidden(true, this.props.statusBarAnimation || 'slide');
+ }
+ }
+ };
+
+ _onTapHandlerStateChange = ({ nativeEvent }) => {
+ if (this.state.drawerShown && nativeEvent.oldState === State.ACTIVE) {
+ this.closeDrawer();
+ }
+ };
+
+ _handleRelease = nativeEvent => {
+ const { drawerWidth, drawerPosition, drawerType } = this.props;
+ const { drawerShown, containerWidth } = this.state;
+ let { translationX: dragX, velocityX, x: touchX } = nativeEvent;
+
+ if (drawerPosition !== 'left') {
+ // See description in _updateAnimatedEvent about why events are flipped
+ // for right-side drawer
+ dragX = -dragX;
+ touchX = containerWidth - touchX;
+ velocityX = -velocityX;
+ }
+
+ const gestureStartX = touchX - dragX;
+ let dragOffsetBasedOnStart = 0;
+
+ if (drawerType === 'front') {
+ dragOffsetBasedOnStart =
+ gestureStartX > drawerWidth ? gestureStartX - drawerWidth : 0;
+ }
+
+ const startOffsetX =
+ dragX + dragOffsetBasedOnStart + (drawerShown ? drawerWidth : 0);
+ const projOffsetX = startOffsetX + DRAG_TOSS * velocityX;
+
+ const shouldOpen = projOffsetX > drawerWidth / 2;
+
+ if (shouldOpen) {
+ this._animateDrawer({
+ fromValue: startOffsetX,
+ toValue: drawerWidth,
+ velocity: velocityX,
+ });
+ } else {
+ this._animateDrawer({
+ fromValue: startOffsetX,
+ toValue: 0,
+ velocity: velocityX,
+ });
+ }
+ };
+
+ _animateDrawer = ({
+ fromValue,
+ toValue,
+ velocity,
+ }: {
+ fromValue: number,
+ toValue: number,
+ velocity: number,
+ }) => {
+ this.state.dragX.setValue(0);
+ this.state.touchX.setValue(
+ this.props.drawerPosition === 'left' ? 0 : this.state.containerWidth
+ );
+
+ if (typeof fromValue === 'number') {
+ this.state.drawerTranslation.setValue(fromValue);
+ }
+
+ const willShow = toValue !== 0;
+ this.setState({ drawerShown: willShow });
+ this._emitStateChanged(SETTLING, willShow);
+ if (this.props.hideStatusBar) {
+ StatusBar.setHidden(willShow, this.props.statusBarAnimation || 'slide');
+ }
+ Animated.spring(this.state.drawerTranslation, {
+ velocity,
+ bounciness: 0,
+ toValue,
+ useNativeDriver: this.props.useNativeAnimations,
+ }).start(({ finished }) => {
+ if (finished) {
+ this._emitStateChanged(IDLE, willShow);
+ if (willShow) {
+ this.props.onDrawerOpen && this.props.onDrawerOpen();
+ } else {
+ this.props.onDrawerClose && this.props.onDrawerClose();
+ }
+ }
+ });
+ };
+
+ openDrawer = (options: DrawerMovementOptionType = {}) => {
+ this._animateDrawer({
+ toValue: this.props.drawerWidth,
+ velocity: options.velocity ? options.velocity : 0,
+ });
+ };
+
+ closeDrawer = (options: DrawerMovementOptionType = {}) => {
+ this._animateDrawer({
+ toValue: 0,
+ velocity: options.velocity ? options.velocity : 0,
+ });
+ };
+
+ _renderOverlay = () => {
+ /* Overlay styles */
+ invariant(this._openValue, 'should be set');
+ const overlayOpacity = this._openValue.interpolate({
+ inputRange: [0, 1],
+ outputRange: [0, 0.7],
+ extrapolate: 'clamp',
+ });
+ const dynamicOverlayStyles = {
+ opacity: overlayOpacity,
+ backgroundColor: this.props.overlayColor,
+ };
+ return (
+
+
+
+ );
+ };
+
+ _renderDrawer = () => {
+ const { drawerShown } = this.state;
+ const {
+ drawerBackgroundColor,
+ drawerWidth,
+ drawerPosition,
+ drawerType,
+ contentContainerStyle,
+ } = this.props;
+
+ const fromLeft = drawerPosition === 'left';
+ const drawerSlide = drawerType !== 'back';
+ const containerSlide = drawerType !== 'front';
+
+ const dynamicDrawerStyles = {
+ backgroundColor: drawerBackgroundColor,
+ width: drawerWidth,
+ };
+ const openValue = this._openValue;
+ invariant(openValue, 'should be set');
+
+ let containerStyles;
+ if (containerSlide) {
+ const containerTranslateX = openValue.interpolate({
+ inputRange: [0, 1],
+ outputRange: fromLeft ? [0, drawerWidth] : [0, -drawerWidth],
+ extrapolate: 'clamp',
+ });
+ containerStyles = {
+ transform: [{ translateX: containerTranslateX }],
+ };
+ }
+
+ let drawerTranslateX = 0;
+ if (drawerSlide) {
+ const closedDrawerOffset = fromLeft ? -drawerWidth : drawerWidth;
+ drawerTranslateX = openValue.interpolate({
+ inputRange: [0, 1],
+ outputRange: [closedDrawerOffset, 0],
+ extrapolate: 'clamp',
+ });
+ }
+ const drawerStyles = {
+ transform: [{ translateX: drawerTranslateX }],
+ flexDirection: fromLeft ? 'row' : 'row-reverse',
+ };
+
+ return (
+
+
+ {typeof this.props.children === 'function'
+ ? this.props.children(this._openValue)
+ : this.props.children}
+ {this._renderOverlay()}
+
+
+
+ {this.props.renderNavigationView(this._openValue)}
+
+
+
+ );
+ };
+
+ render() {
+ const { drawerShown, containerWidth } = this.state;
+
+ const {
+ drawerPosition,
+ drawerType,
+ edgeWidth,
+ minSwipeDistance,
+ } = this.props;
+
+ const fromLeft = drawerPosition === 'left';
+
+ // gestureOrientation is 1 if the expected gesture is from left to right and -1 otherwise
+ // e.g. when drawer is on the left and is closed we expect left to right gesture, thus
+ // orientation will be 1.
+ const gestureOrientation = (fromLeft ? 1 : -1) * (drawerShown ? -1 : 1);
+
+ // When drawer is closed we want the hitSlop to be horizontally shorter
+ // than the container size by the value of SLOP. This will make it only
+ // activate when gesture happens not further than SLOP away from the edge
+ const hitSlop = fromLeft
+ ? { right: drawerShown ? 0 : edgeWidth - containerWidth }
+ : { left: drawerShown ? 0 : edgeWidth - containerWidth };
+
+ return (
+
+ {this._renderDrawer()}
+
+ );
+ }
+}
+
+const styles = StyleSheet.create({
+ drawer: { flex: 0 },
+ drawerContainer: {
+ ...StyleSheet.absoluteFillObject,
+ zIndex: 1001,
+ flexDirection: 'row',
+ },
+ containerInFront: {
+ ...StyleSheet.absoluteFillObject,
+ zIndex: 1002,
+ },
+ containerOnBack: {
+ ...StyleSheet.absoluteFillObject,
+ },
+ main: {
+ flex: 1,
+ zIndex: 0,
+ overflow: 'hidden',
+ },
+ overlay: {
+ ...StyleSheet.absoluteFillObject,
+ zIndex: 1000,
+ },
+});
diff --git a/packages/drawer/src/views/DrawerView.js b/packages/drawer/src/views/DrawerView.js
index 6c592f80..811b9745 100644
--- a/packages/drawer/src/views/DrawerView.js
+++ b/packages/drawer/src/views/DrawerView.js
@@ -4,6 +4,7 @@ import { SceneView } from 'react-navigation';
import DrawerActions from '../routers/DrawerActions';
import DrawerLayout from './DrawerLayout';
import DrawerSidebar from './DrawerSidebar';
+import DrawerGestureContext from '../utils/DrawerGestureContext';
/**
* Component that renders the drawer.
@@ -57,6 +58,8 @@ export default class DrawerView extends React.PureComponent {
Dimensions.removeEventListener('change', this._updateWidth);
}
+ drawerGestureRef = React.createRef();
+
_handleDrawerOpen = () => {
this.props.navigation.dispatch({
type: DrawerActions.DRAWER_OPENED,
@@ -84,16 +87,18 @@ export default class DrawerView extends React.PureComponent {
_renderNavigationView = () => {
return (
-
+
+
+
);
};
@@ -109,6 +114,7 @@ export default class DrawerView extends React.PureComponent {
ref={c => {
this._drawer = c;
}}
+ gestureRef={this.drawerGestureRef}
drawerLockMode={
drawerLockMode ||
(this.props.screenProps && this.props.screenProps.drawerLockMode) ||
@@ -135,11 +141,13 @@ export default class DrawerView extends React.PureComponent {
minSwipeDistance={this.props.navigationConfig.minSwipeDistance}
overlayColor={this.props.navigationConfig.overlayColor}
>
-
+
+
+
);
}