Skip to content

Conversation

akwasniewski
Copy link
Contributor

@akwasniewski akwasniewski commented Aug 25, 2025

Description

This PR implements new component -> LogicDetector. It resolves the issue of attaching gestures to inner SVG components. LogicDetector communicates with a NativeDetector higher in the hierarchy, which will be responsible for attaching gestures.

Test plan

tested on the following code

import React from 'react';
import { Text, View, StyleSheet } from 'react-native';
import { NativeDetector, LogicDetector, useGesture } from 'react-native-gesture-handler';

import Svg, { Circle, Rect } from 'react-native-svg';

export default function SvgExample() {
  const circleElementTap = useGesture("TapGestureHandler", {
    onStart: () => {
      'worklet';
      console.log('RNGH: clicked circle')
    }
  });
  const rectElementTap = useGesture("TapGestureHandler", {
    onStart: () => {

      'worklet';
      console.log('RNGH: clicked parallelogram')
    }
  });
  const containerTap = useGesture("TapGestureHandler", {
    onStart: () => {

      'worklet';
      console.log('RNGH: clicked container')
    }
  });
  const vbContainerTap = useGesture("TapGestureHandler", {
    onStart: () => {

      'worklet';
      console.log('RNGH: clicked viewbox container')
    }
  });
  const vbInnerContainerTap = useGesture("TapGestureHandler", {
    onStart: () => {

      'worklet';
      console.log('RNGH: clicked inner viewbox container')
    }
  });
  const vbCircleTap = useGesture("TapGestureHandler", {
    onStart: () => {
      'worklet';
      console.log('RNGH: clicked viewbox circle')
    }
  });

  return (
    <View>
      <View style={styles.container}>
        <Text style={styles.header}>
          Overlapping SVGs with gesture detectors
        </Text>
        <View style={{ backgroundColor: 'tomato' }}>
          <NativeDetector gesture={containerTap}>
            <Svg
              height="250"
              width="250"
              onPress={() => console.log('SVG: clicked container')}>
              <LogicDetector gesture={circleElementTap}>
                <Circle
                  cx="125"
                  cy="125"
                  r="125"
                  fill="green"
                  onPress={() => console.log('SVG: clicked circle')}
                />
              </LogicDetector>
              <LogicDetector gesture={rectElementTap}>
                <Rect
                  skewX="45"
                  width="125"
                  height="250"
                  fill="yellow"
                  onPress={() => console.log('SVG: clicked parallelogram')}
                />
              </LogicDetector>
            </Svg>
          </NativeDetector>
        </View>
        <Text>
          Tapping each color should read to a different console.log output
        </Text>
      </View>
      <View style={styles.container}>
        <Text style={styles.header}>SvgView with SvgView with ViewBox</Text>
        <View style={{ backgroundColor: 'tomato' }}>
          <NativeDetector gesture={vbContainerTap}>
            <Svg
              height="250"
              width="250"
              viewBox="-50 -50 150 150"
              onPress={() => console.log('SVG: clicked viewbox container')}>
              <LogicDetector gesture={vbInnerContainerTap}>
                <Svg
                  height="250"
                  width="250"
                  viewBox="-300 -300 600 600"
                  onPress={() =>
                    console.log('SVG: clicked inner viewbox container')
                  }>
                  <Rect
                    x="-300"
                    y="-300"
                    width="600"
                    height="600"
                    fill="yellow"
                  />
                  <LogicDetector gesture={vbCircleTap}>
                    <Circle
                      r="300"
                      fill="green"
                      onPress={() => console.log('SVG: clicked viewbox circle')}
                    />
                  </LogicDetector>
                </Svg>
              </LogicDetector>
            </Svg>
          </NativeDetector>
        </View>
        <Text>The viewBox property remaps SVG's coordinate space</Text>
      </View>
    </View >
  );
}

const styles = StyleSheet.create({
  container: {
    alignItems: 'center',
    justifyContent: 'center',
    marginBottom: 48,
  },
  header: {
    fontSize: 18,
    fontWeight: 'bold',
    margin: 10,
  },
});

@akwasniewski akwasniewski force-pushed the @akwasniewski/logic-detector branch from 5b65a19 to 81bf405 Compare September 1, 2025 13:47
Copy link
Member

@j-piasecki j-piasecki left a comment

Choose a reason for hiding this comment

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

Did a fist pass, but some stuff will likely change due to #3682 :/

Anyway, you have the hardest part figured out, now it's just a matter of polishing it up.

Comment on lines 7 to 16
- (facebook::react::RNGestureHandlerDetectorEventEmitter::OnGestureHandlerEvent)getNativeEvent;

- (facebook::react::RNGestureHandlerDetectorEventEmitter::OnGestureHandlerLogicEvent)getNativeLogicEvent:
(NSNumber *)childTag;
@end

