Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import com.swmansion.gesturehandler.core.NativeViewGestureHandler
class RNGestureHandlerInteractionManager : GestureHandlerInteractionController {
private val waitForRelations = SparseArray<IntArray>()
private val simultaneousRelations = SparseArray<IntArray>()
private val requiredToFailByRelations = SparseArray<IntArray>()

fun dropRelationsForHandlerWithTag(handlerTag: Int) {
waitForRelations.remove(handlerTag)
simultaneousRelations.remove(handlerTag)
Expand All @@ -33,10 +35,15 @@ class RNGestureHandlerInteractionManager : GestureHandlerInteractionController {
val tags = convertHandlerTagsArray(config, KEY_SIMULTANEOUS_HANDLERS)
simultaneousRelations.put(handler.tag, tags)
}
if (config.hasKey(KEY_REQUIRED_BY_OTHER_TO_FAIL)) {
val tags = convertHandlerTagsArray(config, KEY_REQUIRED_BY_OTHER_TO_FAIL)
requiredToFailByRelations.put(handler.tag, tags)
}
}

override fun shouldWaitForHandlerFailure(handler: GestureHandler<*>, otherHandler: GestureHandler<*>) =
waitForRelations[handler.tag]?.any { tag -> tag == otherHandler.tag } ?: false
(waitForRelations[handler.tag]?.any { tag -> tag == otherHandler.tag } ?: false) ||
(requiredToFailByRelations[otherHandler.tag]?.any { tag -> tag == handler.tag } ?: false)

override fun shouldRequireHandlerToWaitForFailure(
handler: GestureHandler<*>,
Expand All @@ -63,5 +70,6 @@ class RNGestureHandlerInteractionManager : GestureHandlerInteractionController {
companion object {
private const val KEY_WAIT_FOR = "waitFor"
private const val KEY_SIMULTANEOUS_HANDLERS = "simultaneousHandlers"
private const val KEY_REQUIRED_BY_OTHER_TO_FAIL = "requiredToFailBy"
}
}
211 changes: 207 additions & 4 deletions docs/docs/fundamentals/gesture-composition.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
id: gesture-composition
title: Composing gestures
sidebar_label: Composing gestures
title: Gesture composition & interactions
sidebar_label: Gesture composition & interactions
sidebar_position: 4
---

Expand Down Expand Up @@ -200,6 +200,209 @@ function App() {
}
```

## Composition vs `simultaneousWithExternalGesture` and `requireExternalGestureToFail`
# Cross-component interactions

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.
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 wish to customize how gestures interact with each other across multiple components, there are different mechanisms for that.

## requireExternalGestureToFail

`requireExternalGestureToFail` allows for delaying activation of the handler until all handlers passed as arguments to this method fail (or don't begin at all).

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:

```jsx
import React from 'react';
import { View, StyleSheet } from 'react-native';
import {
GestureDetector,
Gesture,
GestureHandlerRootView,
} from 'react-native-gesture-handler';

export default function Example() {
const innerTap = Gesture.Tap()
.numberOfTaps(2)
.onStart(() => {
console.log('inner tap');
});

const outerTap = Gesture.Tap()
.onStart(() => {
console.log('outer tap');
})
.requireExternalGestureToFail(innerTap);

return (
<GestureHandlerRootView style={styles.container}>
<GestureDetector gesture={outerTap}>
<View style={styles.outer}>
<GestureDetector gesture={innerTap}>
<View style={styles.inner} />
</GestureDetector>
</View>
</GestureDetector>
</GestureHandlerRootView>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
outer: {
width: 250,
height: 250,
backgroundColor: 'lightblue',
},
inner: {
width: 100,
height: 100,
backgroundColor: 'blue',
alignSelf: 'center',
},
});
```

## requiredToFailByExternalGesture

`requiredToFailByExternalGesture` works similarily to `requireExternalGestureToFail` but the direction of the relation is reversed - insted of making the gesture it's called on wait for failure of gestures passed as arguments, it's making gestures passed as arguments wait for the gesture it's called on. It's especially usefull for making lists where the `ScrollView` component needs to wait for every gesture underneath it. All that's required is passing a ref of the `ScrollView` to the gesture object, for example:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first sentence is a complete gibberish :grief:

also, typos in insted and usefull

also, the last sentence I'd write something like:

All that's required to do is to pass a ref

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you take a look at 03878fd and 5cf17b9?


```jsx
import React, { useRef } from 'react';
import { StyleSheet } from 'react-native';
import {
GestureDetector,
Gesture,
GestureHandlerRootView,
ScrollView,
} from 'react-native-gesture-handler';
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
} from 'react-native-reanimated';

const ITEMS = ['red', 'green', 'blue', 'yellow'];

function Item({ backgroundColor, scrollRef }) {
const scale = useSharedValue(1);
const zIndex = useSharedValue(1);

const pinch = Gesture.Pinch()
.requiredToFailByExternalGesture(scrollRef)
.onBegin(() => {
zIndex.value = 100;
})
.onChange((e) => {
scale.value *= e.scaleChange;
})
.onFinalize(() => {
scale.value = withTiming(1, undefined, (finished) => {
if (finished) {
zIndex.value = 1;
}
});
});

const animatedStyles = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
zIndex: zIndex.value,
}));

