最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

react native - How should I prevent a horizontal swipe on a collapsible header with TabView? - Stack Overflow

programmeradmin2浏览0评论

I'm implementing a collapsible header with TabView where each tab contains a FlatList. The header (an Animated.View) overlaps the TabView, and I'm using pointerEvents="box-none" to allow vertical scroll events to pass through. However, horizontal swipes on the header area still trigger tab changes.

Current Behavior:

  • Vertical scrolling works correctly
  • Horizontal swipes on the header unintentionally switch tabs

Package Versions:

  • "react": "19.0.0"
  • "react-native": "0.78.0"
  • "react-native-reanimated": "^3.17.1"
  • "react-native-tab-view": "^4.0.6"
import React, { useCallback, useRef, useState } from 'react';
import { Animated, FlatList, LayoutChangeEvent, Text, TouchableOpacity, View } from 'react-native';

import { CollapsibleFlatList } from './CollapsibleFlatList';

import { TabBarProps, TabView } from 'react-native-tab-view';

const TABBAR_HEIGHT = 60;

type TabRoute = {
  key: string;
  title: string;
};

const tabRoutes: TabRoute[] = [
  { key: 'screen1', title: 'screen1' },
  { key: 'screen2', title: 'screen2' },
  { key: 'screen3', title: 'screen3' },
];