@interface RNGestureHandlerStateChange (NativeEvent)

- (facebook::react::RNGestureHandlerDetectorEventEmitter::OnGestureHandlerStateChange)getNativeEvent;

- (facebook::react::RNGestureHandlerDetectorEventEmitter::OnGestureHandlerLogicStateChange)getNativeLogicEvent:
(NSNumber *)childTag;
Copy link
Member

Choose a reason for hiding this comment

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

Then, you wouldn't need getNativeLogicEvent here

Copy link
Member

Choose a reason for hiding this comment

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

I'm pretty sure that this and below comment were supposed to be in reverse order

@@ -105,4 +105,68 @@ export function deepEqual(obj1: any, obj2: any) {
return true;
}

export function invokeNullableMethod(
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure this is the way to go. Once #3682 is merged, you should be able to cleanly handle all cases in hooks it adds.

const attachedNativeHandlerTags =
logicChildren.current.get(key)?.attachedNativeHandlerTags;
if (attachedHandlerTags && attachedNativeHandlerTags) {
detachHandlers(
Copy link
Member

Choose a reason for hiding this comment

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

Why detach when shouldKeepLogicChild is true?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Overhauled detaching in 593c44f

@akwasniewski akwasniewski force-pushed the @akwasniewski/logic-detector branch from 94984b2 to 4e718b3 Compare September 8, 2025 12:18
@akwasniewski akwasniewski requested a review from m-bert September 9, 2025 10:24
@akwasniewski akwasniewski marked this pull request as ready for review September 11, 2025 09:14
Copy link
Contributor

@m-bert m-bert left a comment

Choose a reason for hiding this comment

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

Good job! I think some type changes might conflict with #3700, but it shouldn't be a big deal 😅

@@ -31,9 +31,13 @@ open class GestureHandler {
var tag = 0
var view: View? = null
private set

// parent view is used for logicDetector
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// parent view is used for logicDetector
// Parent view is used for Logic Detector.

Could also be lowercase, but I think camelCase in that case seems off.

Comment on lines +134 to +141
export function useDetectorContext() {
const ctx = useContext(DetectorContext);
if (!ctx) {
throw new Error('Logic detector must be under a Native Detector');
}
return ctx;
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Can we have this in separate file?

@@ -187,3 +187,31 @@ export function hash(str: string) {
}
return h >>> 0;
}

export function invokeNullableMethod(
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't like this name, we already have this in web implementation and it is quite confusing. I think it would be better to rename it right now while we remember what it does. Also a comment with explanation would be great.

@@ -48,3 +76,23 @@ export type CallbackHandlers = Omit<
export type AnimatedEvent = ((...args: any[]) => void) & {
_argMapping: (Animated.Mapping | null)[];
};

export interface LogicChildrenProps {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure about this Props suffix, but that's not strong opinion.

this.delegate.init(viewRef, this);
}

public detach() {
if (this.state === State.ACTIVE) {
this.cancel();
this.cancelTouches();
Copy link
Contributor

Choose a reason for hiding this comment

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

Why?

Comment on lines +225 to +229
[self attachHandlers:child.handlerTags
actionType:RNGestureHandlerActionTypeLogicDetector
viewTag:child.viewTag
attachedHandlers:logicChildren[child.viewTag]];
}
Copy link
Contributor

Choose a reason for hiding this comment

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

There's no overlap between newProps.handlerTags and newProps.logicChildren, right?

logicChildren?: Set<LogicChildrenProps>;
}

export interface LogicChildrenProps {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure If I like the idea of having 2 types named the same. Though it is not a strong opinion, I'd add something, for example Web suffix.

And same as in types.ts, I'm not fond of adding Props suffix in this case.

Comment on lines +102 to +106
const logicChildrenToDelete: Set<number> = new Set();

for (const key of logicChildren.current.keys()) {
logicChildrenToDelete.add(key);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Can't we just do

Suggested change
const logicChildrenToDelete: Set<number> = new Set();
for (const key of logicChildren.current.keys()) {
logicChildrenToDelete.add(key);
}
const logicChildrenToDelete: Set<number> = new Set(logicChildren.current.keys());

?

});

logicChildrenToDelete.forEach((childTag) => {
if (attachedHandlerTags && attachedNativeHandlerTags) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure if I get this condition. As far as I remember attachedNativeHandlerTags was used for Native gestures, so why do we want only them to be detached?

Comment on lines +125 to +126
logicChildren.current.get(childTag)!,
logicChildren.current.get(childTag)!
Copy link
Contributor

Choose a reason for hiding this comment

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

I get the idea, but it seems quite confusing why they're the same, could we add a little comment?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants