Skip to content

[Android] Fix ScrollView invalid pointer id during navigation#4177

Merged
j-piasecki merged 1 commit into
mainfrom
@jpiasecki/fix-scroll-view-invalid-pointer-id
May 15, 2026
Merged

[Android] Fix ScrollView invalid pointer id during navigation#4177
j-piasecki merged 1 commit into
mainfrom
@jpiasecki/fix-scroll-view-invalid-pointer-id

Conversation

@j-piasecki

Copy link
Copy Markdown
Member

Description

Fixes #3921

The flow for this crash was:

  • User has multiple pointers on a scroll view with an active NativeViewGestureHandler attached.
  • Native stack navigation is started, and the screen with the scroll view is removed from the stack with an animation.
  • Android moves the unmounted screen from mChildren to mDisappearingChildren - it's now considered unmounted, but its lifetime is prolonged for the animation to finish. A synthetic ACTION_CANCEL event is dispatched by the OS, Scrollview mounted on this screen receives the event and sets mActivePointerId = INVALID_POINTER as a result.
  • User lifts a pointer from the screen, NativeViewGestureHandler forwards that event to the ScrollView, despite it now being "unmounted". Since its internal tracking has been reset, it tries to look for a pointer with an index of INVALID_POINTER (-1), which throws an exception.

Gesture Handler has a path that should prevent this from happening (checking isViewAttachedUnderWrapper before passing an event to the handler), however, due to the exiting animation, the view remained partially mounted. Android clears parent -> child link, but not child -> parent. RNGH was using the second one to ensure the view is still a valid target for events.

This PR changes the behavior of isViewAttachedUnderWrapper to rely on the first link, which is cleared. This is technically a breaking change, since now all gestures on the outgoing screen are canceled as soon as navigation starts, but I'd classify this change in behavior as a bug fix.

Test plan

Run the reproduction code, tap on any of the items. During the 500ms window between tap and navigation start, begin a multi-touch gesture on the scroll view (like a pinch). Release the pointers DURING the navigation animation. Before this change, the app crashes, after this change, it does not.

Reproduction code
import { NavigationContainer } from '@react-navigation/native';
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { StyleSheet, Text, View } from 'react-native';
import {
  FlatList,
  GestureHandlerRootView,
  Touchable,
} from 'react-native-gesture-handler';

type RootStackParamList = {
  Main: undefined;
  Details: { id: number };
};

const Stack = createNativeStackNavigator<RootStackParamList>();

const ITEMS = Array.from({ length: 50 }, (_, i) => ({
  id: i,
  title: `Card ${i + 1}`,
}));

const NAVIGATE_DELAY_MS = 500;

export default function App() {
  return (
    <GestureHandlerRootView style={styles.root}>
      <NavigationContainer>
        <Stack.Navigator
          screenOptions={{
            animation: 'slide_from_right',
          }}>
          <Stack.Screen
            name="Main"
            component={MainScreen}
            options={{ title: 'Main' }}
          />
          <Stack.Screen
            name="Details"
            component={DetailsScreen}
            options={{ title: 'Details' }}
          />
        </Stack.Navigator>
      </NavigationContainer>
    </GestureHandlerRootView>
  );
}

function MainScreen({
  navigation,
}: NativeStackScreenProps<RootStackParamList, 'Main'>) {
  return (
    <View style={styles.container}>
      <FlatList
        data={ITEMS}
        keyExtractor={(item) => item.id.toString()}
        renderItem={({ item }) => (
          <Touchable
            style={styles.card}
            onPress={() => {
              setTimeout(() => {
                navigation.navigate('Details', { id: item.id });
              }, NAVIGATE_DELAY_MS);
            }}>
            <Text style={styles.cardText}>{item.title}</Text>
          </Touchable>
        )}
      />
    </View>
  );
}

function DetailsScreen({
  route,
}: NativeStackScreenProps<RootStackParamList, 'Details'>) {
  return (
    <View style={styles.detailsContainer}>
      <Text style={styles.detailsText}>
        Details for item #{route.params.id}
      </Text>
    </View>
  );
}

const styles = StyleSheet.create({
  root: {
    flex: 1,
  },
  container: {
    flex: 1,
    backgroundColor: '#fff',
  },
  card: {
    paddingVertical: 20,
    paddingHorizontal: 16,
    borderBottomWidth: StyleSheet.hairlineWidth,
    borderBottomColor: '#ccc',
    backgroundColor: '#fff',
  },
  cardText: {
    fontSize: 16,
    color: '#000',
  },
  detailsContainer: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
    backgroundColor: '#fff',
  },
  detailsText: {
    fontSize: 18,
    color: '#000',
  },
});

Copilot AI review requested due to automatic review settings May 14, 2026 11:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes an Android crash (IllegalArgumentException: pointerIndex out of range) that occurred when multi-touch gestures on a ScrollView continued to be forwarded to the native view after its host screen had been moved to mDisappearingChildren during a native-stack navigation transition. The reachability check in GestureHandlerOrchestrator is updated to detect this "logically detached but still drawn" state.

Changes:

  • Rewrites isViewAttachedUnderWrapper to traverse from the view up via parent.indexOfChild(current), treating a child whose parent no longer lists it (disappearing child) as detached, so the orchestrator cancels the handler instead of forwarding events that hit the broken mActivePointerId state.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

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

Sadly it doesn't reproduce on my own device 😞 But luckily it does on emulator 😄

@j-piasecki j-piasecki merged commit 5ddbf1e into main May 15, 2026
7 checks passed
@j-piasecki j-piasecki deleted the @jpiasecki/fix-scroll-view-invalid-pointer-id branch May 15, 2026 06:11
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.

[Android] com.facebook.react.views.scroll.ReactScrollView.onTouchEvent java.lang.IllegalArgumentException

3 participants