export const Curation = () => {
  const [headerHeight, setHeaderHeight] = useState(0);
  const [tabIndex, setTabIndex] = useState(0);
  const tabIndexRef = useRef(0);
  const isListGlidingRef = useRef(false);
  const listArrRef = useRef<Array<{ key: string; value: FlatList<any> | null }>>([]);
  const listOffsetRef = useRef<Record<string, number>>({});

  const scrollY = useRef(new Animated.Value(0)).current;

  const headerTranslateY = scrollY.interpolate({
    inputRange: [0, headerHeight],
    outputRange: [0, -headerHeight],
    extrapolate: 'clamp',
  });

  const tabBarTranslateY = scrollY.interpolate({
    inputRange: [0, headerHeight],
    outputRange: [headerHeight, 0],
    extrapolate: 'clamp',
  });

  const headerOnLayout = useCallback((event: LayoutChangeEvent) => {
    const { height } = event.nativeEvent.layout;
    setHeaderHeight(height);
  }, []);

  const onTabIndexChange = useCallback((id: number) => {
    setTabIndex(id);
    tabIndexRef.current = id;
  }, []);

  const onTabPress = useCallback((idx: number) => {
    if (!isListGlidingRef.current) {
      setTabIndex(idx);
      tabIndexRef.current = idx;
    }
  }, []);

  const scrollYValueRef = useRef(0);

  React.useEffect(() => {
    const listenerId = scrollY.addListener(({ value }) => {
      scrollYValueRef.current = value;
    });
    return () => {
      scrollY.removeListener(listenerId);
    };
  }, [scrollY]);

  const syncScrollOffset = useCallback(() => {
    const focusedTabKey = tabRoutes[tabIndexRef.current].key;

    listArrRef.current.forEach(item => {
      if (item.key !== focusedTabKey) {
        if (scrollYValueRef.current < headerHeight && scrollYValueRef.current >= 0) {
          if (item.value) {
            item.value?.scrollToOffset({
              offset: scrollYValueRef.current,
              animated: false,
            });
            listOffsetRef.current[item.key] = scrollYValueRef.current;
          }
        } else if (scrollYValueRef.current >= headerHeight) {
          if (
            listOffsetRef.current[item.key] < headerHeight ||
            listOffsetRef.current[item.key] === null
          ) {
            if (item.value) {
              item.value?.scrollToOffset({
                offset: headerHeight,
                animated: false,
              });
              listOffsetRef.current[item.key] = headerHeight;
            }
          }
        }
      } else {
        if (item.value) {
          listOffsetRef.current[item.key] = scrollYValueRef.current;
        }
      }
    });
  }, [headerHeight]);

  const onMomentumScrollBegin = useCallback(() => {
    isListGlidingRef.current = true;
  }, []);

  const onMomentumScrollEnd = useCallback(() => {
    isListGlidingRef.current = false;
    syncScrollOffset();
  }, [syncScrollOffset]);

  const onScrollEndDrag = useCallback(() => {
    syncScrollOffset();
  }, [syncScrollOffset]);

  const renderTabBar = useCallback(
    (props: TabBarProps<TabRoute>) => {
      return (
        <Animated.View
          style={{
            flexDirection: 'row',
            alignItems: 'center',
            height: TABBAR_HEIGHT,
            backgroundColor: 'white',
            zIndex: 1,
            transform: [{ translateY: tabBarTranslateY }],
            position: 'absolute',
            top: 0,
            width: '100%',
          }}>
          {props.navigationState.routes.map((route: TabRoute, idx: number) => {
            return (
              <TouchableOpacity
                style={{ flex: 1 }}
                key={idx}
                onPress={() => {
                  onTabPress(idx);
                }}>
                <View style={{ justifyContent: 'center', alignItems: 'center', height: '100%' }}>
                  <Text style={{ fontSize: 15, color: 'black' }}>{route.title}</Text>
                </View>
              </TouchableOpacity>
            );
          })}
        </Animated.View>
      );
    },
    [onTabPress, tabBarTranslateY],
  );

  const renderScene = useCallback(
    ({ route }: { route: TabRoute }) => {
      const isFocused = route.key === tabRoutes[tabIndex].key;

      if (route.key === 'screen1') {
        return (
          <CollapsibleFlatList
            headerHeight={headerHeight}
            tabBarHeight={TABBAR_HEIGHT}
            scrollY={scrollY}
            onMomentumScrollBegin={onMomentumScrollBegin}
            onMomentumScrollEnd={onMomentumScrollEnd}
            onScrollEndDrag={onScrollEndDrag}
            tabRoute={route}
            listArrRef={listArrRef}
            isTabFocused={isFocused}
          />
        );
      } else if (route.key === 'screen2') {
        return (
          <CollapsibleFlatList
            headerHeight={headerHeight}
            tabBarHeight={TABBAR_HEIGHT}
            scrollY={scrollY}
            onMomentumScrollBegin={onMomentumScrollBegin}
            onMomentumScrollEnd={onMomentumScrollEnd}
            onScrollEndDrag={onScrollEndDrag}
            tabRoute={route}
            listArrRef={listArrRef}
            isTabFocused={isFocused}
          />
        );
      } else {
        return (
          <CollapsibleFlatList
            headerHeight={headerHeight}
            tabBarHeight={TABBAR_HEIGHT}
            scrollY={scrollY}
            onMomentumScrollBegin={onMomentumScrollBegin}
            onMomentumScrollEnd={onMomentumScrollEnd}
            onScrollEndDrag={onScrollEndDrag}
            tabRoute={route}
            listArrRef={listArrRef}
            isTabFocused={isFocused}
          />
        );
      }
    },
    [headerHeight, tabIndex, onMomentumScrollBegin, onMomentumScrollEnd, onScrollEndDrag, scrollY],
  );

  return (
    <View style={{ flex: 1 }}>
      {headerHeight > 0 && (
        <TabView
          navigationState={{ index: tabIndex, routes: tabRoutes }}
          renderScene={renderScene}
          renderTabBar={renderTabBar}
          onIndexChange={onTabIndexChange}
          swipeEnabled
        />
      )}
      <Animated.View
        style={{
          position: 'absolute',
          width: '100%',
          transform: [{ translateY: headerTranslateY }],
        }}
        onLayout={headerOnLayout}
        pointerEvents="box-none">
        <View style={{ height: 200, backgroundColor: 'white' }} pointerEvents="box-none">
          <Text>header</Text>
        </View>
      </Animated.View>
    </View>
  );
};

Attempted Solutions:

  • Dynamically toggle swipeEnabled in TabView
  • Dynamically adjust pointerEvents on Animated.View
  • Tried panResponder/GestureDetector

What I Was Expecting:

  • Maintain vertical scroll functionality on header
  • Prevent horizontal swipes over header from switching tabs
  • Keep native-feeling interactions

All my attempts resulted in unnatural scroll behavior. Please let me know if there's another solution or another way to implement a screen with a collapsible header and TabView. (Unless there's a consistently updated library available, I'd rather implement it myself.)

发布评论

评论列表(0)

  1. 暂无评论