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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.14 12:33:14