Skip to content

Commit

Permalink
Merge pull request #37080 from margelo/feat/swipe-down-to-close
Browse files Browse the repository at this point in the history
feat: swipe down to close
  • Loading branch information
srikarparsi authored Feb 26, 2024
2 parents 280ea03 + 44efead commit dbcdbce
Show file tree
Hide file tree
Showing 7 changed files with 109 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type AttachmentCarouselPagerContextValue = {
isScrollEnabled: SharedValue<boolean>;
onTap: () => void;
onScaleChanged: (scale: number) => void;
onSwipeDown: () => void;
};

const AttachmentCarouselPagerContext = createContext<AttachmentCarouselPagerContextValue | null>(null);
Expand Down
11 changes: 9 additions & 2 deletions src/components/Attachments/AttachmentCarousel/Pager/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,15 @@ type AttachmentCarouselPagerProps = {
* @param showArrows If set, it will show/hide the arrows. If not set, it will toggle the arrows.
*/
onRequestToggleArrows: (showArrows?: boolean) => void;

/** A callback that is called when swipe-down-to-close gesture happens */
onClose: () => void;
};

function AttachmentCarouselPager({items, activeSource, initialPage, onPageSelected, onRequestToggleArrows}: AttachmentCarouselPagerProps, ref: ForwardedRef<AttachmentCarouselPagerHandle>) {
function AttachmentCarouselPager(
{items, activeSource, initialPage, onPageSelected, onRequestToggleArrows, onClose}: AttachmentCarouselPagerProps,
ref: ForwardedRef<AttachmentCarouselPagerHandle>,
) {
const styles = useThemeStyles();
const pagerRef = useRef<PagerView>(null);

Expand Down Expand Up @@ -114,9 +120,10 @@ function AttachmentCarouselPager({items, activeSource, initialPage, onPageSelect
isScrollEnabled,
pagerRef,
onTap: handleTap,
onSwipeDown: onClose,
onScaleChanged: handleScaleChange,
}),
[pagerItems, activePageIndex, isPagerScrolling, isScrollEnabled, handleTap, handleScaleChange],
[pagerItems, activePageIndex, isPagerScrolling, isScrollEnabled, handleTap, onClose, handleScaleChange],
);

const animatedProps = useAnimatedProps(() => ({
Expand Down
5 changes: 5 additions & 0 deletions src/components/Attachments/AttachmentCarousel/index.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
[setShouldShowArrows],
);

const goBack = useCallback(() => {
Navigation.goBack();
}, []);

return (
<View style={[styles.flex1, styles.attachmentCarouselContainer]}>
{page == null ? (
Expand Down Expand Up @@ -133,6 +137,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
activeSource={activeSource}
onRequestToggleArrows={toggleArrows}
onPageSelected={({nativeEvent: {position: newPage}}) => updatePage(newPage)}
onClose={goBack}
ref={pagerRef}
/>
</>
Expand Down
3 changes: 3 additions & 0 deletions src/components/Lightbox/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ function Lightbox({isAuthTokenRequired = false, uri, onScaleChanged: onScaleChan
activePage,
onTap,
onScaleChanged: onScaleChangedContext,
onSwipeDown,
pagerRef,
} = useMemo(() => {
if (attachmentCarouselPagerContext === null) {
Expand All @@ -70,6 +71,7 @@ function Lightbox({isAuthTokenRequired = false, uri, onScaleChanged: onScaleChan
activePage: 0,
onTap: () => {},
onScaleChanged: () => {},
onSwipeDown: () => {},
pagerRef: undefined,
};
}
Expand Down Expand Up @@ -212,6 +214,7 @@ function Lightbox({isAuthTokenRequired = false, uri, onScaleChanged: onScaleChan
shouldDisableTransformationGestures={isPagerScrolling}
onTap={onTap}
onScaleChanged={scaleChange}
onSwipeDown={onSwipeDown}
>
<Image
source={{uri}}
Expand Down
8 changes: 7 additions & 1 deletion src/components/MultiGestureCanvas/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import type ChildrenProps from '@src/types/utils/ChildrenProps';
import {DEFAULT_ZOOM_RANGE, SPRING_CONFIG, ZOOM_RANGE_BOUNCE_FACTORS} from './constants';
import type {CanvasSize, ContentSize, OnScaleChangedCallback, OnTapCallback, ZoomRange} from './types';
import type {CanvasSize, ContentSize, OnScaleChangedCallback, OnSwipeDownCallback, OnTapCallback, ZoomRange} from './types';
import usePanGesture from './usePanGesture';
import usePinchGesture from './usePinchGesture';
import useTapGestures from './useTapGestures';
Expand Down Expand Up @@ -47,6 +47,8 @@ type MultiGestureCanvasProps = ChildrenProps & {

/** Handles scale changed event */
onTap?: OnTapCallback;

onSwipeDown?: OnSwipeDownCallback;
};

function MultiGestureCanvas({
Expand All @@ -59,6 +61,7 @@ function MultiGestureCanvas({
shouldDisableTransformationGestures: shouldDisableTransformationGesturesProp,
onTap,
onScaleChanged,
onSwipeDown,
}: MultiGestureCanvasProps) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
Expand Down Expand Up @@ -88,6 +91,7 @@ function MultiGestureCanvas({

const panTranslateX = useSharedValue(0);
const panTranslateY = useSharedValue(0);
const isSwipingDownToClose = useSharedValue(false);
const panGestureRef = useRef(Gesture.Pan());

const pinchScale = useSharedValue(1);
Expand Down Expand Up @@ -172,6 +176,8 @@ function MultiGestureCanvas({
panTranslateY,
stopAnimation,
shouldDisableTransformationGestures,
isSwipingDownToClose,
onSwipeDown,
})
.simultaneousWithExternalGesture(...panGestureSimultaneousList)
.withRef(panGestureRef);
Expand Down
7 changes: 6 additions & 1 deletion src/components/MultiGestureCanvas/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ type OnScaleChangedCallback = (zoomScale: number) => void;
/** Triggered when the canvas is tapped (single tap) */
type OnTapCallback = () => void;

/** Triggered when the swipe down gesture on canvas occurs */
type OnSwipeDownCallback = () => void;

/** Types used of variables used within the MultiGestureCanvas component and it's hooks */
type MultiGestureCanvasVariables = {
canvasSize: CanvasSize;
Expand All @@ -32,6 +35,7 @@ type MultiGestureCanvasVariables = {
minContentScale: number;
maxContentScale: number;
shouldDisableTransformationGestures: SharedValue<boolean>;
isSwipingDownToClose: SharedValue<boolean>;
zoomScale: SharedValue<number>;
totalScale: SharedValue<number>;
pinchScale: SharedValue<number>;
Expand All @@ -45,6 +49,7 @@ type MultiGestureCanvasVariables = {
reset: (animated: boolean, callback: () => void) => void;
onTap: OnTapCallback | undefined;
onScaleChanged: OnScaleChangedCallback | undefined;
onSwipeDown: OnSwipeDownCallback | undefined;
};

export type {CanvasSize, ContentSize, ZoomRange, OnScaleChangedCallback, OnTapCallback, MultiGestureCanvasVariables};
export type {CanvasSize, ContentSize, ZoomRange, OnScaleChangedCallback, OnTapCallback, MultiGestureCanvasVariables, OnSwipeDownCallback};
89 changes: 78 additions & 11 deletions src/components/MultiGestureCanvas/usePanGesture.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/* eslint-disable no-param-reassign */
import {Dimensions} from 'react-native';
import type {PanGesture} from 'react-native-gesture-handler';
import {Gesture} from 'react-native-gesture-handler';
import {useDerivedValue, useSharedValue, useWorkletCallback, withDecay, withSpring} from 'react-native-reanimated';
import {runOnJS, useDerivedValue, useSharedValue, useWorkletCallback, withDecay, withSpring} from 'react-native-reanimated';
import {SPRING_CONFIG} from './constants';
import type {MultiGestureCanvasVariables} from './types';
import * as MultiGestureCanvasUtils from './utils';
Expand All @@ -10,10 +11,24 @@ import * as MultiGestureCanvasUtils from './utils';
// We're using a "withDecay" animation to smoothly phase out the pan animation
// https://docs.swmansion.com/react-native-reanimated/docs/animations/withDecay/
const PAN_DECAY_DECELARATION = 0.9915;
const SCREEN_HEIGHT = Dimensions.get('screen').height;
const SNAP_POINT = SCREEN_HEIGHT / 4;
const SNAP_POINT_HIDDEN = SCREEN_HEIGHT / 1.2;

type UsePanGestureProps = Pick<
MultiGestureCanvasVariables,
'canvasSize' | 'contentSize' | 'zoomScale' | 'totalScale' | 'offsetX' | 'offsetY' | 'panTranslateX' | 'panTranslateY' | 'shouldDisableTransformationGestures' | 'stopAnimation'
| 'canvasSize'
| 'contentSize'
| 'zoomScale'
| 'totalScale'
| 'offsetX'
| 'offsetY'
| 'panTranslateX'
| 'panTranslateY'
| 'shouldDisableTransformationGestures'
| 'stopAnimation'
| 'onSwipeDown'
| 'isSwipingDownToClose'
>;

const usePanGesture = ({
Expand All @@ -27,16 +42,24 @@ const usePanGesture = ({
panTranslateY,
shouldDisableTransformationGestures,
stopAnimation,
isSwipingDownToClose,
onSwipeDown,
}: UsePanGestureProps): PanGesture => {
// The content size after fitting it to the canvas and zooming
const zoomedContentWidth = useDerivedValue(() => contentSize.width * totalScale.value, [contentSize.width]);
const zoomedContentHeight = useDerivedValue(() => contentSize.height * totalScale.value, [contentSize.height]);

// Used to track previous touch position for the "swipe down to close" gesture
const previousTouch = useSharedValue<{x: number; y: number} | null>(null);

// Velocity of the pan gesture
// We need to keep track of the velocity to properly phase out/decay the pan animation
const panVelocityX = useSharedValue(0);
const panVelocityY = useSharedValue(0);

// Disable "swipe down to close" gesture when content is bigger than the canvas
const enableSwipeDownToClose = useDerivedValue(() => canvasSize.height < zoomedContentHeight.value, [canvasSize.height]);

// Calculates bounds of the scaled content
// Can we pan left/right/up/down
// Can be used to limit gesture or implementing tension effect
Expand Down Expand Up @@ -113,8 +136,22 @@ const usePanGesture = ({
});
}
} else {
// Animated back to the boundary
offsetY.value = withSpring(clampedOffset.y, SPRING_CONFIG);
const finalTranslateY = offsetY.value + panVelocityY.value * 0.2;

if (finalTranslateY > SNAP_POINT && zoomScale.value <= 1) {
offsetY.value = withSpring(SNAP_POINT_HIDDEN, SPRING_CONFIG, () => {
isSwipingDownToClose.value = false;
});

if (onSwipeDown) {
runOnJS(onSwipeDown)();
}
} else {
// Animated back to the boundary
offsetY.value = withSpring(clampedOffset.y, SPRING_CONFIG, () => {
isSwipingDownToClose.value = false;
});
}
}

// Reset velocity variables after we finished the pan gesture
Expand All @@ -125,14 +162,36 @@ const usePanGesture = ({
const panGesture = Gesture.Pan()
.manualActivation(true)
.averageTouches(true)
// eslint-disable-next-line @typescript-eslint/naming-convention
.onTouchesMove((_evt, state) => {
.onTouchesUp(() => {
previousTouch.value = null;
})
.onTouchesMove((evt, state) => {
// We only allow panning when the content is zoomed in
if (zoomScale.value <= 1 || shouldDisableTransformationGestures.value) {
return;
if (zoomScale.value > 1 && !shouldDisableTransformationGestures.value) {
state.activate();
}

state.activate();
// TODO: this needs tuning to work properly
if (!shouldDisableTransformationGestures.value && zoomScale.value === 1 && previousTouch.value !== null) {
const velocityX = Math.abs(evt.allTouches[0].x - previousTouch.value.x);
const velocityY = evt.allTouches[0].y - previousTouch.value.y;

if (Math.abs(velocityY) > velocityX && velocityY > 20) {
state.activate();

isSwipingDownToClose.value = true;
previousTouch.value = null;

return;
}
}

if (previousTouch.value === null) {
previousTouch.value = {
x: evt.allTouches[0].x,
y: evt.allTouches[0].y,
};
}
})
.onStart(() => {
stopAnimation();
Expand All @@ -147,15 +206,23 @@ const usePanGesture = ({
panVelocityX.value = evt.velocityX;
panVelocityY.value = evt.velocityY;

panTranslateX.value += evt.changeX;
panTranslateY.value += evt.changeY;
if (!isSwipingDownToClose.value) {
panTranslateX.value += evt.changeX;
}

if (enableSwipeDownToClose.value || isSwipingDownToClose.value) {
panTranslateY.value += evt.changeY;
}
})
.onEnd(() => {
// Add pan translation to total offset and reset gesture variables
offsetX.value += panTranslateX.value;
offsetY.value += panTranslateY.value;

// Reset pan gesture variables
panTranslateX.value = 0;
panTranslateY.value = 0;
previousTouch.value = null;

// If we are swiping (in the pager), we don't want to return to boundaries
if (shouldDisableTransformationGestures.value) {
Expand Down

0 comments on commit dbcdbce

Please sign in to comment.