note173
note173

Reputation: 1875

EditText, clear focus on touch outside

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

Upvotes: 143

Views: 153272

Answers (19)

Ridcully
Ridcully

Reputation: 23655

I have combined the solution of @quealegriamasalegre with the idea to use GestureDetector to handle just clicks and not scrolls and added check so that click on other EditText keeps the keyboard on:

In Activity

private lateinit var gestureDetector: GestureDetector

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    gestureDetector = GestureDetector(this, object : SimpleOnGestureListener() {
        override fun onSingleTapUp(e: MotionEvent): Boolean {
            hideKeyboardOnTapOutsideEditText(e)
            return false
        }
    })
    ....
}

override fun dispatchTouchEvent(ev: MotionEvent): Boolean {       
    gestureDetector.onTouchEvent(ev)
    return super.dispatchTouchEvent(ev)
}

And this extension function for better reusability

fun Activity.hideKeyboardOnTapOutsideEditText(ev: MotionEvent) {
    if (ev.action != MotionEvent.ACTION_UP) return
    // only if a view is currently focused
    currentFocus?.let { focusedView ->
        // applies only to EditText views
        if (focusedView !is EditText) return
        val x = ev.rawX.toInt()
        val y = ev.rawY.toInt()
        // only if the user is clicking outside the EditText view currently in focus
        val outRect = Rect()
        focusedView.getGlobalVisibleRect(outRect)
        if (outRect.contains(x, y)) return
        // only if not another editText has been touched
        if (focusedView.rootView.touchables.filterIsInstance<EditText>    ().any { editText ->
            editText.getGlobalVisibleRect(outRect)
            outRect.contains(x, y)
        }) return
        // if all conditions apply clear focus and close keyboard
        focusedView.clearFocus()
        hideKeyboard()
    }
}

Upvotes: 1

Hadi Satrio
Hadi Satrio

Reputation: 4292

Building upon the answers here, I've created a lightweight FrameLayout that could be used to fulfill the requirement whilst keeping your codebase nice and DRY.

class FocusCleaningFrameLayout @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null
) : FrameLayout(context, attrs) {

    private val backgroundExecutor: Executor = /* ... */
    private val foregroundExecutor: Executor = /* ... */

    override fun dispatchTouchEvent(event: MotionEvent): Boolean {
        backgroundExecutor.execute {
            if (event.action != MotionEvent.ACTION_DOWN) return@execute
            val focusedView = findFocus() ?: return@execute
            if (event.happenedWithin(focusedView.visibleBounds)) return@execute
            foregroundExecutor.execute { focusedView.clearFocus() }
            hideSoftInput(focusedView)
        }
        return super.dispatchTouchEvent(event)
    }

    private fun hideSoftInput(view: View) {
        val imm = context.getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
        imm.hideSoftInputFromWindow(view.windowToken, 0)
    }

    private fun MotionEvent.happenedWithin(bounds: Rect): Boolean {
        return bounds.contains(rawX.toInt(), rawY.toInt())
    }

    private val View.visibleBounds: Rect get() {
        return Rect().apply { getGlobalVisibleRect(this) }
    }
}

The idea is that this is a very common a use-case which one might need to satisfy in multiple activities. While overriding Activity#dispatchTouchEvent() in each of such activities could work, you'll end up with repeating code.

Having this logic be encapsulated makes it easy to reuse. All you would need to do is to have FocusCleaningFrameLayout as the root layout in your activities and you'll be good to go.

<FocusCleaningFrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!-- Rest of your views goes here. -->

</FocusCleaningFrameLayout>

You might notice that FocusCleaningFrameLayout attempts to offload most of its logic onto a worker thread. Depending on your needs, this can be a bit overkill. Large view hierarchies would definitely benefit from having the traversing View#findFocus() be run off the Main thread. But you can choose to omit them should you're confident that you won't need it.

Upvotes: 0

quealegriamasalegre
quealegriamasalegre

Reputation: 3258

A small improvement to the great solutions above

This is basically a kotlin version of @zMan's solution with two improvements:

override fun dispatchTouchEvent(event: MotionEvent): Boolean {
    hideSoftInputKeyboardIfRelevant(event)
    return super.dispatchTouchEvent(event)
}

private fun hideSoftInputKeyboardIfRelevant(event: MotionEvent) {
    // not applicable for messaging user interfaces where keyboard should remain visible even after pressing the send button
    if (this is ContinuousInputInterface) return
    // hide keyboard after touch event ends (otherwise the keyboard hides and reappears quickly if the user clicks another EditText)
    if (event.action != MotionEvent.ACTION_UP) return
    //only if a view is currently focused
    currentFocus?.let { focusedView ->
        // applies only to EditText views
        if (focusedView !is EditText) return
        // only if the user is clicking outside the EditText view currently in focus
        val outRect = Rect()
        focusedView.getGlobalVisibleRect(outRect)
        if (outRect.contains(event.rawX.toInt(), event.rawY.toInt())) return
        // if all conditions apply clear focus and hide keyboard
        focusedView.clearFocus()
        val inputMethodManager = getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
        inputMethodManager.hideSoftInputFromWindow(focusedView.windowToken, 0)
    }
}

it also includes the creation of an empty interface that works like a flag to extend some of your activities with

interface ContinuousInputInterface

class SomeMessagingActivity : 
   BaseActivityThatOverridesDispatchTouchEvent, ContinuousInputInterface { 
}
  • this solution allows you to understand immediately what you are doing in dispatchTouchEvent and also manages to reduce indentations to just one.
  • it also adds a custom exclusion for special activities such as Whatsapp-Style messaging activities where having the EditText lose focus and the keyboard hide on every click outside an EditText might be undesirable. It also allows you to reimplement dispatchTouchEvent from scratch on a child Activity by simply having that activity implement ContinuousInputInterface

Upvotes: 0

Avinta
Avinta

Reputation: 768

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

Upvotes: 7

Codelaby
Codelaby

Reputation: 2873

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"

Upvotes: 4

Andrii
Andrii

Reputation: 1

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

Upvotes: 0

zMan
zMan

Reputation: 3289

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

Upvotes: 305

Adi
Adi

Reputation: 921

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

Upvotes: 7

Mike Ortiz
Mike Ortiz

Reputation: 4041

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

Upvotes: 18

chandan
chandan

Reputation: 636

Just put these properties in the top most parent.

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

Upvotes: 40

Dhruvam Gupta
Dhruvam Gupta

Reputation: 502

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.

Upvotes: 3

Victor Choy
Victor Choy

Reputation: 4246

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

Upvotes: 3

Muhannad A.Alhariri
Muhannad A.Alhariri

Reputation: 3912

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.

Upvotes: 4

suber
suber

Reputation: 393

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.

Upvotes: 37

Sabre
Sabre

Reputation: 4183

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.

Upvotes: 0

Huy Tower
Huy Tower

Reputation: 7966

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.

Upvotes: 0

Ken
Ken

Reputation: 2964

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.

Upvotes: 74

edc598
edc598

Reputation: 317

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.

Upvotes: 5

forumercio
forumercio

Reputation: 230

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

Upvotes: 1

Related Questions