简体   繁体   中英

ReactNative Horizontal ScrollView within material-top-tabs (nested ScrollView)

I am currently trying to position a horizontal ScrollView within the content of the react-navigaion material-top-tabs (which also scrolls horizontally).

The expected behavior:

When dragging within the horizontal ScrollView, only the ScrollView should be affected and scroll .

Current behavior:

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM