如何在Android上点击EditText外部后隐藏软键盘?

426

大家都知道隐藏键盘需要实现:

InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);

但是这里的大问题是如何在用户触摸或选择任何不是EditText或软键盘的其他地方时隐藏键盘?

我尝试在我的父Activity上使用onTouchEvent(),但只有当用户触摸到任何其他视图之外并且没有滚动视图时才起作用。

我尝试实现了一个触摸、点击、焦点监听器,但都没有成功。

我甚至尝试实现自己的滚动视图来拦截触摸事件,但我只能获得事件的坐标而无法获取所点击的视图。

是否有一种标准方法来解决这个问题?在iPhone上,这真的很容易。


我意识到ScrollView并不是问题所在,而是其中的标签。该视图是一个垂直布局,包含如下内容: TextView、EditText、TextView、EditText等等。而这些TextView会阻止EditText失去焦点并隐藏键盘。 - htafoya
你可以在这里找到 getFields() 的解决方案:https://dev59.com/e2sz5IYBdhLWcg3wlY-9 - Reto
键盘可以通过按回车键来关闭,因此我认为这是否值得努力是有问题的。 - gerrytan
5
我找到了这篇回答:https://dev59.com/7m445IYBdhLWcg3wfKcZ#28939113,是最好的一个。 - Loenix
49个回答

11

我修改了 Andre Luis IM 的解决方案,得到了这个:

我创建了一个实用方法来以与 Andre Luiz IM 相同的方式隐藏软键盘:

public static void hideSoftKeyboard(Activity activity) {
    InputMethodManager inputMethodManager = (InputMethodManager)  activity.getSystemService(Activity.INPUT_METHOD_SERVICE);
    inputMethodManager.hideSoftInputFromWindow(activity.getCurrentFocus().getWindowToken(), 0);
}

但是,不要为每个视图都注册OnTouchListener,这会导致性能不佳,相反,只需为根视图注册OnTouchListener。由于事件一直冒泡直到被消耗(EditText默认情况下是其中之一),如果它到达根视图,那么就是因为它没有被消耗,所以我关闭软键盘。

findViewById(android.R.id.content).setOnTouchListener(new OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        Utils.hideSoftKeyboard(activity);
        return false;
    }
});

11

我知道这个帖子已经很旧了,而且正确的答案似乎是有效的,有很多解决方案可用,但我认为下面提到的方法可能会在效率和优雅方面具有额外的好处。

我需要所有我的活动都具有此行为,因此我创建了一个从类Activity继承的类CustomActivity并“挂钩”了dispatchTouchEvent函数。主要有两个条件需要注意:

  1. 如果焦点未更改并且有人在当前输入字段之外点击,则关闭IME
  2. 如果焦点已更改并且下一个焦点元素不是任何类型的输入字段实例,则关闭IME

这是我的结果:

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    if(ev.getAction() == MotionEvent.ACTION_UP) {
        final View view = getCurrentFocus();

        if(view != null) {
            final boolean consumed = super.dispatchTouchEvent(ev);

            final View viewTmp = getCurrentFocus();
            final View viewNew = viewTmp != null ? viewTmp : view;

            if(viewNew.equals(view)) {
                final Rect rect = new Rect();
                final int[] coordinates = new int[2];

                view.getLocationOnScreen(coordinates);

                rect.set(coordinates[0], coordinates[1], coordinates[0] + view.getWidth(), coordinates[1] + view.getHeight());

                final int x = (int) ev.getX();
                final int y = (int) ev.getY();

                if(rect.contains(x, y)) {
                    return consumed;
                }
            }
            else if(viewNew instanceof EditText || viewNew instanceof CustomEditText) {
                return consumed;
            }

            final InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);

            inputMethodManager.hideSoftInputFromWindow(viewNew.getWindowToken(), 0);

            viewNew.clearFocus();

            return consumed;
        }
    }       

    return super.dispatchTouchEvent(ev);
}

顺便提一下:此外,我将这些属性分配给根视图,使得可以清除每个输入字段的焦点,并防止在活动启动时输入字段获得焦点(将内容视图作为“焦点捕捉器”):

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    final View view = findViewById(R.id.content);

    view.setFocusable(true);
    view.setFocusableInTouchMode(true);
}

超棒的,它运行得很好,谢谢!我为你的答案点了一个赞。 - vijay
2
我认为这是复杂布局的最佳解决方案。但是到目前为止,我发现了两个缺点:
  1. EditText上下文菜单无法点击 - 任何点击都会导致EditText失去焦点
  2. 当我们的EditText位于视图底部并且我们长按它(选择单词),然后键盘显示后,我们的“点击点”在键盘上而不是EditText上 - 所以我们再次失去焦点 :/
- wrozwad
@sosite,我认为我在我的回答中解决了这些限制问题,请看一下。 - Andy Dennie
@sosite 我正在使用类似的代码,上下文菜单没有任何问题。上下文菜单上的触摸事件没有分派到我的活动中。 - Kamen Dobrev

11

