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>
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. Amazingly, 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>
)
}
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.