如何让React Native Modal占屏幕50%且保留缩放动画与下拉关闭
React Native 50%高度Modal实现方案
核心思路
放弃通过transparent属性强行控制Modal高度,改为在Modal内部嵌套自定义容器,单独设置容器高度为屏幕的50%,同时通过Animated和PanResponder实现缩放动画与下拉关闭功能。
具体实现步骤
- 容器样式控制:Modal设置
transparent={true}并添加半透明遮罩,内部容器固定高度为Dimensions.get('window').height * 0.5,底部对齐屏幕。 - 缩放动画实现:用Animated.spring在Modal显示时,将容器的scale从0.95过渡到1,配合弹性动画增强自然感。
- 下拉关闭逻辑:通过PanResponder监听容器拖拽手势,当向下拖拽距离超过容器高度1/3时,触发Modal关闭并伴随位移动画;不足阈值则回弹原位。
完整代码示例
import React, { useState, useRef, useEffect } from 'react'; import { View, Modal, StyleSheet, Dimensions, Animated, PanResponder, TouchableWithoutFeedback, Text } from 'react-native'; const { height: screenHeight } = Dimensions.get('window'); const MODAL_HEIGHT = screenHeight * 0.5; const HalfHeightModal = () => { const [modalVisible, setModalVisible] = useState(false); const scaleAnim = useRef(new Animated.Value(0.95)).current; const translateYAnim = useRef(new Animated.Value(0)).current; const panResponder = useRef( PanResponder.create({ onStartShouldSetPanResponder: () => true, onPanResponderMove: (_, gestureState) => { if (gestureState.dy > 0) { translateYAnim.setValue(gestureState.dy); } }, onPanResponderRelease: (_, gestureState) => { if (gestureState.dy > MODAL_HEIGHT / 3) { Animated.timing(translateYAnim, { toValue: MODAL_HEIGHT, duration: 200, useNativeDriver: true, }).start(() => { setModalVisible(false); translateYAnim.setValue(0); }); } else { Animated.spring(translateYAnim, { toValue: 0, useNativeDriver: true, bounciness: 5, }).start(); } }, }) ).current; useEffect(() => { if (modalVisible) { Animated.spring(scaleAnim, { toValue: 1, useNativeDriver: true, bounciness: 5, }).start(); } else { scaleAnim.setValue(0.95); } }, [modalVisible, scaleAnim]); return ( <View style={styles.container}> <TouchableWithoutFeedback onPress={() => setModalVisible(true)}> <View style={styles.openButton}> <Text style={styles.buttonText}>打开50%高度Modal</Text> </View> </TouchableWithoutFeedback> <Modal visible={modalVisible} animationType="none" transparent={true} onRequestClose={() => setModalVisible(false)} > <TouchableWithoutFeedback onPress={() => setModalVisible(false)}> <View style={styles.modalOverlay} /> </TouchableWithoutFeedback> <Animated.View style={[ styles.modalContainer, { transform: [ { scale: scaleAnim }, { translateY: translateYAnim }, ], }, ]} {...panResponder.panHandlers} > <View style={styles.modalContent}> <Text style={styles.contentText}>这是高度为屏幕50%的Modal</Text> <Text style={styles.tipText}>向下拖拽可关闭</Text> </View> </Animated.View> </Modal> </View> ); }; const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#f5f5f5', }, openButton: { width: 220, height: 56, backgroundColor: '#2196F3', borderRadius: 12, justifyContent: 'center', alignItems: 'center', }, buttonText: { color: '#fff', fontSize: 16, fontWeight: '500', }, modalOverlay: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0, 0, 0, 0.5)', }, modalContainer: { position: 'absolute', bottom: 0, left: 0, right: 0, height: MODAL_HEIGHT, backgroundColor: '#fff', borderTopLeftRadius: 20, borderTopRightRadius: 20, shadowColor: '#000', shadowOffset: { width: 0, height: -3 }, shadowOpacity: 0.3, shadowRadius: 6, elevation: 8, }, modalContent: { flex: 1, padding: 20, justifyContent: 'center', alignItems: 'center', }, contentText: { fontSize: 18, fontWeight: '600', marginBottom: 12, }, tipText: { fontSize: 14, color: '#666', }, }); export default HalfHeightModal;
关键说明
- 禁用Modal默认动画,改用自定义Animated动画,规避
transparent属性导致的原生动画失效问题。 - 半透明遮罩独立设置,点击遮罩或下拉容器均可关闭Modal,覆盖两种常用交互场景。
- 容器固定在屏幕底部,高度严格为屏幕的50%,动画流畅度和交互逻辑完全匹配需求。
内容的提问来源于stack exchange,提问作者Manjaka Randrianirina
相关产品推荐
相关产品推荐

