如何在安卓系统中检查软键盘的可见性?

559

我需要做一件非常简单的事情——找出软键盘是否显示。这在安卓系统中可行吗?


9
尽管Reuben Scratton的回答很好,但在平板电脑上似乎有问题。我将检查diff> 128替换为diff> screenHeight/3。 - kingston
2
Reuben Scratton的回答很好,但我需要KaChi的调整才能真正使用它。 - Cullan
2
为什么谷歌不为所有的键盘应用程序制作一个通用的内置方法? - 林果皞
5
我很在意这不是一个系统功能的事实。 - longi
1
这个API仍然缺失了10年,真是太疯狂了。非常高兴我已经远离Android了。 - Reuben Scratton
显示剩余7条评论
45个回答

698

新增答案 于2012年1月25日添加

在撰写以下答案后,有人向我介绍了存在于SDK中自版本1以来一直潜伏着的ViewTreeObserver和相关API。

与其需要自定义布局类型,一个更简单的解决方案是为您的活动根视图(例如@+id/activityRoot)分配已知ID,将GlobalLayoutListener挂钩到ViewTreeObserver中,并从那里计算您的活动视图根与窗口大小之间的大小差异:

final View activityRootView = findViewById(R.id.activityRoot);
activityRootView.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
    @Override
    public void onGlobalLayout() {
        int heightDiff = activityRootView.getRootView().getHeight() - activityRootView.getHeight();
        if (heightDiff > dpToPx(this, 200)) { // if more than 200 dp, it's probably a keyboard...
            // ... do something here
        }
     }
});

使用类似以下工具:

public static float dpToPx(Context context, float valueInDp) {
    DisplayMetrics metrics = context.getResources().getDisplayMetrics();
    return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, valueInDp, metrics);
}

简单易懂!

注意: 您的应用程序必须在Android清单中设置此标志android:windowSoftInputMode="adjustResize",否则上述解决方案将无法正常工作。

原始答案

是的,这是可能的,但比它应该做的要难得多。

如果我需要关心键盘何时出现和消失(这很常见),那么我会自定义我的顶层布局类,覆盖onMeasure()方法。基本逻辑是,如果布局发现自己填充了比窗口总面积少得多的空间,则可能显示软键盘。

import android.app.Activity;
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.widget.LinearLayout;

/*
 * LinearLayoutThatDetectsSoftKeyboard - a variant of LinearLayout that can detect when 
 * the soft keyboard is shown and hidden (something Android can't tell you, weirdly). 
 */

public class LinearLayoutThatDetectsSoftKeyboard extends LinearLayout {

    public LinearLayoutThatDetectsSoftKeyboard(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public interface Listener {
        public void onSoftKeyboardShown(boolean isShowing);
    }
    private Listener listener;
    public void setListener(Listener listener) {
        this.listener = listener;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int height = MeasureSpec.getSize(heightMeasureSpec);
        Activity activity = (Activity)getContext();
        Rect rect = new Rect();
        activity.getWindow().getDecorView().getWindowVisibleDisplayFrame(rect);
        int statusBarHeight = rect.top;
        int screenHeight = activity.getWindowManager().getDefaultDisplay().getHeight();
        int diff = (screenHeight - statusBarHeight) - height;
        if (listener != null) {
            listener.onSoftKeyboardShown(diff>128); // assume all soft keyboards are at least 128 pixels high
        }
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);       
    }

    }

然后在您的Activity类中...

public class MyActivity extends Activity implements LinearLayoutThatDetectsSoftKeyboard.Listener {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
        LinearLayoutThatDetectsSoftKeyboard mainLayout = (LinearLayoutThatDetectsSoftKeyboard)findViewById(R.id.main);
        mainLayout.setListener(this);
        ...
    }


    @Override
    public void onSoftKeyboardShown(boolean isShowing) {
        // do whatever you need to do here
    }

    ...
}

63
在我意识到你必须在你的活动中设置以下属性之前,这对我是不起作用的:android:windowSoftInputMode="adjustResize"。 - ajh158
9
看起来起作用了。此外,如果你不知道根视图的ID,你可以通过以下方式获取视图:((ViewGroup) findViewById(android.R.id.content)).getChildAt(0) - Goldsmith
9
如果您尝试使用实际根视图(android.R.id.content),则可以更自信地说System而不是您的应用程序是更改其高度的实体。让安卓团队给我们一些基本的关于软键盘输入的信息,这样会更加安全。 - Graeme
14
请注意,heightDiff 中将始终包括操作栏的高度。在新答案中,通过检查该高度是否大于某个常量来忽略了这一点,但对于像 Nexus 4 这样的 xxhdpi 设备,100 像素不足够。如果您真的想使用这种粗糙的解决方法,请考虑将该值转换为 DP。 - Paul Lammertsma
9
注意:与WindowManager.LayoutParams.FLAG_FULLSCREEN和全屏主题不兼容。 - VAV
显示剩余52条评论

