Android EditText被软键盘遮挡的解决办法

2,374 阅读4分钟

Android EditText被软键盘遮挡的解决办法

1. 问题描述

最近实现一个效果的时候遇到了一些问题,布局很简单,就是一个EditText、周围一些ImageView之类的小控件。布局很快写完了,问题主要出在软键盘弹出时UI会往上推移,此时输入框会被挡住一部分。

以简易布局还原一下情况:

<activity
    android:name=".MainActivity"
    android:exported="true"
    android:screenOrientation="portrait"
    android:windowSoftInputMode="adjustPan|stateHidden">
</activity>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <EditText
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:layout_above="@id/splitView"
        android:background="#CC000000" />

    <View
        android:id="@+id/splitView"
        android:layout_width="match_parent"
        android:layout_height="10dp"
        android:layout_alignParentBottom="true"
        android:background="#0DABA0" />

</RelativeLayout>

初始效果图如下:

点击EditText,软键盘弹出后效果如下,很明显可见,editText有一部分区域被遮挡住了,骚气的底部view也被挡到了。

2. 失败尝试

这个问题困扰了我一天的时间,看网上blog,有一些是建议修改AndroidManifest.xml中windowSoftInputMode属性的值。 adjustResize adjustPan都尝试过,但是没有效果。
在上述演示项目中,将windowSoftInputMode设置为adjustResize,倒是能使editText完全可见,但不适用于我需求中的情况。
后续又想到,editText被遮挡,我可以获取软键盘的高度,给editText设置一个margin使其显示在 软键盘之上呀。软键盘弹出时会导致ViewTree发生变化,影响Activity对应Windwow的大小,因此可以给Activity设置ViewTreeObserver监听,计算出软键盘高度。

private int windowHeight;
private final ViewTreeObserver.OnGlobalLayoutListener globalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() {
    @Override
    public void onGlobalLayout() {
        Rect rect = new Rect();
        //获取当前窗口实际的可见区域
        getWindow().getDecorView().getWindowVisibleDisplayFrame(rect);
        if (windowHeight == 0) {
            windowHeight = rect.height();
        } else {
            // 说明当前window大小发生变化
            if (windowHeight != rect.height()) {
                int keyboardHeight = windowHeight - rect.height();
                Log.e(TAG, "onGlobalLayout: " + keyboardHeight);
            }
        }
    }
};

// onCreate方法中添加监听
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Log.e("===", "onCreate: ");
    r = ActivityMainBinding.inflate(getLayoutInflater());
    setContentView(r.getRoot());

    getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(globalLayoutListener);
    initView();
    initListen();
}
// onDestory方法中移除监听,防止内存泄漏
@Override
protected void onDestroy() {
    super.onDestroy();
    Log.e("===", "onDestroy: ");
    getWindow().getDecorView().getViewTreeObserver().removeOnGlobalLayoutListener(globalLayoutListener);
}

当然,这只是一个简易的方法,能引起window大小变化的不止软键盘的弹出。不管怎么说,终于拿到了软键盘的高度,这下子能解决editText被遮挡的问题了吧?兴高采烈的去尝试,结果发现还是不行。

前面已经说了,软键盘弹出时默认情况下会推动UI向上移动,如果在软键盘弹出时给editText设置等同于键盘高度的底部margin,editText位置会突变。将windowSoftInputMode设置为adjustNothing可以禁止页面UI上移,但此时监听同样不会触发。因为属性值设置为adjustNothing时,软键盘弹出,Activity window的大小并不会受到影响。

3. 解决方法

最后,通过查阅各种博客,终于使用DialogFragment解决了这个问题,网上关于DialogFragment的教程也比较多,大家可以自行检索查阅。
最终效果如下:(终于是能够正常显示了,泪目)

相关代码如下:

<style name="DialogStyle" parent="Base.Theme.AppCompat.Dialog">
    <item name="android:windowBackground">@null</item>
    <item name="android:windowMinWidthMajor">97%</item>
    <item name="android:windowMinWidthMinor">97%</item>
    <item name="android:windowSoftInputMode">stateAlwaysVisible</item>
