Reputation: 408
I am currently trying to position a horizontal ScrollView within the content of the react-navigaion material-top-tabs (which also scrolls horizontally).
When dragging within the horizontal ScrollView, only the ScrollView should be affected and scroll.
Sometimes when dragging within the horizontal ScrollView, the entire top tabs scroll. (The tab is being switched) which is a nightmare for UX.
Do you know of any way to make it work the way it is intended?
Code Snippets:
Navigation.js
// Importing Top Tabs creator
import { createMaterialTopTabNavigator } from "@react-navigation/material-top-tabs";
...
// Creating the Tab Navigator
const Tab = createMaterialTopTabNavigator();
...
// Configure Navigator
<Tab.Navigator
initialRouteName="Tab 1"
screenOptions={{
headerShadowVisible: false,
headerStyle: {
backgroundColor: colors.background,
},
}}
// Using custom TabBar component
tabBar={(props) => <TabBar {...props} />}
>
// The horizontal ScrollView is in "Tab 1"
<Tab.Screen
name="Tab 1"
component={Screen1}
options={{
headerShown: false,
unmountOnBlur: true,
}}
/>
...
<Tab.Screen
name="Tab 4"
component={Screen4}
options={{
headerShown: false,
unmountOnBlur: true,
}}
/>
</Tab.Navigator>
HorizontalScrollView.js
<ScrollView
style={{
display: "flex",
flexDirection: "row",
backgroundColor: colors.background,
paddingHorizontal: 10,
}}
horizontal
showsHorizontalScrollIndicator={false}
overScrollMode="never"
>
...
</ScrollView>
Upvotes: 8
Views: 2299
Reputation: 2471
What worked for me was using the ScrollView component imported from 'react-native-gesture-handler' instead of 'react-native'.
I didn't have to do any additional setup, and it works fine.
import {ScrollView} from 'react-native-gesture-handler';
Upvotes: 2
Reputation: 130
I struggled with this issue too for a few days. For what I can tell, this issue isn't specific to material top tabs or pagerview. If you place a horizontal scrollview or a flatlist into a vertical scrollview, the horizontal gesture gets sometimes stolen by the parent vertical scrollview.
I noticed that the onResponderTerminate callback gets called on the horizontal scrollview. React native provides a way to prevent the termination by passing the onResponderTerminationRequest={(event) => false} prop but that doesn't seem to do anything. The termination callback gets still called. The first bug reports of this are over 6 years old and I didn't find any working fixes either.
A temporary workaround is to build your own scrollview by using react-native-gesture-handler and react-native-reanimated. Down below is a complete example of how to make one. Please note that Animated has to be imported from react-native-reanimated. If you need to use Animated from react-native, import it by using an alias --> import {Animated as Anim} from 'react-native';
import React from 'react';
import { Animated as Anim, ScrollView, Text, View } from 'react-native';
import Animated, {
useAnimatedStyle,
useSharedValue,
withDecay,
withSpring
} from "react-native-reanimated";
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
export const MyTab = () => {
// How many points the view is scrollable.
// In other words, how many points the view overflows.
// You might need to dynamically update this
// This can be calculated by subtracting screen width from the view width.
// If the view doesn't overflow, set this to zero.
const SCROLLABLE_WIDTH = 200;
const animatedX = useSharedValue(0);
const xContext = useSharedValue(0);
const animStyle = useAnimatedStyle(() => {
return {
transform: [{ translateX: animatedX.value }]
}
})
const panGesture = React.useMemo(() => Gesture.Pan()
.activeOffsetX([-17, 17]) // 17 is the optimal value. Anything higher might cause a tab change
.activeOffsetY([-22, 22]) // Allows the vertical scrollview to take over when swiping vertically
.maxPointers(1)
.onStart((e) => {
xContext.value = animatedX.value;
})
.onUpdate((e) => {
const target = xContext.value + e.translationX;
if (target > 0) {
animatedX.value = target * 0.3;
}
else if (target < -SCROLLABLE_WIDTH) {
animatedX.value = -SCROLLABLE_WIDTH + (SCROLLABLE_WIDTH + target) * 0.3;
}
else {
animatedX.value = target;
}
})
.onEnd((e) => {
const target = xContext.value + e.translationX;
if (target > 0) {
animatedX.value = withSpring(0, { mass: 0.3, stiffness: 110 });
}
else if (target < -SCROLLABLE_WIDTH) {
animatedX.value = withSpring(-SCROLLABLE_WIDTH, { mass: 0.3, stiffness: 110 });
}
else {
animatedX.value = withDecay({
velocity: e.velocityX,
clamp: [-SCROLLABLE_WIDTH, 0], // optionally define boundaries for the animation
});
}
}),
[] // Set here your useStates required in the gesture
);
return (
<ScrollView style={{}}>
<GestureDetector
gesture={panGesture}
>
{/* If no static container is set, a tab change might initialize
If you need to hide overflow set overflow: "hidden" to the container style */}
<Animated.View style={{ marginVertical: 50 }}>
<Animated.View
style={[{
flexDirection: "row"
},
animStyle
]}>
<Text style={{ color: "#abcdef", fontSize: 28, marginHorizontal: 20 }}>
{"Horizontally\nscrollable\nitem 1"}
</Text>
<Text style={{ color: "#abcdef", fontSize: 28, marginHorizontal: 20 }}>
{"Horizontally\nscrollable\nitem 2"}
</Text>
<Text style={{ color: "#abcdef", fontSize: 28, marginHorizontal: 20 }}>
{"Horizontally\nscrollable\nitem 3"}
</Text>
<Text style={{ color: "#abcdef", fontSize: 28, marginHorizontal: 20 }}>
{"Horizontally\nscrollable\nitem 4"}
</Text>
</Animated.View>
</Animated.View>
</GestureDetector>
</ScrollView>
)
}
Upvotes: 3