Skip to content

Commit

Permalink
[Android] Fix gestures being able to activate despite their parent al…
Browse files Browse the repository at this point in the history
…ready being active (#3095)

## 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

- use the attached code in place of `EmptyExample.tsx`
- start scrolling the `ScrollView`
- while scrolling the `ScrollView`, drag the `Pan` gesture
- see how before this PR, the `Pan` gesture activated, and with this PR
it doesn't anymore

## Notes

- tested this PR on each of the available examples, found no breaking
changes
- nested gestures may still be run simultaneously if it's explicitly
stated using `Gesture.Simultaneous()` or
`simultaneousWithExternalGesture()`


## Code

<details>

<summary>
Collapsed code
</summary>

```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<PanGestureHandlerEventPayload>) => {
      '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 (
    <View style={styles.container}>
      <View style={styles.externalContainer}>
        <GestureDetector gesture={firstExternalPan}>
          <Animated.View style={firstExternalAnimation}>
            <Text>
              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.
            </Text>
          </Animated.View>
        </GestureDetector>
        <GestureDetector gesture={secondExternalPan}>
          <Animated.View style={secondExternalAnimation}>
            <Text>
              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.
            </Text>
          </Animated.View>
        </GestureDetector>
      </View>

      <View>
        <GestureDetector gesture={scrollGesture}>
          <ScrollView style={styles.list}>
            <GestureDetector gesture={nestedPan}>
              <Animated.View style={nestedAnimation}>
                <Text>GH Gesture</Text>
              </Animated.View>
            </GestureDetector>

            {new Array(20)
              .fill(1)
              .map((value, index) => value * index)
              .map((value) => (
                <View key={value} style={styles.element}>
                  <Text>Entry no. {value}</Text>
                </View>
              ))}
          </ScrollView>
        </GestureDetector>
      </View>
    </View>
  );
}

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',
  },
});

```

</details>
  • Loading branch information
latekvo authored Sep 26, 2024
1 parent 0e7edff commit 28ba683
Showing 1 changed file with 6 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,15 @@ class GestureHandlerOrchestrator(
private fun hasOtherHandlerToWaitFor(handler: GestureHandler<*>) =
gestureHandlers.any { !isFinished(it.state) && shouldHandlerWaitForOther(handler, it) }

private fun shouldBeCancelledByFinishedHandler(handler: GestureHandler<*>) = gestureHandlers.any { shouldHandlerWaitForOther(handler, it) && it.state == GestureHandler.STATE_END }
private fun shouldBeCancelledByFinishedHandler(handler: GestureHandler<*>) =
gestureHandlers.any { shouldHandlerWaitForOther(handler, it) && it.state == GestureHandler.STATE_END }

private fun shouldBeCancelledByActiveHandler(handler: GestureHandler<*>) =
gestureHandlers.any { handler.hasCommonPointers(it) && it.state == GestureHandler.STATE_ACTIVE && !canRunSimultaneously(handler, it) }

private fun tryActivate(handler: GestureHandler<*>) {
// If we are waiting for a gesture that has successfully finished, we should cancel handler
if (shouldBeCancelledByFinishedHandler(handler)) {
if (shouldBeCancelledByFinishedHandler(handler) || shouldBeCancelledByActiveHandler(handler)) {
handler.cancel()
return
}
Expand Down

0 comments on commit 28ba683

Please sign in to comment.