-
-
Notifications
You must be signed in to change notification settings - Fork 980
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
It's hard to understand how to compose gestures with ScrollView #2616
Comments
Hey! 👋 The issue doesn't seem to contain a minimal reproduction. Could you provide a snack or a link to a GitHub repository under your username that reproduces the problem? |
Here's the flow:
I try search again: This "replacing waitFor" link seems promising (although it implies that maybe I'm not supposed to use This page has some mentions of |
I'm not sure I understand the intended design for |
My current workaround looks like this: bluesky-social/social-app@115d60d. This makes it so that I can pinch into individual images in the gallery even though there's a scroll view around them. This still doesn't work like I want though. The behavior I want is:
|
I've seen I tried wrapping it around my const native = Gestures.Native()
.enabled(!isZoomed)
// ...
renderScrollComponent={props =>
<GestureDetector gesture={native}>
<ScrollView {...props} />
</GestureDetector>
} This didn't work — it told me that I'm only allowed to have one native child inside. But this does look like a single child to me — is it not? I tried adding |
Hi!
The approach you described is the best considering what the current API allows you to do (although I don't quite see how callback refs would help clean up in this case). That said we want to add an option to specify
That's a bit tricky. That would be the case assuming there is an active gesture at the time, since in that case Gesture Handler is the only thing handling touch events from the OS, and it would'n deliver them to the ScrollView. However, if there's no gesture active, Gesture Handler doesn't have the exclusivity for handling touch, so it falls back on system behavior - even if native gesture is disabled, falling back on the default system behavior would deliver the events to the scroll view. Although, why not use
I'm not sure why it errored out in this case, will look into it. And as for the question from the original post:
It's a race condition between the two recognizers - Scroll and pinch. One activates based on the translation of the finger, the other based on the distance between two fingers, whichever threshold is met first decides which will activate. Native views have mechanisms for handling that, but Gesture Handler itself doesn't have a concept of hierarchy - it doesn't know that the pinch is attached to a child of the ScrollView. It simply maps handlers to the views they are attached to. |
Alright, so I'm going to document my entire journey here in case it helps guide the API design and/or examples. To clarify, I was not able to find a fully satisfactory solution with the provided primitives. Part 1: No ScrollViewThe code we're starting off will have no scroll view. Essentially it's just an Expo Image with a single composed gesture recognizer. This example is very much based on @j-piasecki's code in #2138 (comment). Here is the source code for the gesture configuration setup: const pinch = Gesture.Pinch()
.onStart((e) => {
// ...
})
.onChange((e) => {
// ...
})
.onEnd(() => {
// ...
});
const pan = Gesture.Pan()
.averageTouches(true)
.onChange((e) => {
// ...
})
.onEnd(() => {
// ...
});
const doubleTap = Gesture.Tap()
.numberOfTaps(2)
.onEnd((e) => {
// ...
});
const dismissSwipePan = Gesture.Pan()
.enabled(!isScaled)
.activeOffsetY([-10, 10])
.failOffsetX([-10, 10])
.maxPointers(1)
.onUpdate(e => {
// ...
})
.onEnd((e) => {
// ...
});
return (
<View>
<GestureDetector gesture={Gesture.Exclusive(dismissSwipePan, Gesture.Simultaneous(pinch, pan), doubleTap)}> (If you'd like to run the app, I'm happy to send an invite code.) Here's how it works: ezgif.com-resize.movTurn on the sound for commentary. Here's a summary:
This is not a showstopper but it's the first thing that kinda sucks. There's going to be a bit more trouble later on though. Part 2: Adding a ScrollView aboveNow let's add a bluesky-social/social-app@d9489c3 Here's the video of the new behavior: ezgif.com-resize.movTurn on the sound for commentary. Here's a summary:
Here the main showstopper issues are:
So it's kind of the opposite of the behavior we want. Let's see if we can find some fix. Part 3: Trying
|
I suspect I can maybe knock out the two remaining issues if I reimplement pinch/pan manually as a single continuous gesture instead of a composition of |
While thinking about this problem:
I accidentally stumbled upon the "manual activation" pattern. I wouldn't have guessed it's possible because My idea is to try something like this: const pan = Gesture.Pan()
.averageTouches(true)
- .enabled(isScaled)
+ .manualActivation(true)
+ .onTouchesDown((e, manager) => {
+ if (e.numberOfTouches > 1 || isScaled) {
+ manager.activate()
+ }
+ })
.onChange((e) => { Essentially, I want to always enable the pan gesture if we start with two fingers — even if we're not zoomed in yet. I don't think this exact solution works though — it breaks the "double tap" gesture. I assume it's because now we're activating the pan "too eagerly" and we should in fact check whether the fingers have panned enough distance. That feels like reimplementing the pan recognizer so not super fun but maybe I'll take a crack at it later. I hope there's some easier way. I also realized there's another problem I haven't yet described. If I start swiping the scroll view (to switch between items) and then start pinching midway, the image scales. However, pinching (and frankly, any of my own gestures) should be completely disabled once the scroll view is moving. Haven't found an easy way to do this. I guess I could listen to scroll view events and set some state variable so that the gestures are disable while scroll view isn't at rest. It would be nice if there was an easier way to disable all of my gestures while the native gesture is ongoing. |
It would be nice if there was an API like |
I think I may have found another way to solve the "pan during first pinch" issue: const pan = Gesture.Pan()
.averageTouches(true)
- .enabled(isScaled)
+ .minPointers(isScaled ? 1 : 2) This gives me the "allow panning, but only when pinched in" behavior. |
I was hoping that maybe I ended up going with this slightly weird approach: bluesky-social/social-app@3c2654a. The idea is to define a gesture that "eats" any gestures when the container isn't positioned exactly at the viewport (i.e. while it's being scrolled or has momentum): const consumeHScroll = Gesture.Manual()
.onTouchesDown((e, manager) => {
const measurement = measure(containerRef);
if (!measurement || measurement.pageX !== 0) {
manager.activate();
} else {
manager.fail();
}
}) Then this gesture is fed to my Open to other solutions. |
Here's what I ended up with: bluesky-social/social-app#1624 |
Description
I've been scratching my head at this for the last few days so I figured I'd report.
Basically, my problem looks like #1082. I have a horizontal
ScrollView
, and it "steals" horizontal pinches inside of it. ThePinch
gesture is inside individual items, but for some reason the outerScrollView
's scroll wins even though I'm definitely pinching (with two fingers).I'm at a loss about how to debug this or even think about this.
There's something that maybe looks like a fix in #1034, but I'll join the chorus of people who didn't understand what the fix is and how to use it. The comment says to use
simultaneousHandlers
. But isn't that the opposite of what the OP wanted? The OP wanted to have the pinch gesture "win" and prevent the scrolling. The documentation forsimultaneousHandlers
seems to imply it's for allowing two gestures to be active at the same time — which is not what the OP wanted. So it's unclear how that fix is related to the issue.There's also #2370 which seems related but was not reviewed. The problem in the video there seems to be exactly the same as what I'm experiencing (except it's vertical rather than horizontal). I have no idea if that fix makes sense though.
There are some scattered examples in the Issues of using
Gesture.Native()
,waitFor
,simultaneousWithExternalGesture
,ScrollView
(undocumented?), and so on. For example, #2332 (comment). I found it very difficult to understand how these APIs are supposed to be used based on the docs. Do I want the built-in RN scroll view to "wait for" my pinch gesture inside it? Shouldn't the innermost gesture always win anyway? If I have many items in the list, do I need a ref to a gesture inside every item, and then somehow coordinate those refs with theScrollView
above? Do I need to be usingScrollView
fromreact-native-gesture-handler
for gestures to work at all? If I search forwaitFor
in the docs, I seem to only be getting results with outdated APIs (like this) which makes it further confusing.Anyway, tldr is:
I hope this can be improved somehow! I'll try to see if I can find some way that works.
Steps to reproduce
irrelevant
Snack or a link to a repository
irrelevant
Gesture Handler version
2.12.1
React Native version
0.72.4
Platforms
Android
JavaScript runtime
Hermes
Workflow
None
Architecture
None
Build type
None
Device
None
Device model
No response
Acknowledgements
Yes
The text was updated successfully, but these errors were encountered: