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

Android端嵌套BottomSheet的FlashList无法滚动问题

Android端自定义BottomSheet嵌套FlashList无法滚动问题

问题说明

仅Android端存在异常:将List组件嵌套进自定义BottomSheet组件后,其中的FlashList无法滚动;移除BottomSheet组件后滚动恢复正常,故问题与BottomSheet相关。iOS端无此问题。

使用方式

组件嵌套结构如下:

<GestureHandlerRootView style={{  height: '100%', width: '100%', position: 'absolute' }}>
    <View style={{ position: 'absolute', height: '100%', width: '100%', }}>
        <BottomSheet ref={ref}>
            <List />
        </BottomSheet>
    </View>
</GestureHandlerRootView>

相关组件代码

BottomSheet组件代码

import { Dimensions, StyleSheet, Text, View } from 'react-native';
import React, { useCallback, useEffect, useImperativeHandle } from 'react';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
  Extrapolate,
  interpolate,
  useAnimatedStyle,
  useSharedValue,
  withSpring,
  withTiming,
} from 'react-native-reanimated';

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

const MAX_TRANSLATE_Y = -SCREEN_HEIGHT + 50;

type BottomSheetProps = {
  children?: React.ReactNode;
};

export type BottomSheetRefProps = {
  scrollTo: (destination: number) => void;
  isActive: () => boolean;
};

const BottomSheet = React.forwardRef<BottomSheetRefProps, BottomSheetProps>(
  ({ children }, ref) => {
    const translateY = useSharedValue(0);
    const active = useSharedValue(false);

    const scrollTo = useCallback((destination: number) => {
      'worklet';
      active.value = destination !== 0;

      translateY.value = withSpring(destination, { damping: 50 });
    }, []);

    const isActive = useCallback(() => {
      return active.value;
    }, []);

    useImperativeHandle(ref, () => ({ scrollTo, isActive }), [
      scrollTo,
      isActive,
    ]);

    const context = useSharedValue({ y: 0 });
    const gesture = Gesture.Pan()
      .onStart(() => {
        context.value = { y: translateY.value };
      })
      .onUpdate((event) => {
        translateY.value = event.translationY + context.value.y;
        translateY.value = Math.max(translateY.value, MAX_TRANSLATE_Y);
      })
      .onEnd(() => {
        if (translateY.value > -SCREEN_HEIGHT / 3) {
          scrollTo(0);
        } else if (translateY.value < -SCREEN_HEIGHT / 1.5) {
          scrollTo(MAX_TRANSLATE_Y);
        }
      });

    const rBottomSheetStyle = useAnimatedStyle(() => {
      const borderRadius = interpolate(
        translateY.value,
        [MAX_TRANSLATE_Y + 50, MAX_TRANSLATE_Y],
        [25, 5],
        Extrapolate.CLAMP
      );

      return {
        borderRadius,
        transform: [{ translateY: translateY.value }],
      };
    });

    return (
      <View>
        <GestureDetector gesture={gesture}>
          <Animated.View style={[styles.bottomSheetContainer, rBottomSheetStyle]}>
            <View style={styles.line} />
            {children}
          </Animated.View>
        </GestureDetector>
      </View>
    );
  }
);

const styles = StyleSheet.create({
  bottomSheetContainer: {
    height: SCREEN_HEIGHT,
    width: '100%',
    backgroundColor: 'black',
    position: 'absolute',
    top: SCREEN_HEIGHT,
    borderRadius: 25,
  },
  line: {
    width: 75,
    height: 4,
    backgroundColor: 'grey',
    alignSelf: 'center',
    marginVertical: 15,
    borderRadius: 2,
  },
});

export default BottomSheet;

List组件代码

import { StyleSheet, Text, View } from 'react-native'
import React from 'react'
import { FlashList } from '@shopify/flash-list'

const List = () => {
    const data = [
        { id: '1', title: 'Item 1' },
        { id: '2', title: 'Item 2' },
        { id: '3', title: 'Item 1' },
        { id: '243', title: 'Item 2' },
        { id: '14dff3', title: 'Item 1' },
        { id: '2564ewe36', title: 'Item 2' },
        { id: '24ere3', title: 'Item 2' },
        { id: '14drereff3', title: 'Item 1' },
        { id: '25erer6436', title: 'Item 2' },
        { id: '24rereer3', title: 'Item 2' },
        { id: '14dferrf3', title: 'Item 1' },
        { id: '2564er36', title: 'Item 2' }
    ];

    return (
        <View style={styles.container}>
            <View style={{ flex: 1, maxHeight: '80%', }}>
                <FlashList
                    nestedScrollEnabled={true}
                    data={data}
                    estimatedItemSize={100}
                    contentContainerStyle={{ padding: 37 }}
                    renderItem={({ item }) => (
                        <>
                            <Text style={styles.text}>
                                React Native
                                React Native
                                React Native
                                React Native
                                React Native
                                React Native
                                React Native
                                React Native
                                React Native
                            </Text>
                        </>
                    )}
                    keyExtractor={(item) => item.id}
                />
            </View>
        </View>
    )
}

