138

My layout contains ListView, SurfaceView and EditText. When I click on the EditText, it receives focus and the on-screen keyboard pops up. When I click somewhere outside of the EditText, it still has the focus (it shouldn't). I guess I could set up OnTouchListener's on the other views in layout and manually clear the EditText's focus. But seems too hackish...

I also have the same situation in the other layout - list view with different types of items, some of which have EditText's inside. They act just like I wrote above.

The task is to make EditText lose focus when user touches something outside of it.

I've seen similar questions here, but haven't found any solution...

fawaad
  • 341
  • 6
  • 12
note173
  • 1,825
  • 2
  • 14
  • 15

16 Answers16

299

Building on Ken's answer, here's the most modular copy-and-paste solution.

No XML needed.

Put it in your Activity and it'll apply to all EditTexts including those within fragments within that activity.

@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 );
}
user2297550
  • 3,142
  • 3
  • 28
  • 39
zMan
  • 3,229
  • 2
  • 15
  • 6
  • 13
    Seems fine. Still I am having one issue, All my edit text are inside a scroll view and the top edit text always have the cursor visible. Even when I click outside, the focus is lost and keyboard is gone, but the cursor is still visible in top edit text. – Vaibhav Gupta Oct 20 '15 at 12:53
  • 2
    Best Solution I came across!! Earlier I was using @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 Oct 06 '16 at 17:37
  • 1
    Great job. Btw. you need to add the code for dialog fragments as well as dialogs have their own root views. See https://stackoverflow.com/questions/16024297/is-there-an-equivalent-for-dispatchtouchevent-from-activity-in-dialog-or-dialo on how to accomplish a dispatchTouchEvent method for DialogFragments. – Nantoka Dec 06 '17 at 17:20
  • Great solution, though in my case, with multiple EditTexts in a list view, a white keyboard-sized rectangle remained on the screen of my Samsung S7 while scrolling. To fix it, I had to add `return true` to the method after hiding the keyboard. – javaxian Mar 26 '18 at 15:33
  • 6
    If the user clicks on another EditText then the keyboard closes and reopens immediately. To solve this I changed `MotionEvent.ACTION_DOWN` to `MotionEvent.ACTION_UP` on your code. Great answer btw ! – Thanasis1101 Feb 01 '20 at 02:08
  • I used this version as well but the comment from @Thanasis1101 did not work for me. I will post my working version code which will keep the keyboard open if the next view is also an edit text – Avinta Apr 18 '20 at 14:06
  • 1
    very neat... yet having multiple EditText brings flickering keyboard, @Thanasis1101 answer didn't worked for me – Aelaf Sep 09 '20 at 08:20
  • Still works like a charm after all these years. Thanks. – dazed Dec 03 '21 at 16:57
74

I tried all these solutions. edc598's was the closest to working, but touch events did not trigger on other Views contained in the layout. In case anyone needs this behavior, this is what I ended up doing:

I created an (invisible) FrameLayout called touchInterceptor as the last View in the layout so that it overlays everything (edit: you also have to use a RelativeLayout as the parent layout and give the touchInterceptor fill_parent attributes). Then I used it to intercept touches and determine if the touch was on top of the EditText or not:

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;
    }
});

Return false to let the touch handling fall through.

It's hacky, but it's the only thing that worked for me.

Ken
  • 2,918
  • 1
  • 25
  • 31
  • 23
    You can do the same thing without adding an additional layout by overriding dispatchTouchEvent(MotionEvent ev) in your activity. – pcans Mar 28 '13 at 15:12
  • thanks for the clearFocus() hint, it also worked for me on SearchView – Jimmy Ilenloa Feb 27 '14 at 12:34
  • 1
    I would like to hint to @zMan's answer below. It is based on this, but doesn't need a view http://stackoverflow.com/a/28939113/969016 – Boy Jun 20 '16 at 12:33
  • Also, if anyone encounters with a situation that keyboard not hiding but focus clearing, first invoke `hideSoftInputFromWindow()` then clear focus – Ahmet Gokdayi Nov 05 '19 at 12:53
  • But it is must to add android:focusableInTouchMode="true" to the top most parent. – Gk Mohammad Emon Nov 04 '20 at 18:39
37

Just put these properties in the top most parent.

android:focusableInTouchMode="true"
android:clickable="true"
android:focusable="true" 
chandan
  • 506
  • 6
  • 5
  • FYI : Though it works for most of the cases, its not working for layout's some inner elements. – SV Madhava Reddy May 08 '19 at 19:19
  • Without this, the solution provided as best answer not working. I'm using fragments so I need to put `android:focusableInTouchMode="true"` in FrameLayout. I'm using sdk version 29 with com.google.android.material:material library. – Falcon May 11 '22 at 10:41
  • I'm pretty sure this solution will mess up the accessibility of your app. This causes TalkBack to think the top most parent is an interactable element and will read it out to the user as such – Shane Neuville Sep 04 '22 at 17:20