311

希望这能够帮助到有需要的人。

Reuben Scratton给出的新答案非常棒且高效,但只有在将windowSoftInputMode设置为adjustResize时才有效。如果你将其设置为adjustPan,则仍然无法使用他的代码片段来检测键盘是否可见。为了解决这个问题,我对上面的代码进行了微小的修改。

final View activityRootView = findViewById(R.id.activityRoot);
activityRootView.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
    Rect r = new Rect();
    //r will be populated with the coordinates of your view that area still visible.
    activityRootView.getWindowVisibleDisplayFrame(r);
   
    int heightDiff = activityRootView.getRootView().getHeight() - r.height();
    if (heightDiff > 0.25*activityRootView.getRootView().getHeight()) { // if more than 25% of the screen, its probably a keyboard...
        ... do something here
    }
 }
}); 

1
适用于 ActionBarActionBarSherlock。非常感谢!顺便说一下,还有一个方法 r.height() :) - Dmitry Zaytsev
1
我使用类似的设置,但你的设置不允许使用中日韩键盘,“建议”栏(必需品)会使屏幕区域增大或缩小超过100个单位(Nexus 10)。相反地,在可能的情况下,尝试使用ScreenHeight / 4作为“调整”值,在所有情况下我们发现键盘至少占据了屏幕的1/4,并且任何“建议栏”都占据不到1/4。 (记得添加一个onOrientationChange方法来重置1/4的值)。 - Graeme
9
"heightDiff > root.getRootView().getHeight() / 4" 是在高分辨率设备上可行的好值。而 100px 太短了。 在具有 1080x1920 分辨率的 Nexus 5 上, 1920 - (996-75) >? 100 = 999 1920 - (1776-75) >? 100 = 219 // 键盘弹出 在具有 480x800 分辨率的 galaxy s2 上, 800 - (800-38) >? 100 = 38 800 - (410-38) >? 100 = 428 // 键盘弹出 因此,神奇的 100px 数字不够好用。 - Flask_KR
1
android:windowSoftInputMode="adjustNothing"怎么样?谢谢。我需要将android:windowSoftInputMode设置为adjustNothing - Jerikc XIONG
3
在较新的手机(如Pixel 3 XL)上出现了故障。 - Nick
显示剩余17条评论

57

在计算机领域,这个问题的存在已经很久了,但仍然是一个不可思议的相关问题!

所以我整合并优化了上述答案...

public interface OnKeyboardVisibilityListener {


    void onVisibilityChanged(boolean visible);
}

public final void setKeyboardListener(final OnKeyboardVisibilityListener listener) {
    final View activityRootView = ((ViewGroup) getActivity().findViewById(android.R.id.content)).getChildAt(0);

    activityRootView.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {

        private boolean wasOpened;

        private final int DefaultKeyboardDP = 100;

        // From @nathanielwolf answer...  Lollipop includes button bar in the root. Add height of button bar (48dp) to maxDiff
        private final int EstimatedKeyboardDP = DefaultKeyboardDP + (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? 48 : 0);

        private final Rect r = new Rect();

        @Override
        public void onGlobalLayout() {
            // Convert the dp to pixels.
            int estimatedKeyboardHeight = (int) TypedValue
                    .applyDimension(TypedValue.COMPLEX_UNIT_DIP, EstimatedKeyboardDP, activityRootView.getResources().getDisplayMetrics());

            // Conclude whether the keyboard is shown or not.
            activityRootView.getWindowVisibleDisplayFrame(r);
            int heightDiff = activityRootView.getRootView().getHeight() - (r.bottom - r.top);
            boolean isShown = heightDiff >= estimatedKeyboardHeight;

            if (isShown == wasOpened) {
                Log.d("Keyboard state", "Ignoring global layout change...");
                return;
            }

            wasOpened = isShown;
            listener.onVisibilityChanged(isShown);
        }
    });
}

对我来说没问题 :)

注意: 如果你发现 DefaultKeyboardDP 不适合你的设备,请调整该值并发布评论让大家知道该值应该是什么...最终我们会得到正确的值以适配所有设备!

更多详情请查看在 Cyborg 上的实现。


2
+1 非常感谢!我尝试了其他答案,但它们都不起作用。然后我找到了你的答案,它像魔法一样奏效。太棒了! :D - Kevin van Mierlo
只有在添加:android:windowSoftInputMode="stateHidden|adjustPan"或android:windowSoftInputMode="stateHidden|adjustResize"时才能正常工作。谢谢! - Lena Bru
如果你只想知道键盘是否已经打开...使用这个! - EGHDK
2
一个小修正: private final int EstimatedKeyboardDP = DefaultKeyboardDP + (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? 48 : 0); - binaryKarmic
2
太棒了!无论"windowSoftInputMode"设置为"adjustPan" / "adjustResize" / "adjustPan|stateHidden" / "adjustResize|stateHidden",甚至没有这个选项,它总是有效的!在小米8上进行了测试。 - Zhou Hongbo
显示剩余10条评论

51

抱歉回答晚了,但是我创建了一个小帮助类来处理打开/关闭事件并通知监听器和其他有用的事情,也许有人会发现它有帮助:

import android.graphics.Rect;
import android.view.View;
import android.view.ViewTreeObserver;

import java.util.LinkedList;
import java.util.List;

public class SoftKeyboardStateWatcher implements ViewTreeObserver.OnGlobalLayoutListener {

    public interface SoftKeyboardStateListener {
        void onSoftKeyboardOpened(int keyboardHeightInPx);
        void onSoftKeyboardClosed();
    }

    private final List<SoftKeyboardStateListener> listeners = new LinkedList<SoftKeyboardStateListener>();
    private final View activityRootView;
    private int        lastSoftKeyboardHeightInPx;
    private boolean    isSoftKeyboardOpened;

    public SoftKeyboardStateWatcher(View activityRootView) {
        this(activityRootView, false);
    }

    public SoftKeyboardStateWatcher(View activityRootView, boolean isSoftKeyboardOpened) {
        this.activityRootView     = activityRootView;
        this.isSoftKeyboardOpened = isSoftKeyboardOpened;
        activityRootView.getViewTreeObserver().addOnGlobalLayoutListener(this);
    }

    @Override
    public void onGlobalLayout() {
        final Rect r = new Rect();
        //r will be populated with the coordinates of your view that area still visible.
        activityRootView.getWindowVisibleDisplayFrame(r);

        final int heightDiff = activityRootView.getRootView().getHeight() - (r.bottom - r.top);
        if (!isSoftKeyboardOpened && heightDiff > 100) { // if more than 100 pixels, its probably a keyboard...
            isSoftKeyboardOpened = true;
            notifyOnSoftKeyboardOpened(heightDiff);
        } else if (isSoftKeyboardOpened && heightDiff < 100) {
            isSoftKeyboardOpened = false;
            notifyOnSoftKeyboardClosed();
        }
    }

    public void setIsSoftKeyboardOpened(boolean isSoftKeyboardOpened) {
        this.isSoftKeyboardOpened = isSoftKeyboardOpened;
    }

    public boolean isSoftKeyboardOpened() {
        return isSoftKeyboardOpened;
    }

    /**
     * Default value is zero {@code 0}.
     *
     * @return last saved keyboard height in px
     */
    public int getLastSoftKeyboardHeightInPx() {
        return lastSoftKeyboardHeightInPx;
    }

    public void addSoftKeyboardStateListener(SoftKeyboardStateListener listener) {
        listeners.add(listener);
    }

    public void removeSoftKeyboardStateListener(SoftKeyboardStateListener listener) {
        listeners.remove(listener);
    }

    private void notifyOnSoftKeyboardOpened(int keyboardHeightInPx) {
        this.lastSoftKeyboardHeightInPx = keyboardHeightInPx;

        for (SoftKeyboardStateListener listener : listeners) {
            if (listener != null) {
                listener.onSoftKeyboardOpened(keyboardHeightInPx);
            }
        }
    }

    private void notifyOnSoftKeyboardClosed() {
        for (SoftKeyboardStateListener listener : listeners) {
            if (listener != null) {
                listener.onSoftKeyboardClosed();
            }
        }
    }
}

使用示例:

final SoftKeyboardStateWatcher softKeyboardStateWatcher 
    = new SoftKeyboardStateWatcher(findViewById(R.id.activity_main_layout);

// Add listener
softKeyboardStateWatcher.addSoftKeyboardStateListener(...);
// then just handle callbacks

2
这个类不小,但实现肯定是的 :). 谢谢,我会尝试的 :) - Atul O Holic
1
如果你对它有问题,请给我发消息:)我在两个项目中成功使用了它。 - Artem Zinnatullin
有些键盘在顶部会有一排额外的自定义按键(例如预测单词)。显然,这些并不是键盘本身的一部分,因为使用 getLastKeyboardHeightInPx() 不包括该行的高度。您知道如何考虑这一点吗? - ygesher
清晰的解决方案,谢谢。 - Cüneyt
这应该是最好的答案。像魅力一样工作 :) - param
显示剩余6条评论

35

避免在高密度设备上错误地检测软键盘的可见性需要一些改进:

  1. 高度差的阈值应该定义为128 dp,而不是128像素
    请参考Google设计文档关于度量和网格的内容48 dp是触摸对象的舒适大小,32 dp是按钮的最小大小。通用软键盘应包括4行键按钮,因此最小键盘高度应为:32 dp * 4 = 128 dp,这意味着阈值大小应通过乘以设备密度转换为像素。对于xxxhdpi设备(密度为4),软键盘高度阈值应为128 * 4 = 512像素。

  2. 根视图和其可见区域之间的高度差:
    根视图高度 - 状态栏高度 - 可见框架高度 = 根视图底部 - 可见框架底部,因为状态栏高度等于根视图可见框架的顶部。

private final String TAG = "TextEditor";
private TextView mTextEditor;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_editor);
    mTextEditor = (TextView) findViewById(R.id.text_editor);
    mTextEditor.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
            isKeyboardShown(mTextEditor.getRootView());
        }
    });
}

private boolean isKeyboardShown(View rootView) {
    /* 128dp = 32dp * 4, minimum button height 32dp and generic 4 rows soft keyboard */
    final int SOFT_KEYBOARD_HEIGHT_DP_THRESHOLD = 128;

    Rect r = new Rect();
    rootView.getWindowVisibleDisplayFrame(r);
    DisplayMetrics dm = rootView.getResources().getDisplayMetrics();
    /* heightDiff = rootView height - status bar height (r.top) - visible frame height (r.bottom - r.top) */
    int heightDiff = rootView.getBottom() - r.bottom;
    /* Threshold size: dp to pixels, multiply with display density */
    boolean isKeyboardShown = heightDiff > SOFT_KEYBOARD_HEIGHT_DP_THRESHOLD * dm.density;

    Log.d(TAG, "isKeyboardShown ? " + isKeyboardShown + ", heightDiff:" + heightDiff + ", density:" + dm.density
            + "root view height:" + rootView.getHeight() + ", rect:" + r);

    return isKeyboardShown;
}

4
这个回答值得被采纳。如果忽略密度,在像素大小相似但形状不同的设备上,结果会有很大的区别。谢谢! - Ginger McMurray
2
谢谢...这是一个更好的条件! - TacB0sS
1
太好了,isKeyboardShown() 方法正是我们所需要的。谢谢。 - Danylo Volokh
它在2020年仍然有效。完美的答案。我尝试了所有的代码,但只有这个完美地运行。 - Mitesh Jain

31

哇,我们有好消息要告诉安卓爱好者们了。是时候告别旧的方法了。 首先,我会添加官方发布说明以了解更多关于这些方法/类的信息,然后我们将看到这些令人惊叹的方法/类。

注意:在这些类/方法发布之前,请勿将其添加到您的发布应用程序中

如何检查键盘可见性

val insets = ViewCompat.getRootWindowInsets(view)
val isKeyboardVisible = insets.isVisible(Type.ime())

很少有其他实用程序

如何获取键盘的高度

val insets = ViewCompat.getRootWindowInsets(view)
val keyboardHeight = insets.getInsets(Type.ime()).bottom

如何显示/隐藏键盘

val controller = view.windowInsetsController

// Show the keyboard
controller.show(Type.ime())

// Hide the keyboard
controller.hide(Type.ime())

注意: WindowInsetsController 是在API-30中添加的,因此请等待兼容性类可用。

如何监听键盘隐藏/显示事件

ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets ->
    val isKeyboardVisible = insets.isVisible(Type.ime())
    if (isKeyboardVisible) {
        // Do it when keyboard is being shown
    } else {
        // Do it when keyboard is hidden
    }

    // Return the insets to keep going down this event to the view hierarchy
    insets
}

