Skip to content

Commit 510905e

Browse files
Add a new gesture relation - blocksHandlers - working like reversed waitFor (#2627)
## Description Introduces a new relation between gestures, which allows to set one gesture recognizer as blocking for another one. It's basically the same as `waitFor` but in the other direction - `waitFor` allows to specify which gestures must fail before this one can activate. The new prop allows to specify which gestures should require this one to fail before they can activate. For the name, I think we've settled at `blocksHandlers` for the old API and `.blocksExternalGesture()` for the new API. There's no need to add another type of composition since it accomplishes the same thing as `Gesture.Exclusive`, and is inherently a between-components relation. ## Test plan Tested on the Example app Since it relies on already existing logic, I assume it will work correctly. I would like to do more testing nonetheless. Also needs to be tested on physical iOS device with multi-touch gestures. --------- Co-authored-by: Kacper Kapuściak <[email protected]>
1 parent 25ba4b8 commit 510905e

File tree

9 files changed

+289
-37
lines changed

9 files changed

+289
-37
lines changed

android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerInteractionManager.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import com.swmansion.gesturehandler.core.NativeViewGestureHandler
99
class RNGestureHandlerInteractionManager : GestureHandlerInteractionController {
1010
private val waitForRelations = SparseArray<IntArray>()
1111
private val simultaneousRelations = SparseArray<IntArray>()
12+
private val blockingRelations = SparseArray<IntArray>()
13+
1214
fun dropRelationsForHandlerWithTag(handlerTag: Int) {
1315
waitForRelations.remove(handlerTag)
1416
simultaneousRelations.remove(handlerTag)
@@ -33,6 +35,10 @@ class RNGestureHandlerInteractionManager : GestureHandlerInteractionController {
3335
val tags = convertHandlerTagsArray(config, KEY_SIMULTANEOUS_HANDLERS)
3436
simultaneousRelations.put(handler.tag, tags)
3537
}
38+
if (config.hasKey(KEY_BLOCKS_HANDLERS)) {
39+
val tags = convertHandlerTagsArray(config, KEY_BLOCKS_HANDLERS)
40+
blockingRelations.put(handler.tag, tags)
41+
}
3642
}
3743

3844
override fun shouldWaitForHandlerFailure(handler: GestureHandler<*>, otherHandler: GestureHandler<*>) =
@@ -41,7 +47,7 @@ class RNGestureHandlerInteractionManager : GestureHandlerInteractionController {
4147
override fun shouldRequireHandlerToWaitForFailure(
4248
handler: GestureHandler<*>,
4349
otherHandler: GestureHandler<*>,
44-
) = false
50+
) = blockingRelations[handler.tag]?.any { tag -> tag == otherHandler.tag } ?: false
4551

4652
override fun shouldHandlerBeCancelledBy(handler: GestureHandler<*>, otherHandler: GestureHandler<*>): Boolean {
4753
if (otherHandler is NativeViewGestureHandler) {
@@ -63,5 +69,6 @@ class RNGestureHandlerInteractionManager : GestureHandlerInteractionController {
6369
companion object {
6470
private const val KEY_WAIT_FOR = "waitFor"
6571
private const val KEY_SIMULTANEOUS_HANDLERS = "simultaneousHandlers"
72+
private const val KEY_BLOCKS_HANDLERS = "blocksHandlers"
6673
}
6774
}

docs/docs/fundamentals/gesture-composition.md

Lines changed: 207 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
id: gesture-composition
3-
title: Composing gestures
4-
sidebar_label: Composing gestures
3+
title: Gesture composition & interactions
4+
sidebar_label: Gesture composition & interactions
55
sidebar_position: 4
66
---
77

@@ -200,6 +200,209 @@ function App() {
200200
}
201201
```
202202

203-
## Composition vs `simultaneousWithExternalGesture` and `requireExternalGestureToFail`
203+
# Cross-component interactions
204204

205-
You may have noticed that gesture composition described above requires you to mount all of the composed gestures under a single `GestureDetector`, effectively attaching them to the same underlying component. If you want to make a relation between gestures that are attached to separate `GestureDetectors`, we have a separate mechanism for that: `simultaneousWithExternalGesture` and `requireExternalGestureToFail` methods that are available on every base gesture. They work exactly the same way as `simultaneousHandlers` and `waitFor` on gesture handlers, that is they just mark the relation between the gestures without joining them into single object.
205+
You may have noticed that gesture composition described above requires you to mount all of the composed gestures under a single `GestureDetector`, effectively attaching them to the same underlying component. You can customize how gestures interact with each other across multiple components in a couple of ways:
206+
207+
## requireExternalGestureToFail
208+
209+
`requireExternalGestureToFail` allows to delay activation of the handler until all handlers passed as arguments to this method fail (or don't begin at all).
210+
211+
For example, you may want to have two nested components, both of them can be tapped by the user to trigger different actions: outer view requires one tap, but the inner one requires 2 taps. If you don't want the first tap on the inner view to activate the outer handler, you must make the outer gesture wait until the inner one fails:
212+
213+
```jsx
214+
import React from 'react';
215+
import { View, StyleSheet } from 'react-native';
216+
import {
217+
GestureDetector,
218+
Gesture,
219+
GestureHandlerRootView,
220+
} from 'react-native-gesture-handler';
221+
222+
export default function Example() {
223+
const innerTap = Gesture.Tap()
224+
.numberOfTaps(2)
225+
.onStart(() => {
226+
console.log('inner tap');
227+
});
228+
229+
const outerTap = Gesture.Tap()
230+
.onStart(() => {
231+
console.log('outer tap');
232+
})
233+
.requireExternalGestureToFail(innerTap);
234+
235+
return (
236+
<GestureHandlerRootView style={styles.container}>
237+
<GestureDetector gesture={outerTap}>
238+
<View style={styles.outer}>
239+
<GestureDetector gesture={innerTap}>
240+
<View style={styles.inner} />
241+
</GestureDetector>
242+
</View>
243+
</GestureDetector>
244+
</GestureHandlerRootView>
245+
);
246+
}
247+
248+
const styles = StyleSheet.create({
249+
container: {
250+
flex: 1,
251+
alignItems: 'center',
252+
justifyContent: 'center',
253+
},
254+
outer: {
255+
width: 250,
256+
height: 250,
257+
backgroundColor: 'lightblue',
258+
},
259+
inner: {
260+
width: 100,
261+
height: 100,
262+
backgroundColor: 'blue',
263+
alignSelf: 'center',
264+
},
265+
});
266+
```
267+
268+
## blocksExternalGesture
269+
270+
`blocksExternalGesture` works similarily to `requireExternalGestureToFail` but the direction of the relation is reversed - instead of being one-to-many relation, it's many-to-one. It's especially useful for making lists where the `ScrollView` component needs to wait for every gesture underneath it. All that's required to do is to pass a ref, for example:
271+
272+
```jsx
273+
import React, { useRef } from 'react';
274+
import { StyleSheet } from 'react-native';
275+
import {
276+
GestureDetector,
277+
Gesture,
278+
GestureHandlerRootView,
279+
ScrollView,
280+
} from 'react-native-gesture-handler';
281+
import Animated, {
282+
useSharedValue,
283+
useAnimatedStyle,
284+
withTiming,
285+
} from 'react-native-reanimated';
286+
287+
const ITEMS = ['red', 'green', 'blue', 'yellow'];
288+
289+
function Item({ backgroundColor, scrollRef }) {
290+
const scale = useSharedValue(1);
291+
const zIndex = useSharedValue(1);
292+
293+
const pinch = Gesture.Pinch()
294+
.blocksExternalGesture(scrollRef)
295+
.onBegin(() => {
296+
zIndex.value = 100;
297+
})
298+
.onChange((e) => {
299+
scale.value *= e.scaleChange;
300+
})
301+
.onFinalize(() => {
302+
scale.value = withTiming(1, undefined, (finished) => {
303+
if (finished) {
304+
zIndex.value = 1;
305+
}
306+
});
307+
});
308+
309+
const animatedStyles = useAnimatedStyle(() => ({
310+
transform: [{ scale: scale.value }],
311+
zIndex: zIndex.value,
312+
}));
313+
314+
return (
315+
<GestureDetector gesture={pinch}>
316+
<Animated.View
317+
style={[
318+
{ backgroundColor: backgroundColor },
319+
styles.item,
320+
animatedStyles,
321+
]}
322+
/>
323+
</GestureDetector>
324+
);
325+
}
326+
327+
export default function Example() {
328+
const scrollRef = useRef();
329+
330+
return (
331+
<GestureHandlerRootView style={styles.container}>
332+
<ScrollView style={styles.container} ref={scrollRef}>
333+
{ITEMS.map((item) => (
334+
<Item backgroundColor={item} key={item} scrollRef={scrollRef} />
335+
))}
336+
</ScrollView>
337+
</GestureHandlerRootView>
338+
);
339+
}
340+
341+
const styles = StyleSheet.create({
342+
container: {
343+
flex: 1,
344+
},
345+
item: {
346+
flex: 1,
347+
aspectRatio: 1,
348+
},
349+
});
350+
```
351+
352+
## simultaneousWithExternalGesture
353+
354+
`simultaneousWithExternalGesture` allows gestures across different components to be recognized simultaneously. For example, you may want to have two nested views, both with tap gesture attached. Both of them require one tap, but tapping the inner one should also activate the gesture attached to the outer view:
355+
356+
```jsx
357+
import React from 'react';
358+
import { View, StyleSheet } from 'react-native';
359+
import {
360+
GestureDetector,
361+
Gesture,
362+
GestureHandlerRootView,
363+
} from 'react-native-gesture-handler';
364+
365+
export default function Example() {
366+
const innerTap = Gesture.Tap()
367+
.onStart(() => {
368+
console.log('inner tap');
369+
});
370+
371+
const outerTap = Gesture.Tap()
372+
.onStart(() => {
373+
console.log('outer tap');
374+
})
375+
.simultaneousWithExternalGesture(innerTap);
376+
377+
return (
378+
<GestureHandlerRootView style={styles.container}>
379+
<GestureDetector gesture={outerTap}>
380+
<View style={styles.outer}>
381+
<GestureDetector gesture={innerTap}>
382+
<View style={styles.inner} />
383+
</GestureDetector>
384+
</View>
385+
</GestureDetector>
386+
</GestureHandlerRootView>
387+
);
388+
}
389+
390+
const styles = StyleSheet.create({
391+
container: {
392+
flex: 1,
393+
alignItems: 'center',
394+
justifyContent: 'center',
395+
},
396+
outer: {
397+
width: 250,
398+
height: 250,
399+
backgroundColor: 'lightblue',
400+
},
401+
inner: {
402+
width: 100,
403+
height: 100,
404+
backgroundColor: 'blue',
405+
alignSelf: 'center',
406+
},
407+
});
408+
```

docs/docs/gestures/_shared/base-gesture-config.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ Adds a gesture that should be recognized simultaneously with this one.
5757

5858
Adds a relation requiring another gesture to fail, before this one can activate.
5959

60+
### `blocksExternalGesture(otherGesture1, otherGesture2, ...)`
61+
62+
Adds a relation that makes other gestures wait with activation until this gesture fails (or doesn't start at all).
63+
6064
**IMPORTANT:** Note that this method only marks the relation between gestures, without [composing them](/docs/fundamentals/gesture-composition).[`GestureDetector`](/docs/gestures/gesture-detector) will not recognize the `otherGestures` and it needs to be added to another detector in order to be recognized.
6165

6266
### `activeCursor(value)` (**web only**)

ios/RNGestureHandler.m

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ @implementation RNGestureHandler {
6666
RNGestureHandlerState _state;
6767
RNManualActivationRecognizer *_manualActivationRecognizer;
6868
NSArray<NSNumber *> *_handlersToWaitFor;
69+
NSArray<NSNumber *> *_handlersThatShouldWait;
6970
NSArray<NSNumber *> *_simultaneousHandlers;
7071
RNGHHitSlop _hitSlop;
7172
uint16_t _eventCoalescingKey;
@@ -98,6 +99,7 @@ - (void)resetConfig
9899
_shouldCancelWhenOutside = NO;
99100
_handlersToWaitFor = nil;
100101
_simultaneousHandlers = nil;
102+
_handlersThatShouldWait = nil;
101103
_hitSlop = RNGHHitSlopEmpty;
102104
_needsPointerData = NO;
103105

@@ -109,6 +111,7 @@ - (void)configure:(NSDictionary *)config
109111
[self resetConfig];
110112
_handlersToWaitFor = [RCTConvert NSNumberArray:config[@"waitFor"]];
111113
_simultaneousHandlers = [RCTConvert NSNumberArray:config[@"simultaneousHandlers"]];
114+
_handlersThatShouldWait = [RCTConvert NSNumberArray:config[@"blocksHandlers"]];
112115

113116
id prop = config[@"enabled"];
114117
if (prop != nil) {
@@ -380,6 +383,14 @@ - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
380383
}
381384
}
382385

386+
if (handler != nil) {
387+
for (NSNumber *handlerTag in _handlersThatShouldWait) {
388+
if ([handler.tag isEqual:handlerTag]) {
389+
return YES;
390+
}
391+
}
392+
}
393+
383394
return NO;
384395
}
385396

src/handlers/gestureHandlerCommon.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ const commonProps = [
2323
'activeCursor',
2424
] as const;
2525

26-
const componentInteractionProps = ['waitFor', 'simultaneousHandlers'] as const;
26+
const componentInteractionProps = [
27+
'waitFor',
28+
'simultaneousHandlers',
29+
'blocksHandlers',
30+
] as const;
2731

2832
export const baseGestureHandlerProps = [
2933
...commonProps,
@@ -155,6 +159,7 @@ export type BaseGestureHandlerProps<
155159
id?: string;
156160
waitFor?: React.Ref<unknown> | React.Ref<unknown>[];
157161
simultaneousHandlers?: React.Ref<unknown> | React.Ref<unknown>[];
162+
blocksHandlers?: React.Ref<unknown> | React.Ref<unknown>[];
158163
testID?: string;
159164
cancelsTouchesInView?: boolean;
160165
// TODO(TS) - fix event types

src/handlers/gestures/GestureDetector.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,11 +193,17 @@ function attachHandlers({
193193
);
194194
}
195195

196+
let blocksHandlers: number[] = [];
197+
if (handler.config.blocksHandlers) {
198+
blocksHandlers = extractValidHandlerTags(handler.config.blocksHandlers);
199+
}
200+
196201
RNGestureHandlerModule.updateGestureHandler(
197202
handler.handlerTag,
198203
filterConfig(handler.config, ALLOWED_PROPS, {
199204
simultaneousHandlers: simultaneousWith,
200205
waitFor: requireToFail,
206+
blocksHandlers: blocksHandlers,
201207
})
202208
);
203209
}

src/handlers/gestures/gesture.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export interface BaseGestureConfig
4141
ref?: React.MutableRefObject<GestureType | undefined>;
4242
requireToFail?: GestureRef[];
4343
simultaneousWith?: GestureRef[];
44+
blocksHandlers?: GestureRef[];
4445
needsPointerData?: boolean;
4546
manualActivation?: boolean;
4647
runOnJS?: boolean;
@@ -144,7 +145,7 @@ export abstract class BaseGesture<
144145
}
145146

146147
private addDependency(
147-
key: 'simultaneousWith' | 'requireToFail',
148+
key: 'simultaneousWith' | 'requireToFail' | 'blocksHandlers',
148149
gesture: Exclude<GestureRef, number>
149150
) {
150151
const value = this.config[key];
@@ -275,6 +276,13 @@ export abstract class BaseGesture<
275276
return this;
276277
}
277278

279+
blocksExternalGesture(...gestures: Exclude<GestureRef, number>[]) {
280+
for (const gesture of gestures) {
281+
this.addDependency('blocksHandlers', gesture);
282+
}
283+
return this;
284+
}
285+
278286
withTestId(id: string) {
279287
this.config.testId = id;
280288
return this;

src/web/interfaces.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export interface Config extends Record<string, ConfigArgs> {
3232
enabled?: boolean;
3333
simultaneousHandlers?: Handler[] | null;
3434
waitFor?: Handler[] | null;
35+
blocksHandlers?: Handler[] | null;
3536
hitSlop?: HitSlop;
3637
shouldCancelWhenOutside?: boolean;
3738
userSelect?: UserSelect;

0 commit comments

Comments
 (0)