Skip to content

[Android][New Arch] First pointer on background blocks onUpdate flow for second pointer on GestureHandlers #3995

@alexandre-autran

Description

@alexandre-autran

Description

On Android with New Architecture enabled, there is a major issue with touch event distribution. If a first finger is placed on a parent container (background), a second finger interacting with a GestureDetector (like a PanGesture) will trigger onBegin, but onUpdate will never fire.

The first finger "locks" the touch stream, preventing subsequent pointers from sending continuous event data to other handlers in the hierarchy. This effectively breaks all multi-touch interactions where one finger acts as a reference or is simply resting on the screen.

Steps to reproduce

### Packages version
react: 19.2.0

react-native: 0.83.1

react-native-gesture-handler: 2.30.0

react-native-reanimated: 4.2.1

### Platforms
[X] Android (Fabric enabled)

[ ] iOS

### Steps to reproduce
Use the provided example code on an Android device with New Architecture.

First: Press and hold a finger anywhere on the background.

Second: While keeping the first finger down, try to drag the purple box with a second finger.

Observed Behavior: The box does not move. onBegin is triggered (can be seen via logs), but onUpdate is never called as long as the first finger is touching the background.

Expected Behavior: Each pointer should be handled independently. The second finger should be able to move the box regardless of where the first finger is placed.

import {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
const App = () => {
  const onLeft = useSharedValue(true)
  const position = useSharedValue(0)

  const panGesture = Gesture.Pan()
    .onBegin(() => console.log('onBegin called'))
    .onUpdate(e => {
      if (onLeft.value) {
        position.value = e.translationX
      } else {
        position.value = END_POSITION + e.translationX
      }
    })
    .onEnd(() => {
      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.flex1}>
      <GestureDetector gesture={panGesture}>
        <Animated.View style={[styles.box, animatedStyle]} />
      </GestureDetector>
    </GestureHandlerRootView>
  )
}

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

export default App

A link to a Gist, an Expo Snack or a link to a repository based on this template that reproduces the bug.

https://gist.github.com/alexandre-autran/e46cf714bc4e558493cb963bc78d1500

Gesture Handler version

2.30.0

React Native version

0.83.1

Platforms

Android

JavaScript runtime

None

Workflow

React Native (without Expo)

Architecture

New Architecture (Fabric)

Build type

None

Device

Real device

Device model

No response

Acknowledgements

Yes

Metadata

Metadata

Assignees

Labels

Platform: AndroidThis issue is specific to AndroidPlatform: iOSThis issue is specific to iOSRepro providedA reproduction with a snack or repo is provided

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions