EditText,在触摸外部时清除焦点

138

我的布局包含ListViewSurfaceViewEditText。当我点击EditText时,它会获得焦点并出现屏幕键盘。当我在EditText之外的某个地方点击时,它仍然保持焦点(这不应该)。

我猜我可以在布局中的其他视图上设置OnTouchListener,并手动清除EditText的焦点。但是这样看起来很hackish...

我在另一个布局中也遇到了同样的情况——列表视图具有不同类型的项目,其中一些包含EditText。它们的行为就像我上面写的那样。

任务是使当用户触摸EditText之外的区域时,EditText失去焦点。

我在这里看到了类似的问题,但没有找到任何解决方案...

18个回答

299

在基于Ken的答案之上,这是最模块化的复制粘贴解决方案。

无需XML。

将其放入您的Activity中,它将适用于所有EditText,包括那个Activity中片段内的EditText。

@Override
public boolean dispatchTouchEvent(MotionEvent event) {
    if (event.getAction() == MotionEvent.ACTION_DOWN) {
        View v = getCurrentFocus();
        if ( v instanceof EditText) {
            Rect outRect = new Rect();
            v.getGlobalVisibleRect(outRect);
            if (!outRect.contains((int)event.getRawX(), (int)event.getRawY())) {
                v.clearFocus();
                InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
                imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
            }
        }
    }
    return super.dispatchTouchEvent( event );
}

13
看起来还不错。但我有一个问题,所有我的编辑文本都在一个滚动视图中,顶部的编辑文本始终显示光标。即使我点击其他地方,焦点丢失并且键盘消失了,但是顶部的编辑文本仍然显示光标。 - Vaibhav Gupta
2
我遇到的最佳解决方案!! 以前我使用的是 @Override public boolean onTouch(View v, MotionEvent event) { if(v.getId()!=search_box.getId()){ if(search_box.hasFocus()) { InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); imm.hideSoftInputFromWindow(search_box.getWindowToken(), 0); search_box.clearFocus(); } return false; } return false; } - Pradeep Kumar Kushwaha
1
干得好。顺便说一下,你还需要添加对话片段的代码,因为对话框有自己的根视图。请参阅 https://dev59.com/knDYa4cB1Zd3GeqPGPmo 了解如何为DialogFragments实现dispatchTouchEvent方法。 - Nantoka
6
如果用户点击另一个EditText,键盘会关闭并立即重新打开。为了解决这个问题,我在您的代码中将“MotionEvent.ACTION_DOWN”更改为“MotionEvent.ACTION_UP”。顺便说一下,答案很棒! - Thanasis1101
1
非常整洁...但是有多个EditText会导致键盘闪烁,@Thanasis1101的答案对我没有用。 - Aelaf
显示剩余3条评论

74

我尝试了所有这些解决方案。edc598的方法最接近有效,但是在布局中包含的其他View上不会触发触摸事件。如果有人需要此行为,请看看我最终采用的方法:

我创建了一个(不可见的)FrameLayout,名为touchInterceptor,并将其作为布局中的最后一个View,以使其覆盖在所有内容上(编辑:您还必须使用RelativeLayout作为父布局,并为touchInterceptor设置fill_parent属性)。然后,我使用它来拦截触摸事件,并确定触摸是否在EditText上方:

FrameLayout touchInterceptor = (FrameLayout)findViewById(R.id.touchInterceptor);
touchInterceptor.setOnTouchListener(new OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            if (mEditText.isFocused()) {
                Rect outRect = new Rect();
                mEditText.getGlobalVisibleRect(outRect);
                if (!outRect.contains((int)event.getRawX(), (int)event.getRawY())) {
                    mEditText.clearFocus();
                    InputMethodManager imm = (InputMethodManager) v.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); 
                    imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
                }
            }
        }
        return false;
    }
});

返回false以使触摸处理透传。

这很糟糕,但这是我唯一有效的方法。


23
您可以通过在活动中重写dispatchTouchEvent(MotionEvent ev)方法来实现相同的操作,而无需添加额外的布局。 - pcans
谢谢你提供的clearFocus()提示,它在SearchView上也对我起作用了。 - Jimmy Ilenloa
1
我想提醒一下@zMan在下面的回答。它基于此,但不需要视图https://dev59.com/7m445IYBdhLWcg3wfKcZ#28939113 - Boy
此外,如果有人遇到键盘未隐藏但焦点已清除的情况,请先调用hideSoftInputFromWindow()然后再清除焦点。 - Ahmet Gokdayi
但是必须将android:focusableInTouchMode="true"添加到最上层的父级。 - Gk Mohammad Emon

37

只需将这些属性放在最顶层的父元素中即可。

android:focusableInTouchMode="true"
android:clickable="true"
android:focusable="true" 