不要遍历所有视图或覆盖dispatchTouchEvent,而是重写Activity的onUserInteraction()方法,这样可以确保在用户点击EditText外部时键盘会消失。

即使EditText位于scrollView内也可以正常工作。

@Override
public void onUserInteraction() {
    if (getCurrentFocus() != null) {
        InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
        imm.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);
    }
}

这是更好的答案。 - ovluca
1
是的!这是一个非常简洁的答案。如果您创建一个所有其他活动都扩展的AbstractActivity,您可以将其作为应用程序中的默认行为。 - wildcat12
在 Kotlin 中,if (currentFocus != null && currentFocus !is EditText) 让我多走了一英里。 - Thinkal VB
工作得很好,但在滚动期间隐藏键盘。 - CoolMind

8
在Kotlin中,我们可以执行以下操作。无需迭代所有视图,也适用于片段。
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
    currentFocus?.let {
        val imm: InputMethodManager = getSystemService(
            Context.INPUT_METHOD_SERVICE
        ) as (InputMethodManager)
        imm.hideSoftInputFromWindow(it.windowToken, 0)
    }
    return super.dispatchTouchEvent(ev)
}

2
这个程序可以运行,但是有一个bug。例如,如果我想在TextView中粘贴文本,键盘会隐藏然后再次显示,这有点烦人。 - George Shalvashvili
1
很好的答案对我来说运作良好,尽管正如George所说,在我的情况下,使用明文(例如在搜索小部件上),键盘会隐藏然后再次显示。 - trainmania100

6

我喜欢htafoya提出的调用dispatchTouchEvent的方法,但是:

  • 我不理解计时器部分(不知道为什么需要测量停机时间?)
  • 我不喜欢在每次视图更改时注册/取消所有EditText(在复杂的层次结构中可能会有很多视图更改和edittext)

因此,我提供了这个相对简单的解决方案:

@Override
public boolean dispatchTouchEvent(final MotionEvent ev) {
    // all touch events close the keyboard before they are processed except EditText instances.
    // if focus is an EditText we need to check, if the touchevent was inside the focus editTexts
    final View currentFocus = getCurrentFocus();
    if (!(currentFocus instanceof EditText) || !isTouchInsideView(ev, currentFocus)) {
        ((InputMethodManager) getApplicationContext().getSystemService(Context.INPUT_METHOD_SERVICE))
            .hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
    }
    return super.dispatchTouchEvent(ev);
}

/**
 * determine if the given motionevent is inside the given view.
 * 
 * @param ev
 *            the given view
 * @param currentFocus
 *            the motion event.
 * @return if the given motionevent is inside the given view
 */
private boolean isTouchInsideView(final MotionEvent ev, final View currentFocus) {
    final int[] loc = new int[2];
    currentFocus.getLocationOnScreen(loc);
    return ev.getRawX() > loc[0] && ev.getRawY() > loc[1] && ev.getRawX() < (loc[0] + currentFocus.getWidth())
        && ev.getRawY() < (loc[1] + currentFocus.getHeight());
}

这里有一个缺点:

从一个 EditText 切换到另一个 EditText 会使键盘隐藏和重新显示 - 在我的情况下,这是期望的方式,因为它显示了您在两个输入组件之间切换。


从能够轻松地插入和播放我的FragmentActivity的角度来看,这种方法是最好的。 - Andrew Aarestad
谢谢,这是最好的方法!我还添加了事件动作检查:int action = ev.getActionMasked(); if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN) {...} - sergey.n
谢谢!你节省了我的时间。最佳答案。 - I.d007

6

请求:我知道自己没有什么影响力,但请认真考虑我的答案。

问题:点击键盘外部或最小化代码时如何关闭软键盘。

解决方案:使用外部库Butterknife。

一行解决方案:

@OnClick(R.id.activity_signup_layout) public void closeKeyboard() { ((InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE)).hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0); }

更易读的解决方案:

@OnClick(R.id.activity_signup_layout) 
public void closeKeyboard() {
        InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
        imm.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);
}

解释:将OnClick监听器绑定到活动的XML布局父ID上,这样点击布局(而不是编辑文本或键盘)将运行该代码片段,隐藏键盘。

示例:如果您的布局文件是R.layout.my_layout,布局ID是R.id.my_layout_id,则Butterknife绑定调用应如下所示:

(@OnClick(R.id.my_layout_id) 
public void yourMethod {
    InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
    imm.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);
}

Butterknife文档链接: http://jakewharton.github.io/butterknife/

推荐使用: Butterknife可以改变你的android开发方式。建议尝试。

注意: 不使用外部库Butterknife也可以实现相同的结果。只需像上面描述的那样将OnClickListener设置为父布局即可。


太棒了,完美的解决方案!谢谢。 - nullforlife

5

很简单,只需使用以下代码使最近的布局可点击且可聚焦:

android:id="@+id/loginParentLayout"
android:clickable="true"
android:focusableInTouchMode="true"