return (
<GestureDetector gesture={pinch}>
<Animated.View
style={[
{ backgroundColor: backgroundColor },
styles.item,
animatedStyles,
]}
/>
</GestureDetector>
);
}

export default function Example() {
const scrollRef = useRef();

return (
<GestureHandlerRootView style={styles.container}>
<ScrollView style={styles.container} ref={scrollRef}>
{ITEMS.map((item) => (
<Item backgroundColor={item} key={item} scrollRef={scrollRef} />
))}
</ScrollView>
</GestureHandlerRootView>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
},
item: {
flex: 1,
aspectRatio: 1,
},
});
```

## simultaneousWithExternalGesture

`simultaneousWithExternalGesture` allows gestures across different components to be recognized simultaneously. We can modify the example from `requireExternalGestureToFail` to showcase this: let's say you have two nested views, again both with tap gesture attached. This time, both of them require one tap, but tapping the inner one should also activate the gesture attached to the outer view:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

People rarely read documentation from start to finish. They mostly skim the headings and code so refering to previous sections isn't that useful

It's better to repeat the full intent of the paragraph rather than refering to previous sections


```jsx
import React from 'react';
import { View, StyleSheet } from 'react-native';
import {
GestureDetector,
Gesture,
GestureHandlerRootView,
} from 'react-native-gesture-handler';

