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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.01 07:35:16