React Native:利用GestureHandler实现双视图高度拖拽调节
React Native 拖拽调节双View高度实现问题
我正在开发一款React Native应用,需要实现两个堆叠的View组件,中间设置拖拽条,通过上下拖拽该条来调节两个View的高度,同时适配其中的内容。其中:
- "Home Option 2"为默认状态
- "Home Option 1.3"是拖拽条向下拖动后的状态
- "Home Option 1.2"是向上拖动后的状态
此外应用底部需添加导航栏(尚未实现)。以下是我的App.tsx和BottomSheet.tsx代码,求思路或帮助!
现有代码
App.tsx
import * as React from 'react'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { StatusBar } from 'expo-status-bar'; import { StyleSheet, TouchableOpacity, View } from 'react-native'; import BottomSheet, { BottomSheetRefProps } from './components/BottomSheet'; import { useCallback, useRef } from 'react'; import MapView, { Marker, Geojson } from "react-native-maps"; import { PROVIDER_GOOGLE } from "react-native-maps"; export default function App() { const ref = useRef<BottomSheetRefProps>(null); const [topViewHeight, setTopViewHeight] = React.useState(0); const onPress = useCallback(() => { const isActive = ref?.current?.isActive(); if (isActive) { ref?.current?.scrollTo(0); } else { ref?.current?.scrollTo(-200); } }, []); return ( <GestureHandlerRootView style={{ flex: 1 }}> <View style={[styles.mapViewContainer, { height: topViewHeight || '50%' }]}> <MapView provider={PROVIDER_GOOGLE} showsUserLocation={true} style={styles.mapView} initialRegion={{ latitude: 00.00 , longitude: -00.00 , latitudeDelta: 00.00 , longitudeDelta: 00.00 , }} > <Marker coordinate={{ latitude: 00.00, longitude: 00.00 }} /> </MapView> </View> <View style={styles.container}> <StatusBar style="light" /> <TouchableOpacity style={styles.button} onPress={onPress} /> <BottomSheet ref={ref} {...{setTopViewHeight, topViewHeight}}> <View style={{ flex: 1, backgroundColor: 'orange' }} /> </BottomSheet> </View> </GestureHandlerRootView> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#111', alignItems: 'center', justifyContent: 'center', }, button: { height: 50, borderRadius: 25, aspectRatio: 1, backgroundColor: 'white', opacity: 0.6, }, mapViewContainer: { width: "95%", overflow: "hidden", background: "transparent", borderRadius: 13, position: 'absolute', top: 0, left: '2.5%', }, mapView: { height: "100%", width: "100%", }, });
BottomSheet.tsx
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, runOnJS, } from 'react-native-reanimated'; const { height: SCREEN_HEIGHT } = Dimensions.get('window'); const NAV_BAR_HEIGHT = 60; // 预留底部导航栏高度 const MIN_SHEET_HEIGHT = 50; // 完全收起时只留拖拽条高度 const MAX_TRANSLATE_Y = -(SCREEN_HEIGHT - NAV_BAR_HEIGHT - MIN_SHEET_HEIGHT); type BottomSheetProps = { children?: React.ReactNode; setTopViewHeight: (height: number) => void; topViewHeight: number; }; export type BottomSheetRefProps = { scrollTo: (destination: number) => void; isActive: () => boolean; }; const BottomSheet = React.forwardRef<BottomSheetRefProps, BottomSheetProps>( ({ children, setTopViewHeight }, ref) => { const translateY = useSharedValue(0); const active = useSharedValue(false); // 联动更新顶部View高度 useEffect(() => { const updateTopHeight = () => { const currentSheetHeight = SCREEN_HEIGHT - NAV_BAR_HEIGHT + translateY.value; const topHeight = SCREEN_HEIGHT - currentSheetHeight - NAV_BAR_HEIGHT; setTopViewHeight(Math.max(topHeight, 0)); }; updateTopHeight(); }, [translateY.value]); const scrollTo = useCallback((destination: number) => { 'worklet'; active.value = destination !== 0; translateY.value = withSpring(destination, { damping: 50 }); runOnJS(setTopViewHeight)(SCREEN_HEIGHT - NAV_BAR_HEIGHT + destination - NAV_BAR_HEIGHT); }, []); 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; // 限制上下边界:不能超过0(完全收起),不能低于MAX_TRANSLATE_Y(完全展开) translateY.value = Math.min(Math.max(translateY.value, MAX_TRANSLATE_Y), 0); // 实时更新顶部高度 runOnJS(setTopViewHeight)(SCREEN_HEIGHT - NAV_BAR_HEIGHT + translateY.value - NAV_BAR_HEIGHT); }) .onEnd(() => { // 优化回弹阈值:以屏幕1/2为分界 const middlePoint = -(SCREEN_HEIGHT - NAV_BAR_HEIGHT) / 2; if (translateY.value > middlePoint) { scrollTo(0); } else { 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 ( <GestureDetector gesture={gesture}> <Animated.View style={[styles.bottomSheetContainer, rBottomSheetStyle]}> <View style={styles.line} /> <View style={styles.contentContainer}> {children} </View> </Animated.View> </GestureDetector> ); } ); const styles = StyleSheet.create({ bottomSheetContainer: { height: SCREEN_HEIGHT - NAV_BAR_HEIGHT, width: '100%', backgroundColor: 'white', position: 'absolute', bottom: NAV_BAR_HEIGHT, // 避开底部导航栏 borderTopLeftRadius: 25, borderTopRightRadius: 25, }, line: { width: 75, height: 4, backgroundColor: 'grey', alignSelf: 'center', marginVertical: 15, borderRadius: 2, }, contentContainer: { flex: 1, overflow: 'auto', // 内容超出时可滚动 }, }); export default BottomSheet;
关键修改点说明
- 布局重构:将顶部地图View改为绝对定位,底部Sheet从屏幕底部(避开导航栏)开始,通过translateY控制显示高度
- 高度联动:在拖拽过程中实时计算顶部View的高度,通过
setTopViewHeight更新状态,实现双View高度同步调节 - 边界限制:明确Sheet的拖拽上下边界,避免超出屏幕范围
- 内容适配:给Sheet内容容器添加滚动属性,确保高度变化时内容能正常显示
- 导航栏预留:提前预留底部导航栏高度,后续添加导航组件时直接替换
NAV_BAR_HEIGHT对应的View即可
内容的提问来源于stack exchange,提问作者emartindev
相关产品推荐
相关产品推荐

