Skip to content

Conversation

@akwasniewski
Copy link
Contributor

@akwasniewski akwasniewski commented Nov 4, 2025

Description

In a project using experimental pointer events, activation of the pan gesture stopped recognition of Js pointer events. This PR should resolve this issue.

Fix

I believe the error is due to the fact that onCancel calls rootView.onChildStartedNativeGesture, which sets the UNSET_CHILD_VIEW_ID in JSPointerDispatcher.kt to current view tag, which is never cleared. This blocks pointer events from being called, as the pointer dispatcher thinks that some child is handling a native gesture. I added cleanup when all fingers have been lifted.

Test plan

Enable experimental pointer events: https://reactnative.dev/blog/2022/12/13/pointer-events-in-react-native#enable-feature-flags.
Use the following code to test the component:

Details
import React from 'react';
import { Text, Pressable, StyleSheet } from 'react-native';
import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler';
import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';


const END_POSITION = 200;

function App(): React.JSX.Element {
  const [count, setCount] = React.useState(0);
  const [pressCount, setPressCount] = React.useState(0);

  const onLeft = useSharedValue(true);
  const position = useSharedValue(0);

  const panGesture = Gesture.Pan()
    .onUpdate((e) => {
      if (onLeft.value) {
        position.value = e.translationX;
      } else {
        position.value = END_POSITION + e.translationX;
      }
    })
    .onEnd((e) => {
      if (position.value > END_POSITION / 2) {
        position.value = withTiming(END_POSITION, { duration: 100 });
        onLeft.value = false;
      } else {
        position.value = withTiming(0, { duration: 100 });
        onLeft.value = true;
      }
    });

  const animatedStyle = useAnimatedStyle(() => ({
    transform: [{ translateX: position.value }],
  }));

  return (
    <GestureHandlerRootView style={styles.container}>
      <Pressable
        style={{ padding: 16, backgroundColor: '#aa0044' }}
        onPointerDown={() => setCount(n => n + 1)}
        onPressIn={() => setPressCount(n => n + 1)}
      >
        <Text style={{ color: 'black' }}>Press me</Text>
      </Pressable>

      <Text style={{ marginBottom: 16, color: 'black' }}>
        pointer: {count} -- press: {pressCount}
      </Text>

      <GestureDetector gesture={panGesture}>
        <Animated.View style={[styles.box, animatedStyle]} />
      </GestureDetector>

    </GestureHandlerRootView>
  );
}

const styles = StyleSheet.create({
  container: {
    padding: 64,
    flex: 1,
  },
  box: {
    height: 120,
    width: 120,
    backgroundColor: '#b58df1',
    borderRadius: 20,
    marginBottom: 30,
  },
});

export default App;

If you have experimental pointer events turned on clicking on the upper pressable should increase both counters even after using pan gesture on the box below. Before this PR, after panning, only one counter worked.

@akwasniewski akwasniewski linked an issue Nov 4, 2025 that may be closed by this pull request
Copy link

@axyz axyz left a comment

Choose a reason for hiding this comment

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

fixes the problem on Android

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.

LGTM (wait for @j-piasecki before merging)

@j-piasecki
Copy link
Member

onChildStartedNativeGesture being called inside onCancel of RootViewGestureHandler is correct. If RootViewGestureHandler gets canceled, it means that there is some other gesture that just activated.

But you're very likely right about it not being cleaned. I think we should be calling onChildEndedNativeGesture after the last finger is lifted from the screen (or when all gestures have finished?).

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.

Ah, forgot to click the thingy 🤦

@akwasniewski
Copy link
Contributor Author

onChildStartedNativeGesture being called inside onCancel of RootViewGestureHandler is correct. If RootViewGestureHandler gets canceled, it means that there is some other gesture that just activated.

But you're very likely right about it not being cleaned. I think we should be calling onChildEndedNativeGesture after the last finger is lifted from the screen (or when all gestures have finished?).

Ok, now it makes sense, Thank you.
After some tinkering around, I think I managed to find the least painful solution to call onChildEndedNativeGesture. It is now called when all no awaiting gestures when we get either onTouchesUpor onTouchesCancel. @j-piasecki, please let me know what you think about it. 03b6b7f

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.

This looks to be a good direction!

@akwasniewski akwasniewski merged commit cbd97db into main Nov 12, 2025
4 checks passed
@akwasniewski akwasniewski deleted the @akwasniewski/fix-experimental-pointers-with-pan branch November 12, 2025 11:37
j-piasecki pushed a commit that referenced this pull request Nov 20, 2025
In a project using experimental pointer events, activation of the pan
gesture stopped recognition of Js pointer events. This PR should resolve
this issue.

I believe the error is due to the fact that `onCancel` calls
rootView.onChildStartedNativeGesture, which sets the
`UNSET_CHILD_VIEW_ID` in JSPointerDispatcher.kt to current view tag,
which is never cleared. This blocks pointer events from being called, as
the pointer dispatcher thinks that some child is handling a native
gesture. I added cleanup when all fingers have been lifted.

Enable experimental pointer events:
https://reactnative.dev/blog/2022/12/13/pointer-events-in-react-native#enable-feature-flags.
Use the following code to test the component:
<details>

```ts
import React from 'react';
import { Text, Pressable, StyleSheet } from 'react-native';
import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler';
import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';

const END_POSITION = 200;

function App(): React.JSX.Element {
  const [count, setCount] = React.useState(0);
  const [pressCount, setPressCount] = React.useState(0);

  const onLeft = useSharedValue(true);
  const position = useSharedValue(0);

  const panGesture = Gesture.Pan()
    .onUpdate((e) => {
      if (onLeft.value) {
        position.value = e.translationX;
      } else {
        position.value = END_POSITION + e.translationX;
      }
    })
    .onEnd((e) => {
      if (position.value > END_POSITION / 2) {
        position.value = withTiming(END_POSITION, { duration: 100 });
        onLeft.value = false;
      } else {
        position.value = withTiming(0, { duration: 100 });
        onLeft.value = true;
      }
    });

  const animatedStyle = useAnimatedStyle(() => ({
    transform: [{ translateX: position.value }],
  }));

  return (
    <GestureHandlerRootView style={styles.container}>
      <Pressable
        style={{ padding: 16, backgroundColor: '#aa0044' }}
        onPointerDown={() => setCount(n => n + 1)}
        onPressIn={() => setPressCount(n => n + 1)}
      >
        <Text style={{ color: 'black' }}>Press me</Text>
      </Pressable>

      <Text style={{ marginBottom: 16, color: 'black' }}>
        pointer: {count} -- press: {pressCount}
      </Text>

      <GestureDetector gesture={panGesture}>
        <Animated.View style={[styles.box, animatedStyle]} />
      </GestureDetector>

    </GestureHandlerRootView>
  );
}

const styles = StyleSheet.create({
  container: {
    padding: 64,
    flex: 1,
  },
  box: {
    height: 120,
    width: 120,
    backgroundColor: '#b58df1',
    borderRadius: 20,
    marginBottom: 30,
  },
});

export default App;
```

</details>

If you have experimental pointer events turned on clicking on the upper
pressable should increase both counters even after using pan gesture on
the box below. Before this PR, after panning, only one counter worked.
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.

Pan gesture breaking pointer events

5 participants