Expo React Native Modal内KeyboardAvoidingView失效问题求助
解决Expo React Native中Modal被键盘挤压变形的问题
问题描述
我在Expo React Native项目中开发了一个Modal组件,当前遇到的问题是:弹出键盘后Modal内所有内容被挤压变形,期望键盘直接覆盖Modal,不让任何内容移位。尝试过在Modal内多个位置使用KeyboardAvoidingView,也试过react-native-modal的avoidKeyboard属性以及KeyboardAwareScrollView,但均无效果。项目使用了react-navigation,怀疑二者存在兼容问题。
组件代码
import React, { Component } from 'react'; import { Modal, View, ImageBackground, Text, ScrollView, Pressable, Image, TextInput, StyleSheet, Dimensions, Keyboard } from 'react-native'; import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome'; import ImageContext from '../contexts/ImageContext'; import CharacterSelectionModal from './CharacterSelectionModal'; export default class NewGameModal extends Component { constructor(props) { super(props); this.state = { playerList: [], characterSelectionModalVisible: false, selectedPlayerId: null, warningMessages: [ { level: "error", message: "Votre Village est vide.", color: "#FF4444", }, ], modalHeight: Dimensions.get('window').height * 0.9, }; } static contextType = ImageContext; componentDidMount() { this.keyboardDidShowListener = Keyboard.addListener( 'keyboardDidShow', () => this.setState({ modalHeight: Dimensions.get('window').height * 0.9 }) ); this.keyboardDidHideListener = Keyboard.addListener( 'keyboardDidHide', () => this.setState({ modalHeight: Dimensions.get('window').height * 0.9 }) ); } componentWillUnmount() { this.keyboardDidShowListener?.remove(); this.keyboardDidHideListener?.remove(); } setModalVisible = (isVisible) => { this.setState({ characterSelectionModalVisible: isVisible }); }; setSelectedPlayerId = (playerId) => { this.setState({ selectedPlayerId: playerId }); }; selectCharacter = (playerId, characterName) => { // 实现角色选择逻辑 }; changePlayerName = (playerId, value) => { // 实现修改玩家名称逻辑 }; checkVillage = () => { // 实现村庄检查逻辑 }; removePlayer = (playerId) => { // 实现移除玩家逻辑 }; addPlayer = () => { // 实现添加玩家逻辑 }; clearPlayerList = () => { // 实现清空玩家列表逻辑 }; getPlayerListGroupByCharacter = () => { // 实现按角色分组逻辑 return []; }; getImageByName = (imageName) => { // 实现根据名称获取图片逻辑 return require("../assets/cards_90_90/no_role2_90_90.png"); }; render() { return ( <Modal animationType="slide" transparent={true} visible={this.props.modalVisible} onRequestClose={() => { this.props.navigation.navigate("Home"); }} > <View style={{...styles.centeredView, height: this.state.modalHeight}}> <ImageBackground source={require("../assets/stain_pattern.png")} resizeMode="repeat" style={styles.modalYellowBackgroundImage} > <View style={styles.modalTitleView}> <Text style={styles.redText}>Nouveau Village</Text> </View> <ImageBackground source={require("../assets/stain_pattern.png")} resizeMode="repeat" style={styles.villageRepartitionBackgroundImage} > <ScrollView style={styles.villageRepartitionView} contentContainerStyle={styles.villageRepartitionContainerView} persistentScrollbar={true} > {this.getPlayerListGroupByCharacter().map((char, index) => ( <View style={styles.characterRepartitionView} key={"repartition-" + index} > <Text style={styles.sYellowText}> {char.name} {char.count}/{char.max} </Text> </View> ))} </ScrollView> </ImageBackground> {this.state.warningMessages.length === 0 ? null : ( <ImageBackground source={require("../assets/stain_pattern.png")} resizeMode="repeat" style={styles.warningBackgroundImage} > {this.state.warningMessages.slice(0, 3).map((m, index) => ( <Text style={{ ...styles.message, color: m.color }} key={"warning-" + index} > {m.message} </Text> ))} </ImageBackground> )} <ImageBackground source={require("../assets/stain_pattern.png")} resizeMode="repeat" style={styles.playersListBackgroundImage} > <CharacterSelectionModal modalVisible={this.state.characterSelectionModalVisible} closeModal={this.setModalVisible} playerId={this.state.selectedPlayerId} selectCharacter={this.selectCharacter} /> <ScrollView style={styles.playersListScrollView} contentContainerStyle={ this.state.playerList.length === 0 ? styles.playersListScrollViewContainer : undefined } > {this.state.playerList.length === 0 ? ( <View> <Text style={styles.yellowText}> Votre Village est vide. </Text> <Text style={styles.yellowText}> Commencez par ajouter des joueurs </Text> <Text style={styles.yellowText}> avec le bouton "+" ci dessous. </Text> </View> ) : ( this.state.playerList.map((player, index) => ( <View key={"player-" + index}> <ImageBackground source={require("../assets/stain_pattern.png")} resizeMode="repeat" style={{ ...styles.modalPlayerCardBackgroundImage, backgroundColor: player.character ? player.character.backgroundColor : "#95A2C3", }} > <View style={styles.playerCard}> <Pressable onPress={() => { this.setSelectedPlayerId(player.playerId); this.setModalVisible(true); }} > <Image style={styles.playerCardImage} source={ player.character ? this.getImageByName(player.character.image) : require("../assets/cards_90_90/no_role2_90_90.png") } resizeMode="contain" /> </Pressable> <TextInput style={styles.playerCardText} placeholder="Nom Joueur" value={player.name} onChangeText={(value) => this.changePlayerName(player.playerId, value) } onBlur={() => this.checkVillage()} /> <Pressable style={styles.playerRemove} onPress={() => this.removePlayer(player.playerId)} > <FontAwesomeIcon name="close" style={styles.playerRemoveIcon} /> </Pressable> </View> </ImageBackground> </View> )) )} </ScrollView> <View style={styles.overlayButtonView}> <Pressable style={styles.overlayButton} onPress={() => this.addPlayer()} > <Text style={styles.sBlueText}>Ajouter Joueur</Text> </Pressable> <Pressable style={styles.overlayButton} onPress={() => this.clearPlayerList()} > <Text style={styles.sBlueText}>Vider le Village</Text> </Pressable> <Pressable style={styles.overlayButton} onPress={() => console.log("confirm")} > <Text style={styles.sBlueText}>Créer Village</Text> </Pressable> </View> </ImageBackground> </ImageBackground> </View> </Modal> ); } } const styles = StyleSheet.create({ centeredView: { flex: 1, flexDirection: "column", justifyContent: "center", backgroundColor: "#FFEFBC", alignItems: "center", margin: 22, shadowColor: "#000", shadowOffset: { width: 0, height: 2, }, shadowOpacity: 0.25, shadowRadius: 4, elevation: 5, borderRadius: 20, }, modalYellowBackgroundImage: { width: '100%', height: '100%', borderRadius: 20, padding: 10, }, modalTitleView: { alignItems: 'center', marginBottom: 10, }, redText: { fontSize: 20, fontWeight: 'bold', color: '#FF4444', }, villageRepartitionBackgroundImage: { width: '100%', height: 80, borderRadius: 10, marginBottom: 10, }, villageRepartitionView: { width: '100%', height: '100%', }, villageRepartitionContainerView: { padding: 5, }, characterRepartitionView: { marginBottom: 5, }, sYellowText: { color: '#FFEFBC', fontSize: 14, }, warningBackgroundImage: { width: '100%', borderRadius: 10, marginBottom: 10, padding: 10, }, message: { fontSize: 14, marginBottom: 5, }, playersListBackgroundImage: { width: '100%', flex: 1, borderRadius: 10, padding: 10, }, playersListScrollView: { width: '100%', flex: 1, marginBottom: 10, }, playersListScrollViewContainer: { alignItems: 'center', justifyContent: 'center', flex: 1, }, yellowText: { color: '#FFEFBC', fontSize: 16, marginBottom: 5, }, modalPlayerCardBackgroundImage: { width: '100%', borderRadius: 10, padding: 5, marginBottom: 10, }, playerCard: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', }, playerCardImage: { width: 50, height: 50, }, playerCardText: { flex: 1, marginHorizontal: 10, color: '#fff', fontSize: 16, }, playerRemove: { padding: 5, }, playerRemoveIcon: { color: '#fff', fontSize: 20, }, overlayButtonView: { flexDirection: 'row', justifyContent: 'space-around', width: '100%', }, overlayButton: { padding: 10, backgroundColor: '#95A2C3', borderRadius: 10, }, sBlueText: { color: '#fff', fontSize: 14, fontWeight: 'bold', }, });
解决方法
1. 关闭React Navigation的键盘自动处理
React Navigation默认会自动调整布局以适应键盘,这会和Modal的固定布局冲突。在导航容器中添加keyboardHandlingEnabled: false禁用该行为:
import { NavigationContainer } from '@react-navigation/native'; <NavigationContainer screenOptions={{ keyboardHandlingEnabled: false, }} > {/* 你的导航栈/标签栏 */} </NavigationContainer>
2. 给Modal内容设置固定高度
去掉KeyboardAvoidingView(不需要内容避开键盘),给Modal外层容器设置固定高度(基于屏幕高度的比例),确保它不会随键盘弹出被压缩:
- 在组件state中添加
modalHeight,初始值设为屏幕高度的90% - 监听键盘弹出/隐藏事件,保持高度不变
- 外层View绑定该固定高度
3. 确保ScrollView布局稳定
如果TextInput在ScrollView内,设置ScrollView的flex属性为1,即使键盘遮挡部分内容,用户仍可滚动查看,同时保证整体布局不会变形。
内容的提问来源于stack exchange,提问作者AnthonyDa
相关产品推荐
相关产品推荐

