software-mansion / react-native-gesture-handler

Declarative API exposing platform native touch and gesture system to React Native.
https://docs.swmansion.com/react-native-gesture-handler/
MIT License
6.13k stars 982 forks source link

[Android] Fix gestures being able to activate despite their parent already being active #3095

Closed latekvo closed 1 month ago

latekvo commented 2 months ago

Description

This PR fixes invalid activation of gestures nested inside other gestures, like Pan gesture nested inside Native gesture attached to ScrollView

Gestures nested inside native elements such as ScrollView used to be able to steal pointers from their already active parents.

That is no longer possible, already active parents cannot have their active pointers stolen.

Related to #2622

Test plan

Notes

Code

Collapsed code ```js import React from 'react'; import { StyleSheet, Text, View, ScrollView } from 'react-native'; import { Gesture, GestureDetector, GestureUpdateEvent, PanGestureHandlerEventPayload, } from 'react-native-gesture-handler'; import Animated, { SharedValue, useAnimatedStyle, useSharedValue, withSpring, } from 'react-native-reanimated'; export default function EmptyExample() { const firstExternalPosition = useSharedValue<{ x: number; y: number }>({ x: 0, y: 0, }); const secondExternalPosition = useSharedValue<{ x: number; y: number }>({ x: 0, y: 0, }); const nestedPosition = useSharedValue<{ x: number; y: number }>({ x: 0, y: 0, }); const setter = ( position: SharedValue<{ x: number; y: number; }> ) => { return (event: GestureUpdateEvent) => { 'worklet'; position.value = { x: event.translationX, y: event.translationY, }; }; }; const resetter = ( position: SharedValue<{ x: number; y: number; }> ) => { return () => { 'worklet'; position.value = { x: withSpring(0), y: withSpring(0), }; }; }; const scrollGesture = Gesture.Native(); const firstExternalPan = Gesture.Pan() .onUpdate(setter(firstExternalPosition)) .onFinalize(resetter(firstExternalPosition)); const secondExternalPan = Gesture.Pan() .onUpdate(setter(secondExternalPosition)) .onFinalize(resetter(secondExternalPosition)); const nestedPan = Gesture.Pan() // .simultaneousWithExternalGesture(scrollGesture) .onUpdate(setter(nestedPosition)) .onFinalize(resetter(nestedPosition)); const firstExternalAnimation = useAnimatedStyle(() => { return { ...styles.box, transform: [ { translateX: firstExternalPosition.value.x }, { translateY: firstExternalPosition.value.y }, ], }; }); const secondExternalAnimation = useAnimatedStyle(() => { return { ...styles.box, transform: [ { translateX: secondExternalPosition.value.x }, { translateY: secondExternalPosition.value.y }, ], }; }); const nestedAnimation = useAnimatedStyle(() => { return { ...styles.box, transform: [ { translateX: nestedPosition.value.x }, { translateY: nestedPosition.value.y }, ], }; }); return ( Square showcasing 2 disconnected gestures can be moved independantly regardless of changes in this PR, and regardless if one of them is nested inside a native handler. Square showcasing 2 disconnected gestures can be moved independantly regardless of changes in this PR, and regardless if one of them is nested inside a native handler. GH Gesture {new Array(20) .fill(1) .map((value, index) => value * index) .map((value) => ( Entry no. {value} ))} ); } const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#F5FCFF', gap: 20, }, externalContainer: { flexDirection: 'row', gap: 20, marginTop: 300, }, box: { position: 'relative', backgroundColor: 'tomato', width: 200, height: 200, }, list: { width: 200, backgroundColor: 'plum', }, element: { margin: 1, height: 40, backgroundColor: 'orange', }, }); ```