</style>
public class TestDialogFragment extends DialogFragment {
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setStyle(DialogFragment.STYLE_NO_TITLE, R.style.DialogStyle);
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        Window window = getDialog().getWindow();
        // 解决未全屏显示的问题
        window.getDecorView().setPadding(0, 0, 0, 0);
        WindowManager.LayoutParams wlp = window.getAttributes();
        wlp.gravity = Gravity.BOTTOM;
        wlp.width = WindowManager.LayoutParams.MATCH_PARENT;
        wlp.height = WindowManager.LayoutParams.WRAP_CONTENT;
        return inflater.inflate(R.layout.dialog_test, container);
    }

    @Override
    public void onStart() {
        super.onStart();
    }

    @Override
    public void onDismiss(@NonNull DialogInterface dialog) {
        super.onDismiss(dialog);
    }
}

dialog_test.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <EditText
        android:id="@+id/editText"
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:layout_above="@id/splitView"
        android:background="#CC000000"
        android:inputType="text"
        android:imeOptions="actionDone"/>

    <View
        android:id="@+id/splitView"
        android:layout_width="match_parent"
        android:layout_height="10dp"
        android:layout_alignParentBottom="true"
        android:background="#0DABA0" />

</RelativeLayout>

需要注意的是,使用DialogFragment 有内存泄漏的危险,看到一个帖子这样设置可以避免内存泄漏,是否有效有待验证:
使用时:

FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction ft = fragmentManager.beginTransaction();
ft.add(TabsFragment.newInstance(), "Fragment");
ft.addToBackStack(null);
ft.commit();

重写DialogFragment onDismiss方法:

@Override
public void onDismiss(@NonNull DialogInterface dialog) {
    super.onDismiss(dialog);
    FragmentManager fragmentManager = getChildFragmentManager();
    FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
    if (fragmentManager.getBackStackEntryCount() > 0)
        fragmentManager.popBackStack();
    fragmentTransaction.commit();
}

最后,值得一提的是,在DialogFragment中,软键盘的弹出和收起逻辑略有不同,在此给出我实现弹出和隐藏时有效的方法:

private void openKeyboard() {
    InputMethodManager imm =
            (InputMethodManager) editText.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
    if (imm != null)
        imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT);
}

private void closeKeyboard() {
    InputMethodManager imm =
            (InputMethodManager) editText.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
    if (imm != null) {
        imm.hideSoftInputFromWindow(editText.getWindowToken(), 0);
        imm.toggleSoftInput(0, InputMethodManager.HIDE_NOT_ALWAYS);
    }
}

可通过设置setCancelable(true)实现点击其他区域隐藏dialogFragment。

4. 总结

本文归根结底,也只是找到了一种解决editText被遮挡的方法,博主也不清楚涉及到的底层原理,有大佬有所了解的可以补充一下,不胜感激。最后,贴一下TestDialogFragment的完整代码,希望能帮到大家。

package com.example.customview.view;

import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;

import com.example.customview.R;

public class TestDialogFragment extends DialogFragment {

    private EditText editText;

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setStyle(DialogFragment.STYLE_NO_TITLE, R.style.DialogStyle);
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        setCancelable(true);
        Window window = getDialog().getWindow();
        // 解决未全屏显示的问题
        window.getDecorView().setPadding(0, 0, 0, 0);
        WindowManager.LayoutParams wlp = window.getAttributes();
        wlp.gravity = Gravity.BOTTOM;
        wlp.width = WindowManager.LayoutParams.MATCH_PARENT;
        wlp.height = WindowManager.LayoutParams.WRAP_CONTENT;

        View view = inflater.inflate(R.layout.dialog_test, container);
        editText = view.findViewById(R.id.editText);

        editText.requestFocus();
        openKeyboard();

        getDialog().setOnCancelListener(new DialogInterface.OnCancelListener() {
            @Override
            public void onCancel(DialogInterface dialog) {
                // TODO 取消监听
            }
        });
        return view;
    }

    @Override
    public void onStart() {
        super.onStart();
    }

    @Override
    public void onDismiss(@NonNull DialogInterface dialog) {
        closeKeyboard();
        super.onDismiss(dialog);
        FragmentManager fragmentManager = getChildFragmentManager();
        FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
        if (fragmentManager.getBackStackEntryCount() > 0)
            fragmentManager.popBackStack();
        fragmentTransaction.commit();
    }

    private void openKeyboard() {
        InputMethodManager imm =
                (InputMethodManager) editText.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
        if (imm != null)
            imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT);
    }

    private void closeKeyboard() {
        InputMethodManager imm =
                (InputMethodManager) editText.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
        if (imm != null) {
            imm.hideSoftInputFromWindow(editText.getWindowToken(), 0);
            imm.toggleSoftInput(0, InputMethodManager.HIDE_NOT_ALWAYS);
        }
    }
}