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

Add a new gesture relation - blocksHandlers - working like reversed waitFor #2627

Merged
merged 16 commits into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from all 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 blockingRelations = SparseArray<IntArray>()

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

override fun shouldWaitForHandlerFailure(handler: GestureHandler<*>, otherHandler: GestureHandler<*>) =
Expand All @@ -41,7 +47,7 @@ class RNGestureHandlerInteractionManager : GestureHandlerInteractionController {
override fun shouldRequireHandlerToWaitForFailure(
handler: GestureHandler<*>,
otherHandler: GestureHandler<*>,
) = false
) = blockingRelations[handler.tag]?.any { tag -> tag == otherHandler.tag } ?: false

override fun shouldHandlerBeCancelledBy(handler: GestureHandler<*>, otherHandler: GestureHandler<*>): Boolean {
if (otherHandler is NativeViewGestureHandler) {
Expand All @@ -63,5 +69,6 @@ class RNGestureHandlerInteractionManager : GestureHandlerInteractionController {
companion object {
private const val KEY_WAIT_FOR = "waitFor"
private const val KEY_SIMULTANEOUS_HANDLERS = "simultaneousHandlers"
private const val KEY_BLOCKS_HANDLERS = "blocksHandlers"
}
}
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. You can customize how gestures interact with each other across multiple components in a couple of ways:

## requireExternalGestureToFail

`requireExternalGestureToFail` allows to delay 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',
},
});
```

## blocksExternalGesture

`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:

```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()
.blocksExternalGesture(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. 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:

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

### `blocksExternalGesture(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[@"blocksHandlers"]];

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',
'blocksHandlers',
] 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>[];
blocksHandlers?: React.Ref<unknown> | React.Ref<unknown>[];
testID?: string;
cancelsTouchesInView?: boolean;
// TODO(TS) - fix event types
Expand Down
6 changes: 6 additions & 0 deletions src/handlers/gestures/GestureDetector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -197,11 +197,17 @@ function attachHandlers({
);
}

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

RNGestureHandlerModule.updateGestureHandler(
handler.handlerTag,
filterConfig(handler.config, ALLOWED_PROPS, {
simultaneousHandlers: simultaneousWith,
waitFor: requireToFail,
blocksHandlers: blocksHandlers,
})
);
}
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[];
blocksHandlers?: 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' | 'blocksHandlers',
gesture: Exclude<GestureRef, number>
) {
const value = this.config[key];
Expand Down Expand Up @@ -275,6 +276,13 @@ export abstract class BaseGesture<
return this;
}

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

withTestId(id: string) {
this.config.testId = id;
return this;
Expand Down
1 change: 1 addition & 0 deletions src/web/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export interface Config extends Record<string, ConfigArgs> {
enabled?: boolean;
simultaneousHandlers?: Handler[] | null;
waitFor?: Handler[] | null;
blocksHandlers?: Handler[] | null;
hitSlop?: HitSlop;
shouldCancelWhenOutside?: boolean;
userSelect?: UserSelect;
Expand Down
Loading
Loading