AnimatedView内ScrollView无法滚动,需实现可拖拽滚动底部弹窗
BottomSheet 滚动与拖拽冲突问题排查及解决方案
问题描述
这是一个预期具备滚动功能的BottomSheet(底部弹窗)组件。原本以为将子组件及ScrollView移至GestureDetector外部就能使其正常滚动,但问题仍未解决。需要排查问题原因,同时实现既能拖动视图执行动画,又能让ScrollView内的内容跟随拖拽滚动的效果。
现有代码
const [scrollDirection, setScrollDirection] = useState(''); const translateY = useSharedValue(0); let previousY = 0; const context = useSharedValue({y: 0}); const isDragging = useSharedValue(false); const longPressGesture = Gesture.LongPress() .onStart(() => { isDragging.value = true; }) .minDuration(250); const gesture = Gesture.Pan() .onStart(() => { context.value = {y: translateY.value}; }) .onUpdate((event) => { if(event.translationY > previousY){ runOnJS(setScrollDirection)('down'); } else{ runOnJS(setScrollDirection)('up'); } translateY.value = event.translationY + context.value.y; translateY.value = Math.max(translateY.value, canOpenMaxHeight ? MAX_TRANSLATE_Y : MIN_TRANSLATE_Y) previousY = event.translationY; }) .onEnd(() => { if (translateY.value > MIN_TRANSLATE_Y && scrollDirection==='down'){ translateY.value = withSpring(0, {damping: 20}); runOnJS(setCloseSheet)(true); } else if (translateY.value < MIN_TRANSLATE_Y && scrollDirection==='up'){ if(canOpenMaxHeight){ translateY.value = withSpring(MAX_TRANSLATE_Y, {damping: 20}, () => { runOnJS(setCloseSheet)(false); }); } else{ translateY.value = withSpring(MIN_TRANSLATE_Y, {damping: 20}, () => { runOnJS(setCloseSheet)(false); }); } } else if(translateY.value > MAX_TRANSLATE_Y && scrollDirection==='down'){ translateY.value = withSpring(MIN_TRANSLATE_Y, {damping: 20}); } }) const composedGesture = Gesture.Race(gesture, longPressGesture); const bottomSheetStyle = useAnimatedStyle(() => { return { transform: [{translateY: translateY.value}], } }); useEffect(() => { if(visible){ translateY.value = withSpring(MIN_TRANSLATE_Y, {damping: 20}, () => { runOnJS(setCloseSheet)(false); }); } },[visible]) if(!visible){ return null; } const onOverlayPress = () => { translateY.value = withSpring(0, {damping: 20}); setCloseSheet(true); } return ( <> <View style={styles.overlay}> <TouchableWithoutFeedback onPress={onOverlayPress} style={{width: '100%', height: '100%'}}> </TouchableWithoutFeedback> </View> <Animated.View style={[styles.container, bottomSheetStyle]}> <GestureDetector gesture={gesture}> <View style={styles.headerContainer}> <View style={styles.header} /> </View> </GestureDetector> <ScrollView> {children} </ScrollView> </Animated.View> </> ); };
问题原因分析
- 手势事件抢占:Pan手势默认会抢占触摸事件优先级,即便ScrollView不在GestureDetector内部,父级动画视图的事件传递逻辑仍会干扰ScrollView接收触摸信号。
- 滚动与拖拽逻辑未区分:没有判断ScrollView的滚动状态(是否处于顶部/底部),导致拖拽BottomSheet和滚动内容的操作触发条件模糊,互相干扰。
解决方案
步骤1:监听ScrollView滚动状态
添加滚动监听,实时记录ScrollView是否处于顶部或底部:
const isAtTop = useSharedValue(true); const isAtBottom = useSharedValue(false); const scrollHandler = useAnimatedScrollHandler({ onScroll: (event) => { isAtTop.value = event.contentOffset.y <= 0; isAtBottom.value = event.contentOffset.y >= (event.contentSize.height - event.layoutMeasurement.height) - 10; }, });
步骤2:修改Pan手势响应条件
让Pan手势仅在ScrollView无法继续滚动时(顶部向下拽、底部向上拽)才响应,避免与ScrollView事件冲突:
const gesture = Gesture.Pan() .enabled((isAtTop.value && scrollDirection === 'down') || (isAtBottom.value && scrollDirection === 'up')) .onStart(() => { context.value = {y: translateY.value}; }) .onUpdate((event) => { // 原有逻辑不变 }) .onEnd(() => { // 原有逻辑不变 })
步骤3:优化ScrollView事件控制
给ScrollView添加滚动开关,拖拽BottomSheet时暂时禁用ScrollView滚动:
<ScrollView scrollEnabled={!isDragging.value} onScroll={scrollHandler} scrollEventThrottle={16} > {children} </ScrollView>
步骤4:修复拖拽方向判断变量
把previousY改为共享变量,避免组件重渲染时被重置:
const previousY = useSharedValue(0); // 在onUpdate中更新 .onUpdate((event) => { if(event.translationY > previousY.value){ runOnJS(setScrollDirection)('down'); } else{ runOnJS(setScrollDirection)('up'); } // 原有逻辑不变 previousY.value = event.translationY; })
内容的提问来源于stack exchange,提问作者jonBlackfyre
相关产品推荐
相关产品推荐