const styles = StyleSheet.create({
    container: {
        backgroundColor: 'black',
        borderTopLeftRadius: 40,
        borderTopRightRadius: 40,
        height: '100%',
        width: '100%',
        position: 'absolute'
    },
    text: {
        color: 'gray',
        fontWeight: '500',
        fontSize: 14,
        left: 15,
        textAlign: 'left',
        width: '90%',
        marginBottom: 20
    },
})
export default List

复现步骤

在Android模拟器中,拖动列表区域尝试滚动,列表无响应;iOS端该操作正常。

环境信息

  • Gesture Handler版本:2.12.1
  • React Native版本:0.72.4
  • 平台:Android
  • JavaScript运行时:Hermes
  • 开发模式:非Expo的React Native
  • 架构:Fabric(新架构)
  • 构建类型:调试模式
  • 设备:Android模拟器(Google Pixel 4)

解决方案

问题根源是Android端手势冲突:BottomSheet的Pan手势拦截了FlashList的滚动手势,导致列表无法响应操作。以下是三种可行解决方式:

方法1:限制BottomSheet手势触发区域

将GestureDetector仅包裹顶部的拖拽条(line组件),只有拖动该区域时才触发BottomSheet滑动,列表区域的滚动手势不受影响。修改BottomSheet的返回部分:

return (
  <Animated.View style={[styles.bottomSheetContainer, rBottomSheetStyle]}>
    <GestureDetector gesture={gesture}>
      <View style={styles.line} />
    </GestureDetector>
    {children}
  </Animated.View>
);

此方式最简单,适合仅通过顶部拖拽条控制BottomSheet的场景。

方法2:通过手势优先级配置解决冲突

给BottomSheet的Pan手势添加simultaneousWithExternalGesture,允许FlashList的滚动手势同时响应:

  1. 在List组件中给FlashList添加gestureRef:
// List组件内部
const listGestureRef = useRef();

// FlashList组件属性添加
<FlashList
  gestureRef={listGestureRef}
  // 其余属性不变
/>
  1. 在BottomSheet的Pan手势中配置:
const gesture = Gesture.Pan()
  .simultaneousWithExternalGesture(listGestureRef)
  // 原有onStart、onUpdate、onEnd逻辑不变

方法3:动态判断是否处理BottomSheet手势

通过判断列表的滚动状态,仅当列表无法滚动时(处于顶部向下拖、底部向上拖)才处理BottomSheet的手势:

// BottomSheet中引入useRef获取列表引用,假设List组件暴露FlashList的ref
const listRef = useRef();

const gesture = Gesture.Pan()
  .onStart((event) => {
    const scrollMetrics = listRef.current?.getScrollableMetrics();
    if (!scrollMetrics) return;

    const canScrollVertical = scrollMetrics.contentSize.height > scrollMetrics.layoutMeasurement.height;
    const isAtTop = scrollMetrics.contentOffset.y === 0;
    const isAtBottom = scrollMetrics.contentOffset.y >= (scrollMetrics.contentSize.height - scrollMetrics.layoutMeasurement.height);
    
    // 仅当列表无法滚动或处于边界且拖动方向与滚动方向冲突时,才处理BottomSheet手势
    if (!canScrollVertical || (isAtTop && event.translationY > 0) || (isAtBottom && event.translationY < 0)) {
      context.value = { y: translateY.value };
    }
  })
  .onUpdate((event) => {
    // 仅在允许处理手势时更新BottomSheet位置
    const scrollMetrics = listRef.current?.getScrollableMetrics();
    if (!scrollMetrics) return;
    
    const canScrollVertical = scrollMetrics.contentSize.height > scrollMetrics.layoutMeasurement.height;
    const isAtTop = scrollMetrics.contentOffset.y === 0;
    const isAtBottom = scrollMetrics.contentOffset.y >= (scrollMetrics.contentSize.height - scrollMetrics.layoutMeasurement.height);
    
    if (!canScrollVertical || (isAtTop && event.translationY > 0) || (isAtBottom && event.translationY < 0)) {
      translateY.value = event.translationY + context.value.y;
      translateY.value = Math.max(translateY.value, MAX_TRANSLATE_Y);
    }
  })
  .onEnd(() => {
    // 仅在允许处理手势时执行收尾逻辑
    const scrollMetrics = listRef.current?.getScrollableMetrics();
    if (!scrollMetrics) return;
    
    const canScrollVertical = scrollMetrics.contentSize.height > scrollMetrics.layoutMeasurement.height;
    const isAtTop = scrollMetrics.contentOffset.y === 0;
    const isAtBottom = scrollMetrics.contentOffset.y >= (scrollMetrics.contentSize.height - scrollMetrics.layoutMeasurement.height);
    
    if (!canScrollVertical || (isAtTop && translateY.value > 0) || (isAtBottom && translateY.value < 0)) {
      if (translateY.value > -SCREEN_HEIGHT / 3) {
        scrollTo(0);
      } else if (translateY.value < -SCREEN_HEIGHT / 1.5) {
        scrollTo(MAX_TRANSLATE_Y);
      }
    }
  });

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 23:35:59