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 +76 to +78
for (child in logicChildren) {
shouldKeepLogicChild[child.key] = false
}
Copy link
Member

Choose a reason for hiding this comment

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

It's the same loop as above, and it's not modified in between. Is it needed twice?

Comment on lines +40 to +42
override fun setLogicChildren(view: RNGestureHandlerDetectorView?, value: ReadableArray?) {
view?.setLogicChildren(value)
}
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
override fun setLogicChildren(view: RNGestureHandlerDetectorView?, value: ReadableArray?) {
view?.setLogicChildren(value)
}
override fun setLogicChildren(view: RNGestureHandlerDetectorView, value: ReadableArray?) {
view.setLogicChildren(value)
}

I think we can assume that view will not be nullable like in other methods

@@ -4,6 +4,7 @@ export const ActionType = {
JS_FUNCTION_OLD_API: 3,
JS_FUNCTION_NEW_API: 4,
NATIVE_DETECTOR: 5,
LogicDetector: 6,
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
LogicDetector: 6,
LOGIC_DETECTOR: 6,

handlerTag: Int32;
state: Int32;
handlerData: UnsafeMixed;
childTag: Int32;
Copy link
Member

Choose a reason for hiding this comment

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

As we've talked offline - I think that childTag shouldn't be needed by events, so you should be able to get rid of Logic events, and use the existing ones instead.

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

Comment on lines 321 to 322
NSNumber *parentTag = [_registry getLogicParent:@(detectorView.tag)];
RNGHUIView *parentView = [self viewForReactTag:parentTag];
Copy link
Member

Choose a reason for hiding this comment

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

This should happen before this method - in the current implementation, detectorView isn't actually a detector but whatever view the gesture is attached to.

If you move this to RNGestureHandler, can we follow the same pattern as on Android? I.e., keep a reference to the parent detector in the gesture itself? Then you wouldn't need to go through the view registry at all.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, done in 7fb8ea9

@@ -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?

Comment on lines +18 to +20
useEffect(() => {
setViewTag(findNodeHandle(viewRef.current)!);
}, []);
Copy link
Member

Choose a reason for hiding this comment

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

View under the detector can change. I'm not sure if the effect will retrigger in that case.

If not, you can try with a function ref on Wrap (<Wrap ref={(ref) => console.log(ref)} ...)

};
useEffect(() => {
// TODO: set tags from parent
setViewTag(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER));
Copy link
Member

Choose a reason for hiding this comment

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

You can try using the dom reference as a view tag - I think we are doing it already in some places.

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.

2 participants