36

For the EditText's parent view, let the following 3 attributes be "true":
clickable, focusable, focusableInTouchMode.

If a view want to receive focus, it must satisfy these 3 conditions.

See android.view :

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

Hope it helps.

OlivierH
  • 3,875
  • 1
  • 19
  • 32
suber
  • 383
  • 3
  • 8
  • Totally right: 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 Sep 19 '14 at 05:19
  • 2
    In order to clear focus the other focusable view must be parent of the focused view. It won't work if the view to focus is on another hierarchy. – AxeEffect Sep 19 '14 at 05:20
  • Technically, your statement is incorrect. The parent view has to be `(clickable) *OR* (focusable *AND* focusableInTouchMode)` ;) – Martin Marconcini May 18 '18 at 22:06
18

Ken's answer works, but it is hacky. As pcans alludes to in the answer's comment, the same thing could be done with dispatchTouchEvent. This solution is cleaner as it avoids having to hack the XML with a transparent, dummy FrameLayout. Here is what that looks like:

@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);
}
Michael Saffold
  • 561
  • 4
  • 5
Mike Ortiz
  • 4,031
  • 4
  • 27
  • 54
7

For Me Below things Worked -

1.Adding android:clickable="true" and android:focusableInTouchMode="true" to the parentLayout of EditTexti.e android.support.design.widget.TextInputLayout

<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.Overriding dispatchTouchEvent in Activity class and inserting hideKeyboard() function

@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.Adding setOnFocusChangeListener for EditText

EmployeeId.setOnFocusChangeListener(new View.OnFocusChangeListener() {
            @Override
            public void onFocusChange(View v, boolean hasFocus) {
                if (!hasFocus) {
                    hideKeyboard(v);
                }
            }
        });
Adi
  • 903
  • 2
  • 15
  • 25
7

This is my Version based on zMan's code. It will not hide the keyboard if the next view also is an edit text. It will also not hide the keyboard if the user just scrolls the screen.

@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);
}
Avinta
  • 678
  • 1
  • 9
  • 26
5

You've probably found the answer to this problem already but I've been looking on how to solve this and still can't really find exactly what I was looking for so I figured I'd post it here.

What I did was the following (this is very generalized, purpose is to give you an idea of how to proceed, copying and pasting all the code will not work O:D ):

First have the EditText and any other views you want in your program wrapped by a single view. In my case I used a LinearLayout to wrap everything.

<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>

Then in your code you have to set a Touch Listener to your main LinearLayout.

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;
        }
    });

I hope this helps some people. Or at least helps them start solving their problem.

edc598
  • 317
  • 3
  • 11
  • Yes, I've come to similar solution too. Then I ported a subset of android view system to c++ on opengl and built this feature in it) – note173 Dec 23 '11 at 14:56
  • This is really a very neat example. I was looking for a code like this but all I could find was complex solutions. I used both the X-Y coordinate to be more precise as I had few multi-line EditText as well. Thanks! – Sumitk Apr 13 '12 at 08:32
4

I have a ListView comprised of EditText views. The scenario says that after editing text in one or more row(s) we should click on a button called "finish". I used onFocusChanged on the EditText view inside of listView but after clicking on finish the data is not being saved. The problem was solved by adding

listView.clearFocus();

inside the onClickListener for the "finish" button and the data was saved successfully.

fawaad
  • 341
  • 6
  • 12
Muhannad A.Alhariri
  • 3,702
  • 4
  • 30
  • 46
  • Thanks. You saved my time, I had this scenario in recyclerview and was losing last editText value after click on button, becuase last editText focus didn't change and so onFocusChanged not called. I'd like to find a solution that make last editText would lose focus when outside it, same button click...and find your solution. It,s worked great! – Reyhane Farshbaf Apr 10 '20 at 19:24
  • this is my friend is the best answer for listview and for multiple edittext inside the list – R.F Apr 30 '21 at 13:58
3

I really think it's a more robust way to use getLocationOnScreen than getGlobalVisibleRect. Because I meet a problem. There is a Listview which contain some Edittext and and set ajustpan in the activity. I find getGlobalVisibleRect return a value that looks like including the scrollY in it, but the event.getRawY is always by the screen. The below code works well.

public boolean dispatchTouchEvent(MotionEvent event) {
    if (event.getAction() == MotionEvent.ACTION_DOWN) {
        View v = getCurrentFocus();
        if ( v instanceof EditText) {
            if (!isPointInsideView(event.getRawX(), event.getRawY(), v)) {
                Log.i(TAG, "!isPointInsideView");

                Log.i(TAG, "dispatchTouchEvent clearFocus");
                v.clearFocus();
                InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
                imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
            }
        }
    }
    return super.dispatchTouchEvent( event );
}

