React Native(仅Android) GestureDetector内ScrollView不工作如何解决
问题原因
你的判断完全准确:外层GestureDetector绑定的Pan手势默认拦截所有纵向滑动触摸事件,识别优先级高于内部ScrollView的滚动手势,触摸事件无法传递到内部滚动组件,最终表现为抽屉本身滑动正常,但内部内容无法滚动。
修复方案
解决这类嵌套手势冲突,核心是配置手势共存规则,同时增加滑动场景判断,区分「拖动抽屉调整高度」和「滚动内部内容」两种操作:
- 新增共享值记录内部
ScrollView的实时滚动偏移 - 给外层Pan手势配置手势共存规则,允许和内部ScrollView的滚动手势同时识别
- 增加滑动逻辑分支判断:
- 抽屉未拉到最高展开位置时,纵向滑动优先响应抽屉高度调整
- 抽屉处于最高展开位置时,若ScrollView未滚到顶部,向上滑动优先响应内容滚动;若ScrollView已经滚到顶部,向下滑动优先响应抽屉下拉收起
- 给ScrollView绑定滚动监听,实时同步当前滚动偏移量
修改后完整代码
import { Dimensions, StyleSheet, TouchableOpacity } from 'react-native'; import React, { useCallback, useEffect, useRef } from 'react'; import Animated, { useAnimatedScrollHandler } from 'react-native-reanimated'; import { Gesture, GestureDetector, ScrollView, type ScrollView as ScrollViewType, } from 'react-native-gesture-handler'; import { Extrapolate, interpolate, useAnimatedStyle, useSharedValue, withSpring, } from 'react-native-reanimated'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useHeaderHeight } from '@react-navigation/elements'; const { height: SCREEN_HEIGHT } = Dimensions.get('window'); type Props = { top: number; bottom: number; initialBottomOffset: number; children?: React.ReactNode; }; export const BottomSheet: React.FC<Props> = ({ children, top, bottom, initialBottomOffset, }) => { const translateY = useSharedValue(0); // 记录ScrollView实时滚动偏移 const scrollViewOffset = useSharedValue(0); // 绑定ScrollView实例用于手势关联 const scrollViewRef = useRef<ScrollViewType>(null); const insets = useSafeAreaInsets(); const HEADER_HIGHT = useHeaderHeight(); const MAX_TRANSLATE_Y = -SCREEN_HEIGHT + top; const MIN_TRANSLATE_Y = -(bottom + insets.bottom); const scrollTo = useCallback((destination: number) => { 'worklet'; translateY.value = withSpring(destination, { damping: 50 }); }, []); const context = useSharedValue({ y: 0 }); const gesture = Gesture.Pan() // 配置手势共存,允许和ScrollView滚动手势同时识别 .simultaneousWithExternalGesture(scrollViewRef) .onStart(() => { context.value = { y: translateY.value }; }) .onUpdate(event => { const isSheetFullyExpanded = translateY.value <= MAX_TRANSLATE_Y + 1; // 抽屉完全展开、ScrollView未滚到顶部时,向下滑动交给ScrollView处理 if (isSheetFullyExpanded && event.translationY > 0 && scrollViewOffset.value > 0) { return; } // 抽屉未完全展开时,向上滑动优先响应抽屉拖动 translateY.value = event.translationY + context.value.y; if (event.translationY < 0) { translateY.value = Math.max(translateY.value, MAX_TRANSLATE_Y); } else { translateY.value = Math.min(translateY.value, MIN_TRANSLATE_Y); } if (Math.abs(event.velocityY) > 300) { scrollTo(event.translationY < 0 ? MAX_TRANSLATE_Y : MIN_TRANSLATE_Y); } }); // 监听ScrollView滚动,实时更新偏移量 const scrollHandler = useAnimatedScrollHandler({ onScroll: (event) => { scrollViewOffset.value = event.contentOffset.y; } }); const animatedBottomSheetStyle = useAnimatedStyle(() => { const borderRadius = interpolate( translateY.value, [MAX_TRANSLATE_Y + HEADER_HIGHT, MAX_TRANSLATE_Y], [25, 0], Extrapolate.CLAMP, ); return { borderRadius, transform: [{ translateY: translateY.value }], }; }); useEffect(() => { scrollTo(MIN_TRANSLATE_Y); }, [bottom]); useEffect(() => { scrollTo(-initialBottomOffset); }, [initialBottomOffset]); return ( <GestureDetector gesture={gesture}> <Animated.View style={[styles.bottomSheetContainer, animatedBottomSheetStyle]}> <TouchableOpacity style={styles.handle} onPress={() => scrollTo(MAX_TRANSLATE_Y)} /> <ScrollView ref={scrollViewRef} onScroll={scrollHandler} scrollEventThrottle={16} > {children} </ScrollView> </Animated.View> </GestureDetector> ); }; const styles = StyleSheet.create({ bottomSheetContainer: { position: 'absolute', top: SCREEN_HEIGHT, width: '100%', height: '100%', backgroundColor: '#fff', borderRadius: 25, flex: 1, }, handle: { width: 45, height: 5, backgroundColor: '#CEDEEA', alignSelf: 'center', marginVertical: 10, borderRadius: 4, }, });

补充:如果需要更顺滑的交互,可以在判断逻辑中增加触摸速度阈值、滚动方向锁,避免斜向滑动时出现手势冲突。
内容的提问来源于stack exchange,提问作者Otani Shuzo
相关产品推荐
相关产品推荐

