I'd like to have a ScrollView within a PanGestureHandler where the ScrollView is scrollable but whenever scrollY <= 0 (and its not a momentum scroll) the PanGestureHandler should handle the gesture.
It's kind of hard to explain, so here is a video: https://streamable.com/hplw6
How could this be achieved?
This is my implementation:
enable state, set it to false when ScrollView offset <= 0failed state) ScrollView still waits for it. So I have ScrollView wait for another one when enable = false, (for itself in this case)onScroll event to track scroll offsetimport React from "react";
import { PanGestureHandler, State, ScrollView } from 'react-native-gesture-handler';
class Example extends React.Component {
ref = React.createRef();
scrollRef = React.createRef();
state = {
enable: true
};
_onScrollDown = (event) => {
if (!this.state.enable) return;
const {translationY} = event.nativeEvent;
// handle PanGesture event here
};
_onScroll = ({nativeEvent}) => {
if (nativeEvent.contentOffset.y <= 0 && !this.state.enable) {
this.setState({enable: true });
}
if (nativeEvent.contentOffset.y > 0 && this.state.enable) {
this.setState({enable: false});
}
};
render () {
const { enable } = this.state;
return (
<ScrollView
ref={this.scrollRef}
waitFor={enable ? this.ref : this.scrollRef}
scrollEventThrottle={40}
onScroll={this._onScroll}
>
<PanGestureHandler
enabled={enable}
ref={this.ref}
activeOffsetY={5}
failOffsetY={-5}
onGestureEvent={this._onScrollDown}
>
</PanGestureHandler>
</ScrollView>
);
}
}
I am trying to achieve the same effect as in the video above. Controlling enabled prop is too slow, because requires component update. It would be better if the feature would be implemented on the native side using PanGestureHandler.
Judging by the amount of apps with this kind of bottom panels with scrollable content, I'd say it's quite a popular case. Maybe the lib already allows to implement that somehow. Would really appreciate any feedback from the library authors.
Using scrollview/flatlist from the gesture library permits this to work well on iOS but doesn't seem to work in Android. @hssrrw is correct that controlling enabled is too slow and the pangesturehandler responds to quick before its disabled. If we could define a region within a pangesturehandler that won't respond that could work in many if not most use cases.
I achieved this in another way by using interpolate and transform on an outer view to track the pangesturehandler transY position. Then only wrap the upper region above the scrollview with the pangesturehandler.
I've switched to using the interactable object now and I can't quite grasp how to achieve the same just yet but I'll work on it.
I know this isn't ideal for everyone but this should cover various use cases. I've modified the render output of Interactable.js to something like this:
render() {
return (
<Animated.View style={[
this.props.style,
{
transform: [
{
translateX: this.props.verticalOnly ? 0 : this._transX,
translateY: this.props.horizontalOnly ? 0 : this._transY,
},
],
},
]}>
<PanGestureHandler
maxPointers={1}
enabled={this.props.dragEnabled}
onGestureEvent={this._onGestureEvent}
onHandlerStateChange={this._onGestureEvent}
>
<Animated.View>
{this.props.header}
</Animated.View>
</PanGestureHandler>
<Animated.View>
{this.props.body}
</Animated.View>
</Animated.View>
);
}
The above is just an inversion of the pangesturehandler and the transforming view with the body separated out of the pangesturehandler. The idea is that you take the header section of your draggable object, which includes the handler bar, and insert it as your this.props.header and the rest of the contents, including scrollview, likewise into this.props.body of your component like so:
<Interactable.View
header={this.renderHeader}
body={this.renderBody}
verticalOnly={true}
style={[ styles.interactableContainer ]}
snapPoints={[ { y: this.topSnap }, { y: this.bottomSnap }, ]}
boundaries={{ top: this.topSnap }}
initialPosition={{ y: this.bottomSnap }}
animatedValueY={this._deltaY} />
This way, you can still have as large an area to let the user drag your object up or down but the pangesturehandler won't interfere with the user's scrolling of the contents of that object. The only disadvantage is that the user can't drag the object up or down directly in the scrollview area. In many use cases, like ours, this is exactly what you don't want to happen so this works.
Tested on both Android and iOS and works great.
I'm also stuck on Android. I have a PanGestureHandler which has a descendent which is a ScrollView, but I cannot scroll in this ScrollView, the pan always is taking precedence. I even created a ref and passed it deeply down and then did:
<PanGestureHandler waitFor={ref}>
....
<ScrollView ref={ref} />
....
</PanGestureHandler>
I'm on Android, using RN 0.59.5.
Take a look at the bottom sheet example, it seems to be what you want https://github.com/kmagiera/react-native-gesture-handler/blob/master/Example/bottomSheet/index.js
The behaviour is also implemented by this library https://github.com/osdnk/react-native-reanimated-bottom-sheet
Although the bottom sheet example seemed a bit buggy when I tested it (jumped around if you scrolled it up and down really fast) and reanimated-bottom-sheet doesn't support nesting a ScrollView. The nested view will scroll, but it has to be a plain View without its own scrolling behaviour.
am trying to achieve the same effect as in the video above. Controlling
enabledprop is too slow, because requires component update. It would be better if the feature would be implemented on the native side using PanGestureHandler.Judging by the amount of apps with this kind of bottom panels with scrollable content, I'd say it's quite a popular case. Maybe the lib already allows to implement that somehow. Would really appreciate any feedback from the library authors.
I totally agree with @hssrrw , I think there are really common usecases where you want to toggle a gesture handler and right now you need to come back to the JavaScript thread to do that. And even by doing that, I have to admit it was really buggy when I needed to do a setState while doing a gesture. Moreover, because of those setState I've needed to use use-memo-one or move to class components to memoize all the animated values.
I'm also stuck on Android. I have a
PanGestureHandlerwhich has a descendent which is aScrollView, but I cannot scroll in this ScrollView, the pan always is taking precedence. I even created a ref and passed it deeply down and then did:<PanGestureHandler waitFor={ref}> .... <ScrollView ref={ref} /> .... </PanGestureHandler>I'm on Android, using RN 0.59.5.
Did you fix it ? same issue on android
Any update about this case ?
Our results are confusing. On a Samsung S9 and S10, the scrollview from RN works. But on all other phones we tested including Pixel, scrollview from Gesture Handler works. The S9 doesn't work with scrollview when imported from Gesture Handler though.
@pribeh i got similar results.
All I did was change my import from
import { ScrollView } from 'react-native';
to
import { ScrollView } from 'react-native-gesture-handler';
and it worked.
For people experiencing issues on Android, make sure the hitbox of your ScrollView content is where you expect it to be, explaining:
Sometimes eventhough you can see a view, the hitbox of that view (where the user can click/tap on) is not the same as what you're actually seeing.
To troubleshoot, make sure you pass overflow: 'hidden' on all the child views of your ScrollView and check wether after you do that, a view that should respond to the gesture you're invoking becomes hidden.
In our case we had a View inside a LongPressGestureHandler that when passed overflow: 'hidden' became invisible. All we had to do was re-arrange our view structure, pass flexGrow: 1 to that view, and pretty much make it visible even with overflow: 'hidden' turned on.
As soon as we did that the hitbox was the same as the visible area of that view, and gestures started working fine again.
Can confirm that what @SudoPlz suggested work as intended, my nested scrollview now captures touches. Thanks!
@SudoPlz we tried the same but it doesn't work for Samsung S9 and S10s. Let us know if your seeing different results.
I've managed to get this working with a mixture of NativeViewGestureHandler (more info here: https://github.com/software-mansion/react-native-gesture-handler/issues/492) and not directly nesting gesture handlers (more info here: https://github.com/software-mansion/react-native-gesture-handler/issues/71).
My eventual structure (with stuff removed) to nest a ScrollView with nested FlatLists inside a PanGestureHandler was:
<PanGestureHandler
ref={this.panGestureHandlerRef}
simultaneousHandlers={this.nativeHandlerRef}
>
<Animated.View style={{ flex: 1 }}>
<NativeViewGestureHandler
ref={this.nativeHandlerRef}
simultaneousHandlers={this.panGestureHandlerRef}
>
<Animated.ScrollView
ref={this.wrapperRef}
horizontal
>
// Array of flatlists also here
</Animated.ScrollView>
</NativeViewGestureHandler>
</Animated.View>
</PanGestureHandler>
Hope this helps!
@pribeh check my edited post, it may help you.
@SudoPlz Do you have a working example of this nested scrollview that you could share with us?
All I did was change my import from
import { ScrollView } from 'react-native';
to
import { ScrollView } from 'react-native-gesture-handler';and it worked.
In my case too...
This can be solved if ScrollView component gives access to pan gesture by implementing onGestureEvent prop.
TypeScript definitions says this is posible but in practice is not supported
I'm gonna leave this here, it might help some one...
<PanGestureHandler
activeOffsetX={[-10, 10]}
/* or */
activeOffsetY={[-10, 10]}
>
@a-eid Works !!!!!
I think this can be closed now.
https://github.com/software-mansion/react-native-gesture-handler/blob/master/examples/Example/bottomSheet/index.js#L55
The key here is to add compensation of _reverseLastScrollY to translateY
Most helpful comment
All I did was change my import from
import { ScrollView } from 'react-native';to
import { ScrollView } from 'react-native-gesture-handler';and it worked.
Edit:
For people experiencing issues on Android, make sure the hitbox of your
ScrollViewcontent is where you expect it to be, explaining:Sometimes eventhough you can see a view, the hitbox of that view (where the user can click/tap on) is not the same as what you're actually seeing.
To troubleshoot, make sure you pass
overflow: 'hidden'on all the child views of yourScrollViewand check wether after you do that, a view that should respond to the gesture you're invoking becomes hidden.In our case we had a
Viewinside aLongPressGestureHandlerthat when passedoverflow: 'hidden'became invisible. All we had to do was re-arrange our view structure, passflexGrow: 1to that view, and pretty much make it visible even withoverflow: 'hidden'turned on.As soon as we did that the hitbox was the same as the visible area of that view, and gestures started working fine again.