然后为该布局编写一个方法和一个OnClickListner,以便当最上面的布局在任何地方被触摸时,它将调用一个方法,在该方法中你将编写代码来关闭键盘。以下是两者的代码; // 你需要在OnCreate()中编写此代码

 yourLayout.setOnClickListener(new View.OnClickListener(){
                @Override
                public void onClick(View view) {
                    hideKeyboard(view);
                }
            });

从监听器调用的方法:

 public void hideKeyboard(View view) {
     InputMethodManager imm =(InputMethodManager)getSystemService(Activity.INPUT_METHOD_SERVICE);
        imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
    }

4

我觉得对于这个简单的需求,接受的答案有点复杂。以下是我用没有任何问题的方法解决的。

findViewById(R.id.mainLayout).setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View view, MotionEvent motionEvent) {
            InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
            imm.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);
            return false;
        }
    });

1
如果您使用NestedScrollView或复杂布局,请参见被接受的答案:https://dev59.com/rW855IYBdhLWcg3wvnKE#11656129。您应该知道其他容器可能会消耗触摸事件。 - CoolMind

4
这是fje的答案的另一种变体,可以解决sosite提出的问题。
这里的思路是在Activity的`dispatchTouchEvent`方法中处理向下和向上的动作。在向下的操作中,我们注意当前焦点视图(如果有)以及触摸是否在其中,保存这两个信息以备后用。
在向上的操作中,我们首先进行调度,以允许另一个视图可能获得焦点。如果在此之后,当前聚焦的视图仍然是最初聚焦的视图,并且向下的触摸在该视图内部,则保持键盘打开。
如果当前聚焦的视图与最初聚焦的视图不同,并且它是一个`EditText`,则我们也保持键盘打开。
否则,我们关闭它。
总之,它的工作方式如下:
  • when touching inside a currently focused EditText, the keyboard stays open
  • when moving from a focused EditText to another EditText, the keyboard stays open (doesn't close/reopen)
  • when touching anywhere outside a currently focused EditText that is not another EditText, the keyboard closes
  • when long-pressing in an EditText to bring up the contextual action bar (with the cut/copy/paste buttons), the keyboard stays open, even though the UP action took place outside the focused EditText (which moved down to make room for the CAB). Note, though, that when you tap on a button in the CAB, it will close the keyboard. That may or may not be desirable; if you want to cut/copy from one field and paste to another, it would be. If you want to paste back into the same EditText, it would not be.
  • when the focused EditText is at the bottom of the screen and you long-click on some text to select it, the EditText keeps focus and therefore the keyboard opens like you want, because we do the "touch is within view bounds" check on the down action, not the up action.

    private View focusedViewOnActionDown;
    private boolean touchWasInsideFocusedView;
    
    
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                focusedViewOnActionDown = getCurrentFocus();
                if (focusedViewOnActionDown != null) {
                    final Rect rect = new Rect();
                    final int[] coordinates = new int[2];
    
                    focusedViewOnActionDown.getLocationOnScreen(coordinates);
    
                    rect.set(coordinates[0], coordinates[1],
                            coordinates[0] + focusedViewOnActionDown.getWidth(),
                            coordinates[1] + focusedViewOnActionDown.getHeight());
    
                    final int x = (int) ev.getX();
                    final int y = (int) ev.getY();
    
                    touchWasInsideFocusedView = rect.contains(x, y);
                }
                break;
    
            case MotionEvent.ACTION_UP:
    
                if (focusedViewOnActionDown != null) {
                    // dispatch to allow new view to (potentially) take focus
                    final boolean consumed = super.dispatchTouchEvent(ev);
    
                    final View currentFocus = getCurrentFocus();
    
                    // if the focus is still on the original view and the touch was inside that view,
                    // leave the keyboard open.  Otherwise, if the focus is now on another view and that view
                    // is an EditText, also leave the keyboard open.
                    if (currentFocus.equals(focusedViewOnActionDown)) {
                        if (touchWasInsideFocusedView) {
                            return consumed;
                        }
                    } else if (currentFocus instanceof EditText) {
                        return consumed;
                    }
    
                    // the touch was outside the originally focused view and not inside another EditText,
                    // so close the keyboard
                    InputMethodManager inputMethodManager =
                            (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
                    inputMethodManager.hideSoftInputFromWindow(
                        focusedViewOnActionDown.getWindowToken(), 0);
                    focusedViewOnActionDown.clearFocus();
    
                    return consumed;
                }
                break;
        }
    
        return super.dispatchTouchEvent(ev);
    }
    

4

对于我来说,这是最简单的解决方案(并且由我开发)。

这是隐藏键盘的方法。

public void hideKeyboard(View view){
        if(!(view instanceof EditText)){
            InputMethodManager inputMethodManager=(InputMethodManager)getSystemService(INPUT_METHOD_SERVICE);
            inputMethodManager.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(),0);
        }
    }

现在,将活动的父布局的onclick属性设置为上述方法hideKeyboard,可以通过XML文件的设计视图或在XML文件的文本视图中编写以下代码来实现。
android:onClick="hideKeyboard"

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