Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: swipe down to close #37080

Merged
merged 9 commits into from
Feb 26, 2024
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
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
Loading