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, SingleGestureName } from 'react-native-gesture-handler';

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

export default function SvgExample() {
  const circleElementTap = useGesture(SingleGestureName.Tap, {
    onStart: () => {
      'worklet';
      console.log('RNGH: clicked circle')
    }
  });
  const rectElementTap = useGesture(SingleGestureName.Tap, {
    onStart: () => {
      'worklet';
      console.log('RNGH: clicked parallelogram')
    }
  });
  const containerTap = useGesture(SingleGestureName.Tap, {
    onStart: () => {
      'worklet';
      console.log('RNGH: clicked container')
    }
  });
  const vbContainerTap = useGesture(SingleGestureName.Tap, {
    onStart: () => {
      'worklet';
      console.log('RNGH: clicked viewbox container')
    }
  });
  const vbInnerContainerTap = useGesture(SingleGestureName.Tap, {
    onStart: () => {
      'worklet';
      console.log('RNGH: clicked inner viewbox container')
    }
  });
  const vbCircleTap = useGesture(SingleGestureName.Tap, {
    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.

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unfortunately changes in #3682 did not help in any way, this workaround is the best thing we could do.

Copy link
Member

Choose a reason for hiding this comment

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

Are reanimated events still triggered on the UI thread this way?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It were not triggered on the ui thread. It is finally solved in https://github.com/software-mansion/react-native-gesture-handler/pull/3689/commits. Thank you for your help in office.

@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
Comment on lines 69 to 72
const method = !logicMethods.current.has(e.nativeEvent.handlerTag)
? gesture.gestureEvents.onGestureHandlerStateChange
: logicMethods.current.get(e.nativeEvent.handlerTag)?.current
?.onGestureHandlerStateChange;
Copy link
Member

Choose a reason for hiding this comment

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

Any chance to extract this to a method? It repeats for every event.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Okay, e6fbdba

Copy link
Member

Choose a reason for hiding this comment

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

We can go deeper - you can change the new function to return a function which will handle the event and assign that directly to the event handler, i.e.:

        onGestureHandlerStateChange={createGestureEventHandler('onGestureHandlerStateChange', e);}

instead of

        onGestureHandlerStateChange={(e) => {
          handleGestureEvent('onGestureHandlerStateChange', e);
        }}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That is a really good idea, thanks. Done in 832a039

override fun getCoalescingKey() = coalescingKey

override fun getEventData(): WritableMap = if (actionType == GestureHandler.ACTION_TYPE_NATIVE_DETECTOR) {
override fun getEventData(): WritableMap = if (GestureHandler.usesNativeOrLogicDetector(actionType)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I know this is not exact place, but shouldn't we also update canCoalesce above?

Copy link
Contributor

Choose a reason for hiding this comment

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

Also I'm not sure about TouchEvents, since they have touchesMove callback (cc @j-piasecki)

case RNGestureHandlerActionTypeLogicDetector: {
NSNumber *hostDetectorTag = [_registry handlerWithTag:event.handlerTag].hostDetectorTag;
detectorView = [self viewForReactTag:hostDetectorTag];
// intentionally fall through to RNGestureHandlerActionTypeNativeDetector
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 this approach, could we extract it to a function and then call it in both?

attachedNativeHandlers.current
);
attachedNativeHandlers.current.clear();
detachHandlers(newHandlerTags, attachedNativeHandlers.current);
Copy link
Contributor

Choose a reason for hiding this comment

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

At this point, isn't attachedNativeHandlers empty? If so, maybe add a comment that it is intentionally empty?

useEffect(() => {
return () => {
detachHandlers(attachedHandlerTags.current);
detachHandlers(new Set(), attachedHandlers.current);
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe it won't change much in the optimization, but we can create EMPTY_SET variable and then pass it to avoid calling these constructors.

Comment on lines +19 to +28
if (node) {
if (Platform.OS === 'web') {
setViewTag(node);
} else {
const tag = findNodeHandle(node);
if (tag != null) {
setViewTag(tag);
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

if(!node) {
  return;
}

: [props.gesture.tag],
};

register(logicProps, logicMethods as RefObject<GestureEvents<unknown>>);
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this cast necessary?

@m-bert m-bert dismissed their stale review September 19, 2025 11:30

I'll be on vacation and I don't want to block this PR - I believe it will be good to go when @j-piasecki approves it :D

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