export default function Example() {
const innerTap = Gesture.Tap()
.onStart(() => {
console.log('inner tap');
});

const outerTap = Gesture.Tap()
.onStart(() => {
console.log('outer tap');
})
.simultaneousWithExternalGesture(innerTap);

return (
<GestureHandlerRootView style={styles.container}>
<GestureDetector gesture={outerTap}>
<View style={styles.outer}>
<GestureDetector gesture={innerTap}>
<View style={styles.inner} />
</GestureDetector>
</View>
</GestureDetector>
</GestureHandlerRootView>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
outer: {
width: 250,
height: 250,
backgroundColor: 'lightblue',
},
inner: {
width: 100,
height: 100,
backgroundColor: 'blue',
alignSelf: 'center',
},
});
```
4 changes: 4 additions & 0 deletions docs/docs/gestures/_shared/base-gesture-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ Adds a gesture that should be recognized simultaneously with this one.

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

### `requiredToFailByExternalGesture(otherGesture1, otherGesture2, ...)`

Adds a relation that makes other gestures wait with activation until this gesture fails (or doesn't start at all).

**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.

### `activeCursor(value)` (**web only**)
Expand Down
11 changes: 11 additions & 0 deletions ios/RNGestureHandler.m
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ @implementation RNGestureHandler {
RNGestureHandlerState _state;
RNManualActivationRecognizer *_manualActivationRecognizer;
NSArray<NSNumber *> *_handlersToWaitFor;
NSArray<NSNumber *> *_handlersThatShouldWait;
NSArray<NSNumber *> *_simultaneousHandlers;
RNGHHitSlop _hitSlop;
uint16_t _eventCoalescingKey;
Expand Down Expand Up @@ -98,6 +99,7 @@ - (void)resetConfig
_shouldCancelWhenOutside = NO;
_handlersToWaitFor = nil;
_simultaneousHandlers = nil;
_handlersThatShouldWait = nil;
_hitSlop = RNGHHitSlopEmpty;
_needsPointerData = NO;

Expand All @@ -109,6 +111,7 @@ - (void)configure:(NSDictionary *)config
[self resetConfig];
_handlersToWaitFor = [RCTConvert NSNumberArray:config[@"waitFor"]];
_simultaneousHandlers = [RCTConvert NSNumberArray:config[@"simultaneousHandlers"]];
_handlersThatShouldWait = [RCTConvert NSNumberArray:config[@"requiredToFailBy"]];

id prop = config[@"enabled"];
if (prop != nil) {
Expand Down Expand Up @@ -380,6 +383,14 @@ - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
}
}

if (handler != nil) {
for (NSNumber *handlerTag in _handlersThatShouldWait) {
if ([handler.tag isEqual:handlerTag]) {
return YES;
}
}
}

return NO;
}

Expand Down
7 changes: 6 additions & 1 deletion src/handlers/gestureHandlerCommon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ const commonProps = [
'activeCursor',
] as const;

const componentInteractionProps = ['waitFor', 'simultaneousHandlers'] as const;
const componentInteractionProps = [
'waitFor',
'simultaneousHandlers',
'requiredToFailBy',
] as const;

export const baseGestureHandlerProps = [
...commonProps,
Expand Down Expand Up @@ -155,6 +159,7 @@ export type BaseGestureHandlerProps<
id?: string;
waitFor?: React.Ref<unknown> | React.Ref<unknown>[];
simultaneousHandlers?: React.Ref<unknown> | React.Ref<unknown>[];
requiredToFailBy?: React.Ref<unknown> | React.Ref<unknown>[];
testID?: string;
cancelsTouchesInView?: boolean;
// TODO(TS) - fix event types
Expand Down
8 changes: 8 additions & 0 deletions src/handlers/gestures/GestureDetector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -197,11 +197,19 @@ function attachHandlers({
);
}

let requiredToFailBy: number[] = [];
if (handler.config.requiredToFailBy) {
requiredToFailBy = extractValidHandlerTags(
handler.config.requiredToFailBy
);
}

RNGestureHandlerModule.updateGestureHandler(
handler.handlerTag,
filterConfig(handler.config, ALLOWED_PROPS, {
simultaneousHandlers: simultaneousWith,
waitFor: requireToFail,
requiredToFailBy: requiredToFailBy,
})
);
}
Expand Down
10 changes: 9 additions & 1 deletion src/handlers/gestures/gesture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export interface BaseGestureConfig
ref?: React.MutableRefObject<GestureType | undefined>;
requireToFail?: GestureRef[];
simultaneousWith?: GestureRef[];
requiredToFailBy?: GestureRef[];
needsPointerData?: boolean;
manualActivation?: boolean;
runOnJS?: boolean;
Expand Down Expand Up @@ -144,7 +145,7 @@ export abstract class BaseGesture<
}

private addDependency(
key: 'simultaneousWith' | 'requireToFail',
key: 'simultaneousWith' | 'requireToFail' | 'requiredToFailBy',
gesture: Exclude<GestureRef, number>
) {
const value = this.config[key];
Expand Down Expand Up @@ -275,6 +276,13 @@ export abstract class BaseGesture<
return this;
}

requiredToFailByExternalGesture(...gestures: Exclude<GestureRef, number>[]) {
for (const gesture of gestures) {
this.addDependency('requiredToFailBy', gesture);
}
return this;
}

withTestId(id: string) {
this.config.testId = id;
return this;
Expand Down