To summarize what I am trying to accomplish I sketched this diagram:
I am trying to build a custom navigation. In this navigation, I have a main page. Above this main page there sits another page, not visible to the user yet (as the main page fills the whole screen). The main page has scrollable content on it.
There is one component at the top of the main page, that the user can drag to swipe the other/upper page down.
What I have tried so far to accomplish this
I already have an implementation, that works but has some drawbacks.
- The most outer
ScrollViewhasscrollEnabled=false - The draggable component is an
Animated.Viewto which I applied aPanResponderto - Once the user clicks and drags the draggable component down, the PanResponder captures this movement
- I use the captured movement and do
outerScrollView.scrollTo(...)--> on iOS this works fine but on android I see some small jittering during the scroll movement.
interface Props extends ScrollViewProps {
draggableHeader: ReactNode;
}
const { width, height } = Dimensions.get("window");
export const SwipingNavigator: React.FC<Props> = (props) => {
const scrollView = useRef<ScrollView>();
const scrollToMainPage = (animated = true): void =>
scrollView.current.scrollTo({ x: 0, y: height, animated });
const scrollToUpperPage = (): void =>
scrollView.current.scrollTo({
y: 0,
x: 0,
animated: true,
});
//when component mounts, scroll to main page
useEffect(() => {
scrollToMainPage(false);
}, []);
const panResponder = useRef(
PanResponder.create({
onMoveShouldSetPanResponder: (e, gestureState) => gestureState.dy > 3,
//move the content as the user drags the header
onPanResponderMove: ({ nativeEvent }) => {
scrollView.current.scrollTo({
y: height - nativeEvent.pageY + 40,
x: 0,
animated: false,
});
},
onPanResponderEnd: ({ nativeEvent }) => {
//detect to which page the user wants to scroll -> snap effect
if (nativeEvent.pageY < height / 3) {
scrollToMainPage();
} else {
scrollToUpperPage();
}
},
})
).current;
//assuming child 1 is the main page, child 0 the upper page
return (
<ScrollView
ref={scrollView}
scrollEnabled={false}
bounces={false}
pagingEnabled={true}
disableScrollViewPanResponder={true}
showsVerticalScrollIndicator={false}
{...props}
style={globalStyles.flex1}>
{props.children[0]}
<View style={styles.fs}>
<Animated.View {...panResponder.panHandlers}>
{props.draggableHeader}
</Animated.View>
{props.children[1]}
</View>
</ScrollView>
);
};
const styles = StyleSheet.create({
fs: {
flex: 1,
width,
height,
},
});
As already said: the main issue is that it doesn't look that great on android performance-wise + this is probably not the best solution.
Scrolling down from the other page isn't a concern, as it will have a "close"-button.
Other ideas I had
I thought about using the Pan/Gesture-Handler of the draggable component and "inject" this to the ScrollView. So that the scroll view will only detect movements from this panhandler.
I am currently just stuck and I appreciate every new input you can give to me. Thanks in advance for your help :)
// Edit: here is the snack: https://snack.expo.io/@hannojg/surprised-bacon Unfortunately, on expo, it looks butter smooth on android. However, rendering the main page on a real device with RN make the scrolling look different on android.