FYI:虽然它适用于大多数情况,但对于布局的某些内部元素无效。 - SV Madhava Reddy
如果没有这个,最佳答案提供的解决方案将无法正常工作。我正在使用片段,因此需要在FrameLayout中放置android:focusableInTouchMode="true"。我正在使用sdk版本29和com.google.android.material:material库。 - Falcon
我非常确定这个解决方案会破坏您应用程序的可访问性。这会导致TalkBack认为最顶层的父元素是一个可交互的元素,并将其读出给用户。 - Shane Neuville

36

对于EditText的父视图,请让以下3个属性为"true":
clickable, focusable, focusableInTouchMode.

如果一个视图想要接收焦点,它必须满足这3个条件。

参见android.view

public boolean onTouchEvent(MotionEvent event) {
    ...
    if (((viewFlags & CLICKABLE) == CLICKABLE || 
        (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
        ...
        if (isFocusable() && isFocusableInTouchMode()
            && !isFocused()) {
                focusTaken = requestFocus();
        }
        ...
    }
    ...
}

希望能对你有所帮助。


完全正确:http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.4.4_r1/android/view/View.java#View.onTouchEvent%28android.view.MotionEvent%29 - AxeEffect
2
为了清除焦点,其他可聚焦视图必须是聚焦视图的父级。如果要聚焦的视图在另一个层次结构上,则无法正常工作。 - AxeEffect
从技术上讲,你的说法是不正确的。父视图必须是(可点击) *或者* (可获得焦点 *且* 在触摸模式下可获得焦点) ;) - Martin Marconcini

18

Ken的答案可行,但有一些hacky。正如pcans在答案评论中提到的那样,可以使用dispatchTouchEvent来完成相同的事情。这个解决方案更加干净,因为它避免了使用透明的虚框架来hack XML。以下是实现该解决方案的代码:

@Override
public boolean dispatchTouchEvent(MotionEvent event) {
    EditText mEditText = findViewById(R.id.mEditText);
    if (event.getAction() == MotionEvent.ACTION_DOWN) {
        View v = getCurrentFocus();
        if (mEditText.isFocused()) {
            Rect outRect = new Rect();
            mEditText.getGlobalVisibleRect(outRect);
            if (!outRect.contains((int)event.getRawX(), (int)event.getRawY())) {
                mEditText.clearFocus();
                //
                // Hide keyboard
                //
                InputMethodManager imm = (InputMethodManager) v.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); 
                imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
            }
        }
    }
    return super.dispatchTouchEvent(event);
}

7
以下是我翻译的结果:

以下这些方法对我有效:

1.将EditText的父布局(android.support.design.widget.TextInputLayout)中添加android:clickable="true"android:focusableInTouchMode="true"属性。

<android.support.design.widget.TextInputLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:clickable="true"
    android:focusableInTouchMode="true">
<EditText
    android:id="@+id/employeeID"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:ems="10"
    android:inputType="number"
    android:hint="Employee ID"
    tools:layout_editor_absoluteX="-62dp"
    tools:layout_editor_absoluteY="16dp"
    android:layout_marginTop="42dp"
    android:layout_alignParentTop="true"
    android:layout_alignParentRight="true"
    android:layout_alignParentEnd="true"
    android:layout_marginRight="36dp"
    android:layout_marginEnd="36dp" />
    </android.support.design.widget.TextInputLayout>

2. 在Activity类中覆盖dispatchTouchEvent函数并插入hideKeyboard()函数

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            View view = getCurrentFocus();
            if (view != null && view instanceof EditText) {
                Rect r = new Rect();
                view.getGlobalVisibleRect(r);
                int rawX = (int)ev.getRawX();
                int rawY = (int)ev.getRawY();
                if (!r.contains(rawX, rawY)) {
                    view.clearFocus();
                }
            }
        }
        return super.dispatchTouchEvent(ev);
    }

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

3. 为EditText添加setOnFocusChangeListener

EmployeeId.setOnFocusChangeListener(new View.OnFocusChangeListener() {
            @Override
            public void onFocusChange(View v, boolean hasFocus) {
                if (!hasFocus) {
                    hideKeyboard(v);
                }
            }
        });

7
这是我基于zMan的代码制作的版本。如果下一个视图也是编辑文本,则它不会隐藏键盘。如果用户只是滚动屏幕,它也不会隐藏键盘。
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
    if (event.getAction() == MotionEvent.ACTION_DOWN) {
        downX = (int) event.getRawX();
    }

    if (event.getAction() == MotionEvent.ACTION_UP) {
        View v = getCurrentFocus();
        if (v instanceof EditText) {
            int x = (int) event.getRawX();
            int y = (int) event.getRawY();
            //Was it a scroll - If skip all
            if (Math.abs(downX - x) > 5) {
                return super.dispatchTouchEvent(event);
            }
            final int reducePx = 25;
            Rect outRect = new Rect();
            v.getGlobalVisibleRect(outRect);
            //Bounding box is to big, reduce it just a little bit
            outRect.inset(reducePx, reducePx);
            if (!outRect.contains(x, y)) {
                v.clearFocus();
                boolean touchTargetIsEditText = false;
                //Check if another editText has been touched
                for (View vi : v.getRootView().getTouchables()) {
                    if (vi instanceof EditText) {
                        Rect clickedViewRect = new Rect();
                        vi.getGlobalVisibleRect(clickedViewRect);
                        //Bounding box is to big, reduce it just a little bit
                        clickedViewRect.inset(reducePx, reducePx);
                        if (clickedViewRect.contains(x, y)) {
                            touchTargetIsEditText = true;
                            break;
                        }
                    }
                }
                if (!touchTargetIsEditText) {
                    InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
                    imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
                }
            }
        }
    }
    return super.dispatchTouchEvent(event);
}

