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

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>
  </>
);
};

问题原因分析

  1. 手势事件抢占:Pan手势默认会抢占触摸事件优先级,即便ScrollView不在GestureDetector内部,父级动画视图的事件传递逻辑仍会干扰ScrollView接收触摸信号。
  2. 滚动与拖拽逻辑未区分:没有判断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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.19 01:54:59