1
1
/* eslint-disable no-param-reassign */
2
+ import { Dimensions } from 'react-native' ;
2
3
import type { PanGesture } from 'react-native-gesture-handler' ;
3
4
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' ;
5
6
import { SPRING_CONFIG } from './constants' ;
6
7
import type { MultiGestureCanvasVariables } from './types' ;
7
8
import * as MultiGestureCanvasUtils from './utils' ;
@@ -10,10 +11,24 @@ import * as MultiGestureCanvasUtils from './utils';
10
11
// We're using a "withDecay" animation to smoothly phase out the pan animation
11
12
// https://docs.swmansion.com/react-native-reanimated/docs/animations/withDecay/
12
13
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 ;
13
17
14
18
type UsePanGestureProps = Pick <
15
19
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'
17
32
> ;
18
33
19
34
const usePanGesture = ( {
@@ -27,16 +42,24 @@ const usePanGesture = ({
27
42
panTranslateY,
28
43
shouldDisableTransformationGestures,
29
44
stopAnimation,
45
+ isSwipingDownToClose,
46
+ onSwipeDown,
30
47
} : UsePanGestureProps ) : PanGesture => {
31
48
// The content size after fitting it to the canvas and zooming
32
49
const zoomedContentWidth = useDerivedValue ( ( ) => contentSize . width * totalScale . value , [ contentSize . width ] ) ;
33
50
const zoomedContentHeight = useDerivedValue ( ( ) => contentSize . height * totalScale . value , [ contentSize . height ] ) ;
34
51
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
+
35
55
// Velocity of the pan gesture
36
56
// We need to keep track of the velocity to properly phase out/decay the pan animation
37
57
const panVelocityX = useSharedValue ( 0 ) ;
38
58
const panVelocityY = useSharedValue ( 0 ) ;
39
59
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
+
40
63
// Calculates bounds of the scaled content
41
64
// Can we pan left/right/up/down
42
65
// Can be used to limit gesture or implementing tension effect
@@ -113,8 +136,22 @@ const usePanGesture = ({
113
136
} ) ;
114
137
}
115
138
} 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
+ }
118
155
}
119
156
120
157
// Reset velocity variables after we finished the pan gesture
@@ -125,14 +162,36 @@ const usePanGesture = ({
125
162
const panGesture = Gesture . Pan ( )
126
163
. manualActivation ( true )
127
164
. 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 ) => {
130
169
// 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 ( ) ;
133
172
}
134
173
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
+ }
136
195
} )
137
196
. onStart ( ( ) => {
138
197
stopAnimation ( ) ;
@@ -147,15 +206,23 @@ const usePanGesture = ({
147
206
panVelocityX . value = evt . velocityX ;
148
207
panVelocityY . value = evt . velocityY ;
149
208
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
+ }
152
216
} )
153
217
. onEnd ( ( ) => {
154
218
// Add pan translation to total offset and reset gesture variables
155
219
offsetX . value += panTranslateX . value ;
156
220
offsetY . value += panTranslateY . value ;
221
+
222
+ // Reset pan gesture variables
157
223
panTranslateX . value = 0 ;
158
224
panTranslateY . value = 0 ;
225
+ previousTouch . value = null ;
159
226
160
227
// If we are swiping (in the pager), we don't want to return to boundaries
161
228
if ( shouldDisableTransformationGestures . value ) {
0 commit comments