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 o开发者_开发问答f it.
I've seen similar questions here, but haven't found any solution...
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 );
}
I tried all these solutions. edc598's was the closest to working, but touch events did not trigger on other View
s 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.
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.
Just put these properties in the top most parent.
android:focusableInTouchMode="true"
android:clickable="true"
android:focusable="true"
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);
}
For Me Below things Worked -
1.Adding android:clickable="true"
and android:focusableInTouchMode="true"
to the parentLayout
of EditText
i.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);
}
}
});
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);
}
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.
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.
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())));
}
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.
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"
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
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
.
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
.
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));
精彩评论