You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React Native自定义Bottom Sheet内嵌FlatList滚动冲突问题问询

React Native Bottom Sheet与FlatList滚动冲突解决方案

问题背景

使用react-native-gesture-handler的GestureDetector自定义Bottom Sheet,内嵌FlatList时出现滚动冲突:FlatList滚动会拦截Sheet的手势,导致Sheet无法上移,仅FlatList原地滚动。需要实现:优先让Sheet滚动,当Sheet完全展开到顶部后,再允许FlatList滚动。

核心问题分析

原代码未正确关联Sheet的展开状态与FlatList的滚动权限,且手势逻辑未判断FlatList的滚动位置,导致手势被FlatList优先消耗。

解决方案要点

  1. 使用useAnimatedRef关联FlatList,实时获取滚动偏移量
  2. 在Pan手势的onUpdate中动态控制FlatList的scrollEnabled状态:
    • 当Sheet未完全展开(translationY未达到最大值),强制禁用FlatList滚动
    • 当Sheet完全展开,但FlatList未滚动到顶部(偏移量>0),禁用Sheet手势,让FlatList继续滚动
  3. 修复Sheet回弹逻辑的条件判断错误
  4. 调整手势优先级,确保Sheet手势优先响应

修改后的完整代码

import React, { useCallback, useEffect, useRef } from "react";
import { Dimensions, Text, View } from "react-native";
import { Gesture, GestureDetector, GestureHandlerRootView } from "react-native-gesture-handler";
import Animated, {
  Extrapolate,
  interpolate,
  useAnimatedRef,
  useAnimatedScrollHandler,
  useAnimatedStyle,
  useSharedValue,
  withSpring,
  runOnJS,
} from "react-native-reanimated";
import { useSafeAreaInsets } from "react-native-safe-area-context";

const { height: SCREEN_HEIGHT } = Dimensions.get("window");

const data = Array.from({ length: 25 }, (_, i) => ({
  id: `${i+1}`,
  title: `Item ${i+1}`
}));

