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

Pan gesture doesn't activate for some time after scrolling the parent scrollable #3049

Open MatiPl01 opened 2 months ago

MatiPl01 commented 2 months ago

Description

Pan gesture works usually fine inside a ScrollView or other component except some cases, when I try to drag an item a while after scrolling the scrollable parent container. I would expect the Pan gesture to activate in such a case as well but instead, I have to wait for a noticeable amount of time until it works again. Before this time elapses, the Pan gesture calls just the onTouchesDown and onBegin callbacks and no other callback is called later on (if the gesture cannot be handled, I would expect to just receive the onFinalize callback call to handle such a case).

I noticed the problem only on iOS (simulator and real device).

Example recording

https://github.com/user-attachments/assets/87f6549c-f1e4-411d-a6ae-bb0b835f43b8

Steps to reproduce

  1. Clone this repo and build the app from the main branch
  2. See the issue on iOS

Snack or a link to a repository

https://github.com/MatiPl01/gesture-handler-issues

Gesture Handler version

2.18.1

React Native version

0.74.5

Platforms

iOS

JavaScript runtime

Hermes

Workflow

React Native (without Expo)

Architecture

New and Old

Build type

Debug mode

Device

iOS simulator, real device

Device model

tested on iPhone 15 Pro (real device, simulator)

Acknowledgements

Yes

m-bert commented 2 months ago

Hi! I've looked into your repro. The thing is, on iOS Gesture Handler uses native recognizers and in case of ScrollView it has about ±150ms window in which pan gesture will be recognized as scroll. I don't think there's much that we can do about it 😞 (cc @j-piasecki).

Before this time elapses, the Pan gesture calls just the onTouchesDown and onBegin callbacks and no other callback is called later on

That's strange, even on the video that you've attached I can see that you get both, onTouchesCancelled and onFinalize callbacks. Moreover, onFinalize has second parameter, representing if gesture ended successfully, or not. In my case I get false:

https://github.com/user-attachments/assets/83ec05b3-6180-4bb3-964a-ccd916639813

As a side note, you can simplify your component like this:

<ScrollView contentContainerStyle={styles.content}>
  <GestureDetector gesture={panGesture}>
    <Animated.View style={[styles.box, animatedStyle]} />
  </GestureDetector>
</ScrollView>
MatiPl01 commented 2 months ago

Hi! I've looked into your repro. The thing is, on iOS Gesture Handler uses native recognizers and in case of ScrollView it has about ±150ms window in which pan gesture will be recognized as scroll. I don't think there's much that we can do about it 😞 (cc @j-piasecki).

Before this time elapses, the Pan gesture calls just the onTouchesDown and onBegin callbacks and no other callback is called later on

That's strange, even on the video that you've attached I can see that you get both, onTouchesCancelled and onFinalize callbacks. Moreover, onFinalize has second parameter, representing if gesture ended successfully, or not. In my case I get false:

Nagranie.z.ekranu.2024-08-16.o.11.46.53.mov As a side note, you can simplify your component like this:

<ScrollView contentContainerStyle={styles.content}>
  <GestureDetector gesture={panGesture}>
    <Animated.View style={[styles.box, animatedStyle]} />
  </GestureDetector>
</ScrollView>

onFinalize and onTouchesCancelled are fired only after you release the finger but (at least to me) they should be called before releasing the finger.

See how this can be problematic on the following recording. The ScrollView recognizes a gesture but the pan gesture doesn't call any of callbacks when the ScrollView starts handling pan:

https://github.com/user-attachments/assets/3ab0ce87-67c0-416b-8232-ed6c760eb3ce

I got it working by using the Manual gesture which I can cancel manually when I know that the pan gesture would not be handled but still it'd better if pan gesture could be handled after scroll. I understand that this might be not possible on iOS, though, so we can close this issue if nothing more can be done.

As a side note, you can simplify your component like this

Yeah, I know I can simplify code but this is just a repro and I copy-pasted code from my other project (where I need 2 separate components), did some cleanup and didn't care about the simplest implementation.

m-bert commented 2 months ago

(...) but still it'd better if pan gesture could be handled after scroll.

I don't think it is something that we will be able to achieve right now 😞 As you can see, there's no touchesMoved nor onChange callback, so it might be hard to manipulate scroll without access to them.

I understand that this might be not possible on iOS, though, so we can close this issue if nothing more can be done.

While I can't see good solution to this problem, maybe there is one. Please leave it open until @j-piasecki responds - then we can either close it, or try something else 😅

j-piasecki commented 2 months ago

but still it'd better if pan gesture could be handled after scroll.

The problem here is that the scroll is still technically active (if you set bounces={false} on the ScrollView, the issue is gone. The weird thing here is that UIKit calls onTouchesBegan, where we trigger onTouchesDown and onBegin callbacks, but it doesn't do anything else. From what I was able to find, it doesn't in any way inform us that the gesture got effectively canceled.

I think it's because we're in Possible state, so it assumes no actions were done. If that's true, then it's a weird side effect of mapping UIKit states to RNGH states.

As for your use case, wouldn't moving the highlighting logic to onStart work, since it's not triggered while the scroll is still active? You could try combining it with activateAfterLongPress.

As for the issue, I think we can keep it open as it should be fixable. The relevant info must exist somewhere since the pan recognizer receives down event but doesn't receive any subsequent move events. It's just a matter of finding it, or figuring out that Apple in all it's generosity decided that we mere developers aren't worthy enough to access it.

MatiPl01 commented 2 months ago

but still it'd better if pan gesture could be handled after scroll.

The problem here is that the scroll is still technically active (if you set bounces={false} on the ScrollView, the issue is gone. The weird thing here is that UIKit calls onTouchesBegan, where we trigger onTouchesDown and onBegin callbacks, but it doesn't do anything else. From what I was able to find, it doesn't in any way inform us that the gesture got effectively canceled.

I think it's because we're in Possible state, so it assumes no actions were done. If that's true, then it's a weird side effect of mapping UIKit states to RNGH states.

As for your use case, wouldn't moving the highlighting logic to onStart work, since it's not triggered while the scroll is still active? You could try combining it with activateAfterLongPress.

As for the issue, I think we can keep it open as it should be fixable. The relevant info must exist somewhere since the pan recognizer receives down event but doesn't receive any subsequent move events. It's just a matter of finding it, or figuring out that Apple in all it's generosity decided that we mere developers aren't worthy enough to access it.

I wanted to start the item scale change animation when it is touched with a slight delay (I used withDelay inside the onTouchesDown callback to start the animation after a slight delay and cancelled the animation if onTouchesUp or onTouchesCancelled was called).

I used the Pan gesture together with the activateAfterLongPress function but I wanted to keep the scale animation separate. When the Pan gesture was activated, the onUpdate function started being called with translation offsets (onUpdate starts being called right after onStart, so I cannot move my logic to onStart as the scale animation must begin some time before).

I would try to experiment with activateAfterLongPress a bit more. Maybe I can just activate the Pan gesture earlier, move the scale animation to onStart and ignore updates from onUpdate as long as the scale animation is not finished.

Thank you @m-bert and @j-piasecki for your explanations!