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.12k stars 980 forks source link

Nested Pan / LongPressGestureHandlers with different behavior Android / iOS #1281

Closed hardcodet closed 2 years ago

hardcodet commented 3 years ago

Description

I'm trying to get a list (ScrollView) that contains items that can be reordered. I struggled a bit with the implementation, but got it working on iOS like with nested LongPress / PanGesture-Handlers:

I got both the Pan event and the ScrollView working on iOS by setting activeOffsetX and activeOffsetY as states, which are updated upon LongPress events. The offset defaults to [-100, 100], which causes flicks to scroll the all items on the ScrollView. After a long press, I set the offset to [0,0], and I can start dragging around the items on the UI. But as I said, that only works on iOS, so there's another distinction between the platforms.

On Android, this doesn't work at all. I noticed a few things:

Here's the relevant code to my items. As said, at runtime, they are rendered as children of a ScrollView parent component:

const [isLongPress, setLongPress] = useState(false);
const [offset, setOffset] = useState([-100, 100]);

const onLongPressGestureEvent = useAnimatedGestureHandler<LongPressGestureHandlerGestureEvent>({
    onActive: (nativeEvent) => {
        if(!isLongPress) {
            runOnJS(setLongPress)(true);
            runOnJS(setOffset)([-5, 5]);
        }
    },
    onFinish: () => {
        runOnJS(setLongPress)(false);
        runOnJS(setOffset)([-100, 100]);
    }
});

<Animated.View style={style}>
    <PanGestureHandler
        activeOffsetX={offset}
        activeOffsetY={offset}
        ref={panRef}
        simultaneousHandlers={pressRef}
        enabled={true}
        onGestureEvent={onPanGestureEvent}>
        <Animated.View style={StyleSheet.absoluteFill}>
            <LongPressGestureHandler
                shouldCancelWhenOutside={false}
                maxDist={1000}
                ref={pressRef}
                simultaneousHandlers={panRef}
                onGestureEvent={onLongPressGestureEvent}>
                <Animated.View style={StyleSheet.absoluteFill}>
                    {children}
                </Animated.View>
            </LongPressGestureHandler>
        </Animated.View>
    </PanGestureHandler>
</Animated.View>

What's happening

On Android, short flicks do properly scroll the ScrollView. If I long-press and then start dragging, no scrolling happens (that's good), but also no panning. While dragging my finger, I get continuous LongPress OnActive events, but I get only on Pan OnStart event but no other pan events (active or end).

On iOS, I get simultanous LongPress active / Pan active events, so I can animated the dragged item.

Package versions

Svarto commented 3 years ago

subscribed, have similar issue and would love to see a way to handle this!

MoOx commented 3 years ago

Same kind of problem here. Not sure if that's related to a LongPress nested into a Pan, but I have to listen for BEGIN on Android and ACTIVE for iOS to have something similar on both platform...

naftalibeder commented 3 years ago

This does not engage with your exact question, but just a note that the LongPressGestureHandler gesture event payload includes a y value that updates on drag, so you don't actually need multiple handlers for this use case.

pkvov commented 3 years ago

some problem, IOS works fine, no luck on Android,

const translation = useVector();
const panRef = useRef(null);
const longPressRef = useRef(null);
const isDragging = useSharedValue(false);

const translateX = useDerivedValue(() => {
    if (isDragging.value) {
      return translation.x.value;
    }
    return withSpring(translation.x.value);
  });

  const translateY = useDerivedValue(() => {
    if (isDragging.value) {
      return translation.y.value;
    }
    return withSpring(translation.y.value);
  });

const style = useAnimatedStyle(() => {
    return {
      position: "absolute",
      top: 0,
      left: 0,
      transform: [
        { translateX: translateX.value },
        { translateY: translateY.value }
      ],
    };
  });

const handleLongPressStateChange = (evt) => {
    console.log("handleLongPressStateChange");
    if (evt.nativeEvent.state == State.ACTIVE) {
      console.log("set isDragging to True");
      isDragging.value = true;
    }
    if (evt.nativeEvent.state == State.CANCELLED ||
      evt.nativeEvent.state == State.END) {
      console.log("set isDragging to False");
      isDragging.value = false;
    }
  };
  const onPanGestureEvent = (evt) => {
    console.log("onPanGestureEvent");
    console.log("isDragging: ", isDragging.value);
    if (isDragging.value) {
      const {
        nativeEvent: { translationX: x, translationY: y },
      } = evt;

      console.log("onPanGestureEvent onActive change x and y");
      translation.x.value = x;
      translation.y.value = y;
    }
  };

function nestingLongPressWithPan() {
    return (
      <Animated.View width={width} height={height - 100}>
        <PanGestureHandler
          ref={panRef}
          simultaneousHandlers={longPressRef}
          onGestureEvent={onPanGestureEvent}
          onHandlerStateChange={(evt) => {
            console.log("onHandlerStateChange");
            if (isDragging.value && evt.nativeEvent.state == State.END) {
              console.log("set isDragging to False");
              isDragging.value = false;

              translation.x.value = 0;
              translation.y.value = 0;
            }
          }}
        >
          <Animated.View>
            <LongPressGestureHandler
              ref={longPressRef}
              minDurationMs={500}
              maxDist={400}
              onHandlerStateChange={handleLongPressStateChange}
            >
              <Animated.View
                x="0"
                y="0"
                style={[style]}
              >
                <Text>
                  this text can drag & drop!!!
              </Text>
              </Animated.View>
            </LongPressGestureHandler>
          </Animated.View>
        </PanGestureHandler>
      </Animated.View>
    );
  }

react: 17.0.1 => 17.0.1 react-native: 0.64.2 => 0.64.2 react-native-gesture-handler: 1.10.3 => 1.10.3

j-piasecki commented 3 years ago

Hi and thank you for such a detailed report! Let me address the issues you have mentioned in order:

j-piasecki commented 3 years ago

@pkvov Hi, I checked the sample you have provided and it's working for me (I tested it on RNGH from the master branch). I believe that this is the PR that fixed it: https://github.com/software-mansion/react-native-gesture-handler/pull/1618.

NicholasBoccuzzi commented 2 years ago

@pkvov Hi, I checked the sample you have provided and it's working for me (I tested it on RNGH from the master branch). I believe that this is the PR that fixed it: #1618.

Happy to hear that this was fixed. Thank you! Has the fix been released in an alpha version available to the public or are there plans for a release anytime soon that includes this fix?

j-piasecki commented 2 years ago

@NicholasBoccuzzi It has been released: https://github.com/software-mansion/react-native-gesture-handler/releases/tag/2.0.0