Skip to content

[android] fixed experimental pointers breaking after pan#3801

Merged
akwasniewski merged 5 commits into
mainfrom
@akwasniewski/fix-experimental-pointers-with-pan
Nov 12, 2025
Merged

[android] fixed experimental pointers breaking after pan#3801
akwasniewski merged 5 commits into
mainfrom
@akwasniewski/fix-experimental-pointers-with-pan

Conversation

@akwasniewski

@akwasniewski akwasniewski commented Nov 4, 2025

Copy link
Copy Markdown
Contributor

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

@axyz axyz left a comment

Copy link
Copy Markdown

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

@m-bert m-bert left a comment

Copy link
Copy Markdown
Collaborator

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
Copy Markdown
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?).

@j-piasecki j-piasecki left a comment

Copy link
Copy Markdown
Member

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
Copy Markdown
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

@j-piasecki j-piasecki left a comment

Copy link
Copy Markdown
Member

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

4 participants