React Native自定义BottomSheet:背景变暗与点击外部收起实现方案
解决方案
针对你提出的两个需求,我们可以通过在BottomSheet组件中添加背景遮罩层来同时解决,以下是修改后的代码和关键说明:
修改后的BottomSheet组件代码 (bottomsheet.js)
import { Dimensions, StyleSheet, Text, View, TouchableOpacity } from 'react-native' import React, { useCallback, useImperativeHandle } from 'react' import { Gesture, GestureDetector } from 'react-native-gesture-handler'; import Animated, { interpolate, useAnimatedStyle, useSharedValue, withSpring, createAnimatedComponent } from 'react-native-reanimated'; const AnimatedTouchableOpacity = createAnimatedComponent(TouchableOpacity); const {height: SCREEN_HEIGHT} = Dimensions.get('window'); const MAX_TRANSLATE_Y = -SCREEN_HEIGHT + 50; const BottomSheet = React.forwardRef(({},ref) => { const translateY = useSharedValue(35); const active = useSharedValue(false); const scrollTo = useCallback ((destination = Number) =>{ 'worklet'; active.value = destination !== 35; translateY.value = withSpring(destination, {damping:15}); }, []); const isActive1 = useCallback (()=>{ return active.value; },[]) useImperativeHandle(ref, () => ({scrollTo, isActive1}), [scrollTo, isActive1]); const updatePan = useSharedValue({y:0}); const activateGesture = Gesture.Pan() .onStart(() => { updatePan.value = { y:translateY.value}; }) .onUpdate((e) => { translateY.value = e.translationY + updatePan.value.y; translateY.value = Math.max(translateY.value, MAX_TRANSLATE_Y); }) .onEnd (() => { if (translateY.value > -SCREEN_HEIGHT/3){ scrollTo(35); } else if (translateY.value < -SCREEN_HEIGHT / 1.5) { scrollTo(MAX_TRANSLATE_Y); } }); const rBottomSheetStyle = useAnimatedStyle (() => { const borderRadius = interpolate( translateY.value, [MAX_TRANSLATE_Y + 100, MAX_TRANSLATE_Y], [25, 5], Extrapolate.CLAMP ); return { borderRadius, transform: [{ translateY: translateY.value }], }; }); // 背景遮罩的动画样式 const rBackdropStyle = useAnimatedStyle(() => { const opacity = interpolate( translateY.value, [35, -SCREEN_HEIGHT/3], [0, 0.5], Extrapolate.CLAMP ); return { opacity }; }); return ( <> {/* 全屏背景遮罩 */} <AnimatedTouchableOpacity style={[styles.backdrop, rBackdropStyle]} onPress={() => scrollTo(35)} activeOpacity={1} disabled={!active.value} /> <GestureDetector gesture={activateGesture}> <Animated.View style={[styles.bottomSheetContainer, rBottomSheetStyle]} > <View style={styles.line} /> </Animated.View> </GestureDetector> </> ) }) const styles = StyleSheet.create({ backdrop: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: '#000', zIndex: 99, }, bottomSheetContainer: { height: SCREEN_HEIGHT, width:'100%', backgroundColor: 'white', position: 'absolute', top: SCREEN_HEIGHT, borderRadius: 25, zIndex: 100, }, line:{ width: 75, backgroundColor: 'grey', height: 4, alignSelf: 'center', marginVertical: 15, borderRadius:2, } }) export default BottomSheet
关键修改说明
背景变暗实现
- 添加了全屏的黑色遮罩层,通过
useAnimatedStyle根据BottomSheet的translateY值动态调整透明度:当BottomSheet收起(translateY=35)时透明度为0,展开到一定位置后透明度变为0.5,实现背景渐变变暗的效果。 - 设置遮罩层的
zIndex为99,BottomSheet容器的zIndex为100,确保BottomSheet始终在遮罩上方。
- 添加了全屏的黑色遮罩层,通过
点击空白收起BottomSheet
- 将遮罩层设置为可点击的
AnimatedTouchableOpacity,绑定onPress事件,点击时调用scrollTo(35)触发BottomSheet收起逻辑。 - 添加
disabled={!active.value},只有当BottomSheet处于激活状态时才响应点击,避免收起状态下的误触。
- 将遮罩层设置为可点击的
可选优化(App.js)
为了符合React Hooks规范,建议给onPress函数添加依赖数组:
const onPress = useCallback (() => { const isActive = firstRef?.current?.isActive1(); if (isActive) { firstRef?.current?.scrollTo(35); } else { firstRef?.current?.scrollTo(-200); } }, [firstRef])
内容的提问来源于stack exchange,提问作者Saroj
相关产品推荐
相关产品推荐