非常有用,谢谢...我使用了一个修改过的版本以更好地满足自己的需求,但感谢您的指引! - drmrbrewer
downX 是如何定义的? - Sunkas
私有整型变量 downX; - Avinta

5
您可能已经找到了解决此问题的答案,但我一直在寻找如何解决此问题,仍然不能完全找到我要找的内容,所以我想在这里发布它。
我所做的是以下内容(这是非常概括的,目的是为了给您一个大致的思路,复制和粘贴所有代码将不起作用 O:D):
首先,将EditText和您程序中想要的任何其他视图包装在单个视图中。在我的情况下,我使用LinearLayout来包装所有内容。
<LinearLayout 
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/mainLinearLayout">
 <EditText
  android:id="@+id/editText"/>
 <ImageView
  android:id="@+id/imageView"/>
 <TextView
  android:id="@+id/textView"/>
 </LinearLayout>

然后在你的代码中,你需要为主LinearLayout设置一个Touch监听器。

final EditText searchEditText = (EditText) findViewById(R.id.editText);
mainLinearLayout.setOnTouchListener(new View.OnTouchListener() {

        @Override
        public boolean onTouch(View v, MotionEvent event) {
            // TODO Auto-generated method stub
            if(searchEditText.isFocused()){
                if(event.getY() >= 72){
                    //Will only enter this if the EditText already has focus
                    //And if a touch event happens outside of the EditText
                    //Which in my case is at the top of my layout
                    //and 72 pixels long
                    searchEditText.clearFocus();
                    InputMethodManager imm = (InputMethodManager) v.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
                    imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
                }
            }
            Toast.makeText(getBaseContext(), "Clicked", Toast.LENGTH_SHORT).show();
            return false;
        }
    });

我希望这能帮助一些人。或者至少能帮助他们开始解决问题。


是的,我也想到了类似的解决方案。然后我将 Android 视图系统的子集移植到 OpenGL 上的 C++ 中,并在其中构建了此功能。 - note173
这真的是一个非常好的例子。我一直在寻找像这样的代码,但我能找到的都是复杂的解决方案。我使用了X-Y坐标来更精确地定位,因为我有几个多行EditText。谢谢! - Sumitk

4

我有一个由EditText视图组成的ListView。场景要求我们在一个或多个行中编辑文本后,应单击名为“完成”的按钮。我在listView内部的EditText视图上使用了onFocusChanged,但在点击完成后,数据未保存。问题通过添加以下内容解决:

listView.clearFocus();

在"完成"按钮的onClickListener中,数据已成功保存。

谢谢。你节省了我的时间,我在recyclerview中遇到了这种情况,当按下按钮后,最后一个editText的值丢失,因为最后一个editText焦点没有改变,所以onFocusChanged没有被调用。我想找到一个解决方案,使得最后一个editText在外部失去焦点,同样是在按钮点击时...然后找到了你的解决方案。它很好地解决了我的问题! - Reyhane Farshbaf
这是我的朋友,对于ListView和列表中的多个EditText来说是最好的答案。 - R.F

4

Kotlin中的hidekeyboard()

hidekeyboard()是Kotlin的扩展函数。

fun Activity.hideKeyboard() {
    hideKeyboard(currentFocus ?: View(this))
}

在活动中添加 dispatchTouchEvent
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
    if (event.action == MotionEvent.ACTION_DOWN) {
        val v: View? = currentFocus
        if (v is EditText) {
            val outRect = Rect()
            v.getGlobalVisibleRect(outRect)
            if (!outRect.contains(event.rawX.toInt(), event.rawY.toInt())) {
                v.clearFocus()
                hideKeyboard()
            }
        }
    }
    return super.dispatchTouchEvent(event)
}

在最上层的父级中添加这些属性

android:focusableInTouchMode="true"
android:focusable="true"

如果hideKeyboard()函数基本上是空的,然后无限递归调用自身(虽然我承认至少该参数将阻止它构建,因为hideKeyboard()没有参数并且正在提供一个,从而使用户免受递归的影响),那么hideKeyboard()的意义是什么? - Kobato
这取决于不关闭键盘并且必须隐藏的设备型号,另一个解决方案是检查键盘是否可见,以便在关闭之前进行操作。 - Codelaby

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