Skip to content

Conversation

akwasniewski
Copy link
Contributor

Description

I did some performance tests on the NativeDetector after adding changes that handle attaching LogicDetector, which revealed that the new logic adds quite a bit of overhead, but only on the JS side. New logic on the native side does not seem to have a significant effect.
We concluded that the best solution is to create a separate component that will have all functionalities of NativeDetector and also allow LogicDetector attachment, while NativeDetector will be reverted to how it had been before implementing LogicDetector.

Test plan

This example, which has DelegateDetector above LogicDetector should work

import React from 'react';
import { Text, View, StyleSheet } from 'react-native';
import { DelegateDetector, LogicDetector, useGesture, SingleGestureName, GestureHandlerRootView } 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 (
    <GestureHandlerRootView>
      <View style={styles.container}>
        <Text style={styles.header}>
          Overlapping SVGs with gesture detectors
        </Text>
        <View style={{ backgroundColor: 'tomato' }}>
          <DelegateDetector 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>
          </DelegateDetector>
        </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' }}>
          <DelegateDetector 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>
          </DelegateDetector>
        </View>
        <Text>The viewBox property remaps SVG's coordinate space</Text>
      </View>
    </GestureHandlerRootView>
  );
}

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

And this one which uses NativeDetector above LogicDetector should throw
Logic detector must be a descendant of a delegate detector

import React from 'react';
import { Text, View, StyleSheet } from 'react-native';
import { NativeDetector, LogicDetector, useGesture, SingleGestureName, GestureHandlerRootView } 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 (
    <GestureHandlerRootView>
      <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>
    </GestureHandlerRootView>
  );
}

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

@akwasniewski akwasniewski marked this pull request as ready for review October 3, 2025 16:17
@akwasniewski
Copy link
Contributor Author

Currently the new Detector is named DelegateDetector as LogicDetector delegates their gestures to it. However, I'm not sure if it is a good name, I couldn't think of a better one. I'd appreciate any input on the naming.

Comment on lines 165 to 169
export type { NativeDetectorProps } from './v3/Detectors/common';
export { NativeDetector } from './v3/Detectors/NativeDetector';

export { LogicDetector } from './v3/Detectors/LogicDetector/LogicDetector';
export { DelegateDetector } from './v3/Detectors/LogicDetector/DelegateDetector';
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe let's also create index.ts in v3/detectors (and yes, I'd use lowercase for that directory 😅) which will export these?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oki-dokie, I renamed directory in 88d4290 and created detectors/index in b7e9fcd

@@ -14,7 +14,9 @@ export const DetectorContext = createContext<DetectorContextType | null>(null);
export function useDetectorContext() {
const ctx = useContext(DetectorContext);
if (!ctx) {
throw new Error('Logic detector must be a descendant of a Native Detector');
throw new Error(
'Logic detector must be a descendant of a delegate detector'
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
'Logic detector must be a descendant of a delegate detector'
tagMessage('Logic detector must be a descendant of a delegate detector')

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