3
我有一个问题,它显示:“Unresolved reference isVisible()”,请帮忙。 - Andrew
@PankajKumar,我该如何使用Android Jetpack Compose获取键盘高度? - Asad Yasin
3
哇,终于!经过这么多年的否认,它被证明是必需的。 - enl8enmentnow
有人成功让它在横屏模式下工作了吗?我发现在 API 21-29 的手机上,这只能在竖屏模式下工作。请参见 https://stackoverflow.com/questions/69372950/detect-when-a-keyboard-is-shown-hidden-issues-in-android-with-onapplywindowinset - vallllll
似乎在将manifest设置为adjustPan时无法一致地调用。但是使用adjustResize效果很好。最终我改为使用adjustResize,并在此基础上微调其他所有内容,以便监听器能够正确工作。 - Cody
显示剩余6条评论

8

如果您需要同时隐藏键盘并检查软输入状态,则可以使用以下解决方案:

该想法是,如果您需要同时隐藏键盘并检查软输入状态,请使用以下解决方案:
public boolean hideSoftInput() {
    InputMethodManager imm = (InputMethodManager) getSystemService(Activity.INPUT_METHOD_SERVICE);
    return imm.hideSoftInputFromWindow(mViewPager.getWindowToken(), 0);
}

该方法返回true,如果键盘在隐藏之前已经显示。

这个代码不需要使用高度等参数就能正常工作...谢谢...你节省了我的时间... - Happy
这应该是被接受的答案。所有这些黑科技... 唉。API没有说明布尔值是什么,所以这个方法安全吗? - John Glen
您可以将一个ResultReceiver传递给hideSoftInputFromWindow,它将给您一个明确的答复。 - John Glen

7

我花了一点时间来理解这个问题...我遇到了一些CastException,但是发现你可以在layout.xml中用类名替换LinearLayout。

像这样:

<?xml version="1.0" encoding="UTF-8"?>
<LinearLayout android:layout_width="fill_parent" android:layout_height="fill_parent"
    xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/llMaster">

<com.ourshoppingnote.RelativeLayoutThatDetectsSoftKeyboard android:background="@drawable/metal_background"
    android:layout_width="fill_parent" android:layout_height="fill_parent"
    android:id="@+id/rlMaster" >
    <LinearLayout android:layout_width="fill_parent"
        android:layout_height="1dip" android:background="@drawable/line"></LinearLayout>

          ....

</com.ourshoppingnote.RelativeLayoutThatDetectsSoftKeyboard>    


</LinearLayout>

这样你就不会遇到任何类型转换问题。

如果你不想在每个页面上都这样做,我建议你使用“Android中的MasterPage”。请查看此链接: http://jnastase.alner.net/archive/2011/01/08/ldquomaster-pagesrdquo-in-android.aspx


哇,如果你的包名或类名不一样,小心把这个粘贴到你的XML文件中。Eclipse会决定冻结,你必须关闭它。这是一个如此专业的产品。/s - Spencer Ruport
5
@SpencerRuport,这就是为什么它是免费的。 - Cody
@DoctorOreo - 下载 IntelliJ。它是免费的,而且不会让人感到糟糕。 - Mark
@Mark - 在我发布这篇文章几个月后,我确实尝试了IntelliJ。在我看来,它比Eclipse好多了。他们的大部分产品我认为都非常优秀。我甚至买了几个。 - Cody
抱歉让这个旧的评论线重生。我很高兴你正在使用并享受它。我也喜欢使用IntelliJ以及AppCode用于iOS和PyCharm用于Python工作。干杯! - Mark

6

4

还有一种使用系统插入的解决方案,但是它只适用于API >= 21Android L)。假设你有一个BottomNavigationView,它是LinearLayout的子项,并且当键盘显示时需要隐藏它:

> LinearLayout
  > ContentView
  > BottomNavigationView

你只需要扩展LinearLayout,方法如下:
public class KeyboardAwareLinearLayout extends LinearLayout {
    public KeyboardAwareLinearLayout(Context context) {
        super(context);
    }

    public KeyboardAwareLinearLayout(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public KeyboardAwareLinearLayout(Context context,
                                     @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public KeyboardAwareLinearLayout(Context context, AttributeSet attrs,
                                     int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    public WindowInsets onApplyWindowInsets(WindowInsets insets) {
        int childCount = getChildCount();
        for (int index = 0; index < childCount; index++) {
            View view = getChildAt(index);
            if (view instanceof BottomNavigationView) {
                int bottom = insets.getSystemWindowInsetBottom();
                if (bottom >= ViewUtils.dpToPx(200)) {
                    // keyboard is shown
                    view.setVisibility(GONE);
                } else {
                    // keyboard is hidden
                    view.setVisibility(VISIBLE);
                }
            }
        }
        return insets;
    }
}

这个想法是当键盘弹出时,系统插入值会变化,.bottom 值会有很大的改变。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接