export const MyBottomSheet: React.FC = () => {
  const insets = useSafeAreaInsets();
  const listRef = useAnimatedRef<Animated.FlatList>();
  const flatListRef = useRef<Animated.FlatList>(null);

  const MAX_TRANSLATE_Y = -SCREEN_HEIGHT + insets.top;
  // 初始展开高度:底部露出200px
  const INIT_TRANSLATE_Y = -SCREEN_HEIGHT + 200;
  const translationY = useSharedValue(INIT_TRANSLATE_Y);
  const context = useSharedValue({ y: 0 });
  const listScrollOffset = useSharedValue(0);

  const scrollTo = useCallback(
    (destination: number) => {
      "worklet";
      translationY.value = withSpring(destination, { damping: 50 });
    },
    [translationY],
  );

  // 控制FlatList滚动状态
  const setFlatListScrollEnabled = useCallback((enabled: boolean) => {
    flatListRef.current?.setNativeProps({ scrollEnabled: enabled });
  }, []);

  const gesture = Gesture.Pan()
    .onStart(() => {
      context.value = { y: translationY.value };
      // 手势开始时,先检查FlatList是否在顶部
      if (listScrollOffset.value === 0) {
        runOnJS(setFlatListScrollEnabled)(false);
      }
    })
    .onUpdate((event) => {
      const newTranslation = event.translationY + context.value.y;
      // 限制Sheet的最大上移距离
      translationY.value = Math.max(newTranslation, MAX_TRANSLATE_Y);

      // 逻辑判断:
      // 1. 如果Sheet还没到顶部,强制禁用FlatList滚动
      // 2. 如果Sheet到顶部了,但FlatList不在顶部,允许FlatList滚动,停止Sheet手势
      if (translationY.value < MAX_TRANSLATE_Y) {
        runOnJS(setFlatListScrollEnabled)(false);
      } else {
        if (listScrollOffset.value > 0) {
          runOnJS(setFlatListScrollEnabled)(true);
          // 停止Sheet手势,让FlatList处理滚动
          event.enabled = false;
        } else {
          runOnJS(setFlatListScrollEnabled)(false);
        }
      }
    })
    .onEnd(() => {
      // 修复回弹逻辑:判断当前位置决定回弹到初始状态还是完全展开
      if (translationY.value > INIT_TRANSLATE_Y - 50) {
        // 距离初始状态近,回弹到初始高度
        scrollTo(INIT_TRANSLATE_Y);
      } else if (translationY.value < INIT_TRANSLATE_Y - 50) {
        // 距离完全展开近,回弹到顶部
        scrollTo(MAX_TRANSLATE_Y);
      }
      // 回弹后,根据Sheet状态设置FlatList滚动权限
      if (translationY.value === MAX_TRANSLATE_Y) {
        runOnJS(setFlatListScrollEnabled)(true);
      } else {
        runOnJS(setFlatListScrollEnabled)(false);
      }
    });

  const reanimatedBottomSheetStyle = useAnimatedStyle(() => {
    const borderRadius = interpolate(
      translationY.value,
      [MAX_TRANSLATE_Y + insets.top, MAX_TRANSLATE_Y],
      [20, 0],
      Extrapolate.CLAMP
    );
    return {
      borderRadius,
      transform: [{ translateY: translationY.value }],
    };
  });

  const onListScroll = useAnimatedScrollHandler((event) => {
    // 实时更新FlatList的滚动偏移量
    listScrollOffset.value = event.contentOffset.y;
    // 当FlatList滚动到顶部时,重新允许Sheet手势
    if (event.contentOffset.y === 0) {
      runOnJS(setFlatListScrollEnabled)(false);
    }
  });

  useEffect(() => {
    scrollTo(INIT_TRANSLATE_Y);
  }, [scrollTo]);

  const Item = ({ title }: { title: string }) => (
    <View style={{ padding: 10, borderBottomWidth: 1, borderBottomColor: "#eee" }}>
      <Text>{title}</Text>
    </View>
  );

  return (
    <GestureHandlerRootView style={{ flex: 1, backgroundColor: "#0004" }}>
      <GestureDetector gesture={gesture}>
        <Animated.View
          style={[
            reanimatedBottomSheetStyle,
            {
              height: SCREEN_HEIGHT,
              top: SCREEN_HEIGHT,
              borderRadius: 25,
              backgroundColor: "white",
              shadowColor: "#000",
              shadowOffset: { width: 0, height: -2 },
              shadowOpacity: 0.2,
              shadowRadius: 8,
              elevation: 5,
            },
          ]}
        >
          {/* Sheet顶部拖拽指示器 */}
          <View style={{
            width: 40,
            height: 4,
            backgroundColor: "#ccc",
            borderRadius: 2,
            alignSelf: "center",
            marginTop: 12,
          }} />
          <Animated.FlatList
            ref={(ref) => {
              listRef.current = ref;
              flatListRef.current = ref;
            }}
            style={{ margin: 20, marginTop: 20 }}
            data={data}
            renderItem={({ item }) => <Item title={item.title} />}
            keyExtractor={(item) => item.id}
            onScroll={onListScroll}
            scrollEventThrottle={16}
            scrollEnabled={false} // 默认禁用,由手势控制
          />
        </Animated.View>
      </GestureDetector>
    </GestureHandlerRootView>
  );
};

关键修改说明

  • FlatList滚动状态控制:通过setFlatListScrollEnabled方法,根据Sheet的展开状态和FlatList的滚动位置动态启用/禁用滚动
  • 手势逻辑优化:在onUpdate中判断Sheet是否到达顶部,以及FlatList是否在顶部,决定是否让Sheet继续响应手势
  • 滚动偏移监听:用useAnimatedScrollHandler实时获取FlatList的滚动偏移,为手势判断提供依据
  • 回弹逻辑修复:修正原代码中条件判断的错误,根据当前位置正确回弹到初始状态或完全展开状态

内容的提问来源于stack exchange,提问作者arled

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 14:12:02