/**
 * Determines if given points are inside view
 * @param x - x coordinate of point
 * @param y - y coordinate of point
 * @param view - view object to compare
 * @return true if the points are within view bounds, false otherwise
 */
private boolean isPointInsideView(float x, float y, View view) {
    int location[] = new int[2];
    view.getLocationOnScreen(location);
    int viewX = location[0];
    int viewY = location[1];

    Log.i(TAG, "location x: " + location[0] + ", y: " + location[1]);

    Log.i(TAG, "location xWidth: " + (viewX + view.getWidth()) + ", yHeight: " + (viewY + view.getHeight()));

    // point is inside view bounds
    return ((x > viewX && x < (viewX + view.getWidth())) &&
            (y > viewY && y < (viewY + view.getHeight())));
}
Victor Choy
  • 4,006
  • 28
  • 35
  • thank you so much! It worked for all layout scenarios like RecyclerView, etc. etc. I tried other solution but this one fits all! :) Great job! – Woppi Nov 23 '17 at 08:35
3

In Kotlin

hidekeyboard() is a Kotlin Extension

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

In activity add 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)
}

Add these properties in the top most parent

android:focusableInTouchMode="true"
android:focusable="true"
Codelaby
  • 2,604
  • 1
  • 25
  • 25
  • What's the point of `hideKeyboard()` if the function is basically empty, then calls itself recursively forever (although I'll admit at the very least that argument will stop it from building, since `hideKeyboard()` has no parameters and one is being supplied, thus saving the user from the recursion)? – Kobato Mar 21 '21 at 01:11
  • It depends on the model of the device that does not close the keyboard and must be hidden, another solution would be to check if the keyboard is visible to close once – Codelaby Mar 23 '21 at 08:27
2

Simply define two properties of parent of that EditText as :

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

So when user will touch outside of EditText area, focus will be removed because focus will be transferred to parent view.

Dhruvam Gupta
  • 482
  • 5
  • 10
1

To lose the focus when other view is touched , both views should be set as view.focusableInTouchMode(true).

But it seems that use focuses in touch mode are not recommended. Please take a look here: http://android-developers.blogspot.com/2008/12/touch-mode.html

forumercio
  • 230
  • 2
  • 13
  • Yes, I have read this, but it's not exactly what I want. I don't need the other view to become focused. Although I don't see any alternative way right now... Maybe, somehow make the root view to capture focus when user clicks on non-focusable child? – note173 Jan 28 '11 at 14:13
  • As long as I know , the focus cannot be managed by a class, it is managed by android...no ideas :( – forumercio Jan 28 '11 at 15:33
0

The best way is use the default method clearFocus()

You know how to solve codes in onTouchListener right?

Just call EditText.clearFocus(). It will clear focus in last EditText.

Huy Tower
  • 7,769
  • 16
  • 61
  • 86
0

As @pcans suggested you can do this overriding dispatchTouchEvent(MotionEvent event) in your activity.

Here we get the touch coordinates and comparing them to view bounds. If touch is performed outside of a view then do something.

@Override
public boolean dispatchTouchEvent(MotionEvent event) {
    if (event.getAction() == MotionEvent.ACTION_DOWN) {
        View yourView = (View) findViewById(R.id.view_id);
        if (yourView != null && yourView.getVisibility() == View.VISIBLE) {
            // touch coordinates
            int touchX = (int) event.getX();
            int touchY = (int) event.getY();
            // get your view coordinates
            final int[] viewLocation = new int[2];
            yourView.getLocationOnScreen(viewLocation);

            // The left coordinate of the view
            int viewX1 = viewLocation[0];
            // The right coordinate of the view
            int viewX2 = viewLocation[0] + yourView.getWidth();
            // The top coordinate of the view
            int viewY1 = viewLocation[1];
            // The bottom coordinate of the view
            int viewY2 = viewLocation[1] + yourView.getHeight();

            if (!((touchX >= viewX1 && touchX <= viewX2) && (touchY >= viewY1 && touchY <= viewY2))) {

                Do what you want...

                // If you don't want allow touch outside (for example, only hide keyboard or dismiss popup) 
                return false;
            }
        }
    }
    return super.dispatchTouchEvent(event);
}

Also it's not necessary to check view existance and visibility if your activity's layout doesn't change during runtime (e.g. you don't add fragments or replace/remove views from the layout). But if you want to close (or do something similiar) custom context menu (like in the Google Play Store when using overflow menu of the item) it's necessary to check view existance. Otherwise you will get a NullPointerException.

Sabre
  • 4,131
  • 5
  • 36
  • 57
0

This simple snippet of code does what you want

GestureDetector gestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() {
            @Override
            public boolean onSingleTapConfirmed(MotionEvent e) {
                KeyboardUtil.hideKeyboard(getActivity());
                return true;
            }
        });
mScrollView.setOnTouchListener((v, e) -> gestureDetector.onTouchEvent(e));
Andrii
  • 1