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优先消耗。
解决方案要点
- 使用
useAnimatedRef关联FlatList,实时获取滚动偏移量 - 在Pan手势的
onUpdate中动态控制FlatList的scrollEnabled状态:- 当Sheet未完全展开(
translationY未达到最大值),强制禁用FlatList滚动 - 当Sheet完全展开,但FlatList未滚动到顶部(偏移量>0),禁用Sheet手势,让FlatList继续滚动
- 当Sheet未完全展开(
- 修复Sheet回弹逻辑的条件判断错误
- 调整手势优先级,确保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
相关产品推荐
相关产品推荐

