Skip to content

Commit dbcdbce

Browse files
authored
Merge pull request #37080 from margelo/feat/swipe-down-to-close
feat: swipe down to close
2 parents 280ea03 + 44efead commit dbcdbce

File tree

7 files changed

+109
-15
lines changed

7 files changed

+109
-15
lines changed

src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ type AttachmentCarouselPagerContextValue = {
2626
isScrollEnabled: SharedValue<boolean>;
2727
onTap: () => void;
2828
onScaleChanged: (scale: number) => void;
29+
onSwipeDown: () => void;
2930
};
3031

3132
const AttachmentCarouselPagerContext = createContext<AttachmentCarouselPagerContextValue | null>(null);

src/components/Attachments/AttachmentCarousel/Pager/index.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,15 @@ type AttachmentCarouselPagerProps = {
4242
* @param showArrows If set, it will show/hide the arrows. If not set, it will toggle the arrows.
4343
*/
4444
onRequestToggleArrows: (showArrows?: boolean) => void;
45+
46+
/** A callback that is called when swipe-down-to-close gesture happens */
47+
onClose: () => void;
4548
};
4649

47-
function AttachmentCarouselPager({items, activeSource, initialPage, onPageSelected, onRequestToggleArrows}: AttachmentCarouselPagerProps, ref: ForwardedRef<AttachmentCarouselPagerHandle>) {
50+
function AttachmentCarouselPager(
51+
{items, activeSource, initialPage, onPageSelected, onRequestToggleArrows, onClose}: AttachmentCarouselPagerProps,
52+
ref: ForwardedRef<AttachmentCarouselPagerHandle>,
53+
) {
4854
const styles = useThemeStyles();
4955
const pagerRef = useRef<PagerView>(null);
5056

@@ -114,9 +120,10 @@ function AttachmentCarouselPager({items, activeSource, initialPage, onPageSelect
114120
isScrollEnabled,
115121
pagerRef,
116122
onTap: handleTap,
123+
onSwipeDown: onClose,
117124
onScaleChanged: handleScaleChange,
118125
}),
119-
[pagerItems, activePageIndex, isPagerScrolling, isScrollEnabled, handleTap, handleScaleChange],
126+
[pagerItems, activePageIndex, isPagerScrolling, isScrollEnabled, handleTap, onClose, handleScaleChange],
120127
);
121128

122129
const animatedProps = useAnimatedProps(() => ({

src/components/Attachments/AttachmentCarousel/index.native.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
102102
[setShouldShowArrows],
103103
);
104104

105+
const goBack = useCallback(() => {
106+
Navigation.goBack();
107+
}, []);
108+
105109
return (
106110
<View style={[styles.flex1, styles.attachmentCarouselContainer]}>
107111
{page == null ? (
@@ -133,6 +137,7 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source,
133137
activeSource={activeSource}
134138
onRequestToggleArrows={toggleArrows}
135139
onPageSelected={({nativeEvent: {position: newPage}}) => updatePage(newPage)}
140+
onClose={goBack}
136141
ref={pagerRef}
137142
/>
138143
</>

src/components/Lightbox/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ function Lightbox({isAuthTokenRequired = false, uri, onScaleChanged: onScaleChan
5959
activePage,
6060
onTap,
6161
onScaleChanged: onScaleChangedContext,
62+
onSwipeDown,
6263
pagerRef,
6364
} = useMemo(() => {
6465
if (attachmentCarouselPagerContext === null) {
@@ -70,6 +71,7 @@ function Lightbox({isAuthTokenRequired = false, uri, onScaleChanged: onScaleChan
7071
activePage: 0,
7172
onTap: () => {},
7273
onScaleChanged: () => {},
74+
onSwipeDown: () => {},
7375
pagerRef: undefined,
7476
};
7577
}
@@ -212,6 +214,7 @@ function Lightbox({isAuthTokenRequired = false, uri, onScaleChanged: onScaleChan
212214
shouldDisableTransformationGestures={isPagerScrolling}
213215
onTap={onTap}
214216
onScaleChanged={scaleChange}
217+
onSwipeDown={onSwipeDown}
215218
>
216219
<Image
217220
source={{uri}}

src/components/MultiGestureCanvas/index.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import useStyleUtils from '@hooks/useStyleUtils';
1010
import useThemeStyles from '@hooks/useThemeStyles';
1111
import type ChildrenProps from '@src/types/utils/ChildrenProps';
1212
import {DEFAULT_ZOOM_RANGE, SPRING_CONFIG, ZOOM_RANGE_BOUNCE_FACTORS} from './constants';
13-
import type {CanvasSize, ContentSize, OnScaleChangedCallback, OnTapCallback, ZoomRange} from './types';
13+
import type {CanvasSize, ContentSize, OnScaleChangedCallback, OnSwipeDownCallback, OnTapCallback, ZoomRange} from './types';
1414
import usePanGesture from './usePanGesture';
1515
import usePinchGesture from './usePinchGesture';
1616
import useTapGestures from './useTapGestures';
@@ -47,6 +47,8 @@ type MultiGestureCanvasProps = ChildrenProps & {
4747

4848
/** Handles scale changed event */
4949
onTap?: OnTapCallback;
50+
51+
onSwipeDown?: OnSwipeDownCallback;
5052
};
5153

5254
function MultiGestureCanvas({
@@ -59,6 +61,7 @@ function MultiGestureCanvas({
5961
shouldDisableTransformationGestures: shouldDisableTransformationGesturesProp,
6062
onTap,
6163
onScaleChanged,
64+
onSwipeDown,
6265
}: MultiGestureCanvasProps) {
6366
const styles = useThemeStyles();
6467
const StyleUtils = useStyleUtils();
@@ -88,6 +91,7 @@ function MultiGestureCanvas({
8891

8992
const panTranslateX = useSharedValue(0);
9093
const panTranslateY = useSharedValue(0);
94+
const isSwipingDownToClose = useSharedValue(false);
9195
const panGestureRef = useRef(Gesture.Pan());
9296

9397
const pinchScale = useSharedValue(1);
@@ -172,6 +176,8 @@ function MultiGestureCanvas({
172176
panTranslateY,
173177
stopAnimation,
174178
shouldDisableTransformationGestures,
179+
isSwipingDownToClose,
180+
onSwipeDown,
175181
})
176182
.simultaneousWithExternalGesture(...panGestureSimultaneousList)
177183
.withRef(panGestureRef);

src/components/MultiGestureCanvas/types.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ type OnScaleChangedCallback = (zoomScale: number) => void;
2424
/** Triggered when the canvas is tapped (single tap) */
2525
type OnTapCallback = () => void;
2626

27+
/** Triggered when the swipe down gesture on canvas occurs */
28+
type OnSwipeDownCallback = () => void;
29+
2730
/** Types used of variables used within the MultiGestureCanvas component and it's hooks */
2831
type MultiGestureCanvasVariables = {
2932
canvasSize: CanvasSize;
@@ -32,6 +35,7 @@ type MultiGestureCanvasVariables = {
3235
minContentScale: number;
3336
maxContentScale: number;
3437
shouldDisableTransformationGestures: SharedValue<boolean>;
38+
isSwipingDownToClose: SharedValue<boolean>;
3539
zoomScale: SharedValue<number>;
3640
totalScale: SharedValue<number>;
3741
pinchScale: SharedValue<number>;
@@ -45,6 +49,7 @@ type MultiGestureCanvasVariables = {
4549
reset: (animated: boolean, callback: () => void) => void;
4650
onTap: OnTapCallback | undefined;
4751
onScaleChanged: OnScaleChangedCallback | undefined;
52+
onSwipeDown: OnSwipeDownCallback | undefined;
4853
};
4954

50-
export type {CanvasSize, ContentSize, ZoomRange, OnScaleChangedCallback, OnTapCallback, MultiGestureCanvasVariables};
55+
export type {CanvasSize, ContentSize, ZoomRange, OnScaleChangedCallback, OnTapCallback, MultiGestureCanvasVariables, OnSwipeDownCallback};

src/components/MultiGestureCanvas/usePanGesture.ts

Lines changed: 78 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
/* eslint-disable no-param-reassign */
2+
import {Dimensions} from 'react-native';
23
import type {PanGesture} from 'react-native-gesture-handler';
34
import {Gesture} from 'react-native-gesture-handler';
4-
import {useDerivedValue, useSharedValue, useWorkletCallback, withDecay, withSpring} from 'react-native-reanimated';
5+
import {runOnJS, useDerivedValue, useSharedValue, useWorkletCallback, withDecay, withSpring} from 'react-native-reanimated';
56
import {SPRING_CONFIG} from './constants';
67
import type {MultiGestureCanvasVariables} from './types';
78
import * as MultiGestureCanvasUtils from './utils';
@@ -10,10 +11,24 @@ import * as MultiGestureCanvasUtils from './utils';
1011
// We're using a "withDecay" animation to smoothly phase out the pan animation
1112
// https://docs.swmansion.com/react-native-reanimated/docs/animations/withDecay/
1213
const PAN_DECAY_DECELARATION = 0.9915;
14+
const SCREEN_HEIGHT = Dimensions.get('screen').height;
15+
const SNAP_POINT = SCREEN_HEIGHT / 4;
16+
const SNAP_POINT_HIDDEN = SCREEN_HEIGHT / 1.2;
1317

1418
type UsePanGestureProps = Pick<
1519
MultiGestureCanvasVariables,
16-
'canvasSize' | 'contentSize' | 'zoomScale' | 'totalScale' | 'offsetX' | 'offsetY' | 'panTranslateX' | 'panTranslateY' | 'shouldDisableTransformationGestures' | 'stopAnimation'
20+
| 'canvasSize'
21+
| 'contentSize'
22+
| 'zoomScale'
23+
| 'totalScale'
24+
| 'offsetX'
25+
| 'offsetY'
26+
| 'panTranslateX'
27+
| 'panTranslateY'
28+
| 'shouldDisableTransformationGestures'
29+
| 'stopAnimation'
30+
| 'onSwipeDown'
31+
| 'isSwipingDownToClose'
1732
>;
1833

1934
const usePanGesture = ({
@@ -27,16 +42,24 @@ const usePanGesture = ({
2742
panTranslateY,
2843
shouldDisableTransformationGestures,
2944
stopAnimation,
45+
isSwipingDownToClose,
46+
onSwipeDown,
3047
}: UsePanGestureProps): PanGesture => {
3148
// The content size after fitting it to the canvas and zooming
3249
const zoomedContentWidth = useDerivedValue(() => contentSize.width * totalScale.value, [contentSize.width]);
3350
const zoomedContentHeight = useDerivedValue(() => contentSize.height * totalScale.value, [contentSize.height]);
3451

52+
// Used to track previous touch position for the "swipe down to close" gesture
53+
const previousTouch = useSharedValue<{x: number; y: number} | null>(null);
54+
3555
// Velocity of the pan gesture
3656
// We need to keep track of the velocity to properly phase out/decay the pan animation
3757
const panVelocityX = useSharedValue(0);
3858
const panVelocityY = useSharedValue(0);
3959

60+
// Disable "swipe down to close" gesture when content is bigger than the canvas
61+
const enableSwipeDownToClose = useDerivedValue(() => canvasSize.height < zoomedContentHeight.value, [canvasSize.height]);
62+
4063
// Calculates bounds of the scaled content
4164
// Can we pan left/right/up/down
4265
// Can be used to limit gesture or implementing tension effect
@@ -113,8 +136,22 @@ const usePanGesture = ({
113136
});
114137
}
115138
} else {
116-
// Animated back to the boundary
117-
offsetY.value = withSpring(clampedOffset.y, SPRING_CONFIG);
139+
const finalTranslateY = offsetY.value + panVelocityY.value * 0.2;
140+
141+
if (finalTranslateY > SNAP_POINT && zoomScale.value <= 1) {
142+
offsetY.value = withSpring(SNAP_POINT_HIDDEN, SPRING_CONFIG, () => {
143+
isSwipingDownToClose.value = false;
144+
});
145+
146+
if (onSwipeDown) {
147+
runOnJS(onSwipeDown)();
148+
}
149+
} else {
150+
// Animated back to the boundary
151+
offsetY.value = withSpring(clampedOffset.y, SPRING_CONFIG, () => {
152+
isSwipingDownToClose.value = false;
153+
});
154+
}
118155
}
119156

120157
// Reset velocity variables after we finished the pan gesture
@@ -125,14 +162,36 @@ const usePanGesture = ({
125162
const panGesture = Gesture.Pan()
126163
.manualActivation(true)
127164
.averageTouches(true)
128-
// eslint-disable-next-line @typescript-eslint/naming-convention
129-
.onTouchesMove((_evt, state) => {
165+
.onTouchesUp(() => {
166+
previousTouch.value = null;
167+
})
168+
.onTouchesMove((evt, state) => {
130169
// We only allow panning when the content is zoomed in
131-
if (zoomScale.value <= 1 || shouldDisableTransformationGestures.value) {
132-
return;
170+
if (zoomScale.value > 1 && !shouldDisableTransformationGestures.value) {
171+
state.activate();
133172
}
134173

135-
state.activate();
174+
// TODO: this needs tuning to work properly
175+
if (!shouldDisableTransformationGestures.value && zoomScale.value === 1 && previousTouch.value !== null) {
176+
const velocityX = Math.abs(evt.allTouches[0].x - previousTouch.value.x);
177+
const velocityY = evt.allTouches[0].y - previousTouch.value.y;
178+
179+
if (Math.abs(velocityY) > velocityX && velocityY > 20) {
180+
state.activate();
181+
182+
isSwipingDownToClose.value = true;
183+
previousTouch.value = null;
184+
185+
return;
186+
}
187+
}
188+
189+
if (previousTouch.value === null) {
190+
previousTouch.value = {
191+
x: evt.allTouches[0].x,
192+
y: evt.allTouches[0].y,
193+
};
194+
}
136195
})
137196
.onStart(() => {
138197
stopAnimation();
@@ -147,15 +206,23 @@ const usePanGesture = ({
147206
panVelocityX.value = evt.velocityX;
148207
panVelocityY.value = evt.velocityY;
149208

150-
panTranslateX.value += evt.changeX;
151-
panTranslateY.value += evt.changeY;
209+
if (!isSwipingDownToClose.value) {
210+
panTranslateX.value += evt.changeX;
211+
}
212+
213+
if (enableSwipeDownToClose.value || isSwipingDownToClose.value) {
214+
panTranslateY.value += evt.changeY;
215+
}
152216
})
153217
.onEnd(() => {
154218
// Add pan translation to total offset and reset gesture variables
155219
offsetX.value += panTranslateX.value;
156220
offsetY.value += panTranslateY.value;
221+
222+
// Reset pan gesture variables
157223
panTranslateX.value = 0;
158224
panTranslateY.value = 0;
225+
previousTouch.value = null;
159226

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

0 commit comments

Comments
 (0)