-
-
Notifications
You must be signed in to change notification settings - Fork 972
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
Pan gesture inside a ScrollView blocks scrolling #1933
Comments
Hi! Gestures blocking scrolling when they are placed inside ScrollView is by design, otherwise you wouldn't be able to use them. const TOUCH_SLOP = 5;
const TIME_TO_ACTIVATE_PAN = 400;
const DragComponent = (props) => {
const touchStart = useSharedValue({ x: 0, y: 0, time: 0 });
const gesture = Gesture.Pan()
.manualActivation(true)
.onTouchesDown((e) => {
touchStart.value = {
x: e.changedTouches[0].x,
y: e.changedTouches[0].y,
time: Date.now(),
};
})
.onTouchesMove((e, state) => {
if (Date.now() - touchStart.value.time > TIME_TO_ACTIVATE_PAN) {
state.activate();
} else if (
Math.abs(touchStart.value.x - e.changedTouches[0].x) > TOUCH_SLOP ||
Math.abs(touchStart.value.y - e.changedTouches[0].y) > TOUCH_SLOP
) {
state.fail();
}
})
.onUpdate(() => {
console.log('pan update');
});
return (
<GestureDetector gesture={gesture}>
<View>{props.children}</View>
</GestureDetector>
);
};
const Main = () => {
return (
<ScrollView>
<DragComponent>
<View style={{ width: '100%', height: 400, backgroundColor: 'red' }} />
</DragComponent>
<View style={{ width: '100%', height: 1000, backgroundColor: 'blue' }} />
</ScrollView>
);
}; |
Amazing! Thank you! I wanted to avoid implementing a LongPress behavior so I did the following and it works great!
|
Great! And yeah, your solution looks much cleaner. Since you've solved the problem, I'll close the issue. |
@ethanshar @j-piasecki Apologies for bringing this issue back from the dead, but I'm having the exact same problem (need to detect pan gestures inside a scrollview) and the I've tried both of your solutions with the same results. I have an example repo with this problem https://github.com/Jorundur/expo-gesture-handler-skia-test, just need to run Can you see what's incorrect with my implementation? Any help would be much appreciated! |
@Jorundur You're missing export default function App() {
return (
<GestureHandlerRootView style={styles.container}>
<ScrollView>
<Slider height={400} width={400} />
<View style={styles.box} />
<View style={styles.box} />
<View style={styles.box} />
</ScrollView>
</GestureHandlerRootView>
);
} It seems to be working correctly. |
@j-piasecki Unfortunately that still doesn't make it work, but thank you for pointing that out though, it was of course missing! I've pushed that up to my repo now. The issue can still be seen when you try to scroll starting from the graph area, i.e. if you press it and start scrolling before 250ms have passed (since that's when the longPress activates and the pan for the graph kicks in). The screen doesn't scroll for me at all if I try scrolling when touching the graph area. Also - I have a question about something you said in a previous comment:
This makes a lot of sense for probably most use cases, for example the original one in this issue where a user has to drag some stuff around inside a
Is there any way to allow |
Oh, sorry I didn't notice that. I've tried Android first and the gestures didn't work at all without the root view so I figured it must've been it 😅. As for your question, yes you can make Here's what I didimport { StyleSheet, Text, View } from "react-native";
import {
Skia,
Group,
useComputedValue,
useValue,
Line,
Canvas,
Circle,
Fill,
LinearGradient,
Path,
vec,
useSharedValueEffect,
} from "@shopify/react-native-skia";
import React, { useMemo, useRef } from "react";
import {
Gesture,
GestureDetector,
GestureHandlerRootView,
ScrollView
} from "react-native-gesture-handler";
import { useSharedValue } from "react-native-reanimated";
export default function App() {
const panRef = useRef(null)
const lpRef = useRef(null)
return (
<GestureHandlerRootView style={styles.container}>
<ScrollView simultaneousGestures={[panRef, lpRef]}>
<Slider height={400} width={400} panRef={panRef} lpRef={lpRef} />
<View style={styles.box} />
<View style={styles.box} />
<View style={styles.box} />
</ScrollView>
</GestureHandlerRootView>
);
}
const Slider = ({ height, width, panRef, lpRef }) => {
const path = useMemo(
() => createGraphPath(width, height, 60, false),
[height, width]
);
const touchPos = useValue(
getPointAtPositionInPath(width / 2, width, 60, path)
);
const lineP1 = useComputedValue(
() => vec(touchPos.current.x, touchPos.current.y + 14),
[touchPos]
);
const lineP2 = useComputedValue(
() => vec(touchPos.current.x, height),
[touchPos]
);
const xPosShared = useSharedValue(width / 2);
useSharedValueEffect(() => {
touchPos.current = getPointAtPositionInPath(
xPosShared.value,
width,
60,
path
);
}, xPosShared);
const isDragging = useSharedValue(false);
const longPressGesture = Gesture.LongPress()
.onStart(() => {
isDragging.value = true;
})
.minDuration(250)
.withRef(lpRef);
const dragGesture = Gesture.Pan()
.manualActivation(true)
.onTouchesMove((e, state) => {
if (isDragging.value) {
state.activate();
xPosShared.value = e.changedTouches[0].x;
} else {
state.fail();
}
})
.onStart(() => {
console.log("onStart!");
})
.onUpdate((event) => {
console.log("onUpdate!");
})
.onEnd(() => {
console.log("onEnd!");
})
.onFinalize(() => {
isDragging.value = false;
})
.withRef(panRef)
.simultaneousWithExternalGesture(longPressGesture);
const composedGesture = Gesture.Race(dragGesture, longPressGesture);
return (
<View style={{ height, marginBottom: 10 }}>
<GestureDetector gesture={composedGesture}>
<Canvas style={styles.graph}>
<Fill color="black" />
<Path
path={path}
strokeWidth={4}
style="stroke"
strokeJoin="round"
strokeCap="round"
>
<LinearGradient
start={vec(0, height * 0.5)}
end={vec(width * 0.5, height * 0.5)}
colors={["black", "#DA4167"]}
/>
</Path>
<Group color="#fff">
<Circle c={touchPos} r={10} />
<Circle color="#DA4167" c={touchPos} r={7.5} />
<Line p1={lineP1} p2={lineP2} />
</Group>
</Canvas>
</GestureDetector>
<Text>Touch and drag to move center point</Text>
</View>
);
};
const getPointAtPositionInPath = (x, width, steps, path) => {
const index = Math.max(0, Math.floor(x / (width / steps)));
const fraction = (x / (width / steps)) % 1;
const p1 = path.getPoint(index);
if (index < path.countPoints() - 1) {
const p2 = path.getPoint(index + 1);
// Interpolate between p1 and p2
return {
x: p1.x + (p2.x - p1.x) * fraction,
y: p1.y + (p2.y - p1.y) * fraction,
};
}
return p1;
};
const createGraphPath = (width, height, steps, round = true) => {
const retVal = Skia.Path.Make();
let y = height / 2;
retVal.moveTo(0, y);
const prevPt = { x: 0, y };
for (let i = 0; i < width; i += width / steps) {
// increase y by a random amount between -10 and 10
y += Math.random() * 30 - 15;
y = Math.max(height * 0.2, Math.min(y, height * 0.7));
if (round && i > 0) {
const xMid = (prevPt.x + i) / 2;
const yMid = (prevPt.y + y) / 2;
retVal.quadTo(prevPt.x, prevPt.y, xMid, yMid);
prevPt.x = i;
prevPt.y = y;
} else {
retVal.lineTo(i, y);
}
}
return retVal;
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center",
marginTop: 50,
},
box: {
height: 400,
width: 400,
backgroundColor: "blue",
margin: 4,
},
graph: {
flex: 1,
},
}); The bad news is, that you may need to eject to see it working as there is a problem with relations between gestures in the Expo Go app (it will work on the production build and there is a possibility that it will work when using a custom dev client). |
@j-piasecki That did the trick! Thank you so much 🎉 Since I got it to work simultaneously using this solution I even removed the long press gesture and only have the pan gesture now which is no longer manually activated. Now the pan and scrolling works perfectly at the same time. For me this even works in the Expo Go shell but I'll keep in mind that if gesture logic looks off in Expo Go then I should also try a dev-client build to see if it persists. |
Hello @j-piasecki, how do you get this to work with a third-party list library (say flash-list)? These libraries do not support |
Hey all, just thought I'd let you know I found another way which only requires a single Gesture plus doesn't require the long press trigger (tested on ios sim + physical device but not Android):
|
ı removed state.fail() and added it to onTouchesUp callback but it also works without state.fail(). I tested it android physical device and fully worked. |
Hello, |
my two cents - not a universal solution, but its simple and it worked for me as a way or recognising 'horizontal 'swipes' on a screen that vertically scrolled. As in swipe left to go forward a page, swipe right to go back. The problem was either the scrollview responder tended to swallow all events, preventing swipe recognition, or if I set my 'swipe handler' to capture events then it would swallow all events preventing scrolling. The solution was for the swipe handler to never respond true to onStartShouldSetPanResponderCapture. That way it wouldnt interfere with scrolling. In this situation, the swipe handler still gets calls to onMoveShouldSetPanResponder and I used this function to test dx/dy for something that looks like a swipe, and if it did, then I perfromed that swipe. The summary is you opt out of the start/move/release lifecycle of a gesture, and just sniff move for something that looks like the gesture you are interested in. the wrinkle is that move could be called multiple times within a single gesture, but once you decide that your gesture has been recognized, you only want to act on that one time, not once for each call to move. So just set a flag in onStartShouldSetPanResponderCapture to start listening for your gesture, and then clear that flag once it is recognized, so as to only act on it once. something list this:
|
My solution is working
|
Hi everyone, this is my solution. In all previous solutions, I've encountered a problem: the action fails if my finger doesn't move straight. My idea is to check isHorizontalPanning only within 100ms so that your finger can move smoothly without worrying about being perfectly straight P/S. Give me a reaction if you like this. const TIME_TO_ACTIVATE_PAN = 100
const touchStart = useSharedValue({ x: 0, y: 0, time: 0 })
const taskGesture = Gesture.Pan()
.manualActivation(true)
.onBegin(e => {
touchStart.value = {
x: e.x,
y: e.y,
time: Date.now(),
}
})
.onTouchesMove((e, state) => {
const xDiff = Math.abs(e.changedTouches[0].x - touchStart.value.x)
const yDiff = Math.abs(e.changedTouches[0].y - touchStart.value.y)
const isHorizontalPanning = xDiff > yDiff
const timeToCheck = Date.now() - touchStart.value.time
if (timeToCheck <= TIME_TO_ACTIVATE_PAN ) {
if (isHorizontalPanning) {
state.activate()
} else {
state.fail()
}
}
})
.onUpdate(event => (translateX.value = event.translationX))
.onEnd(() => {
const shouldBeDismissed = translateX.value < TRANSLATE_X_THRESHOLD
if (shouldBeDismissed) {
translateX.value = withTiming(TRANSLATE_X_THRESHOLD)
} else {
translateX.value = withTiming(0)
}
}) |
In my case, following improvisation worked. I added additional check of isDragging. If user is dragging the item, state will never fail unless user end the drag.
|
You can do something like this,
This will handle both gestures simultaneously, I am just using translationX in my myPanGesture gesture so i havent tried yet with others. |
Hello, I'm also facing a similar issue. I'm using react-native-reanimated-carousel to create a carousel. However, when I swipe left or right, the onPress event of the Item is affected. I used PanGestureHandler to handle the swipe of the Item, but then the swipe event of the carousel doesn't work anymore. Could you help me look into this case? Thank you very much.
ItemVideo
|
My perfect solutions imports.... type SwappableProps = { const THRESHHOLD = width * 0.05; const Swippable: React.FC = ({id, onOpen, close, children, enableLeftSwipe, enableRightSwipe, rightSwipeChild, leftSwipeChild}) => {
const animatedStyle = useAnimatedStyle(() => { return (
); export default Swippable; |
Your approach is fantastic! In 2024, I successfully solved this problem, and it's definitely the simplest solution, without a doubt! |
Description
I have a use case where I have a draggable element which I implement using Pan Gesture.
This element is being rendered inside a ScrollView and block scrolling when attempting to scroll in the pan area (of the element)
Assuming that the pan conflicts with the scroll I tried to approach it differently and added a LongPress gesture that once started enables the panning, so as long as the user didn't long press the element, the pan gesture should not block the scrolling.
I pretty much implemented this example with minor changes
https://docs.swmansion.com/react-native-gesture-handler/docs/gesture-composition#race
This is the LongPress gesture
This is the Pan gesture
And finally
I was thinking on invoking
dragGesture.enabled(false/true)
to enable/disable the panning, but TBH, I'm not sure where to call it.Any ideas how to approach this?
Platforms
Screenshots
Steps To Reproduce
Expected behavior
Actual behavior
Snack or minimal code example
This is a small code snippet that demonstrate the general issue of pan/scroll not working together.
Scrolling on the DragComponent area will not work, only below it will work
Package versions
The text was updated successfully, but these errors were encountered: