Android 软键盘隐藏寻找最优解
本文原创,转载请注明出处。
欢迎关注我的 简书 ,关注我的专题 Android Class 我会长期坚持为大家收录简书上高质量的 Android 相关博文。
写在前面:
最近我自己的开发任务接近尾声,提交测试之后收到了一个 bug,这个 bug 描述起来是这个样子的:
希望当点击外部软键盘隐藏的时候,EditText 的光标也消失。
当我看到这个 bug 的时候,心里想,额…应该不难吧,隐藏软键盘大家都会,那当我隐藏软键盘的时候,让 EditText 的 Cursor 消失就不好了?
事实上解决这个问题确实不难,但是作为一个稍微有点追(jiao)求(qing)的程序员,其实解决这个问题,还是经历了一些思考过程的,所以我把它整理出来,分享给大家。
先来看看这个 bug 的描述:当软键盘隐藏,光标消失。
测试的这段描述直接对我这种心思单纯的程序猿造成了误导,因为它直接把我的思路引到了光标的处理上:
先不说软键盘了,直接看看处理 cursor 是什么效果:
这个 Demo 项目我目前有两个 EditText et1,et2,还有一个不做任何处理的 button,此时我仅仅给 et1 隐藏光标 cursor,调用 et1.setCursorVisible(false)
,可以看到上图的效果,et1 的光标消失了。
是啊通常我们项目里面的 EditText 只有一个光标,那光标是消失了,万一底下有那条线呢?不管了?
不要说再隐藏下面那条线就 ok 了,这样一来就太复杂了,说明我们思考的出发点有问题。好吧我们试图将思路拉回到正轨。
仔细想想,EditText 有焦点的时候,光标量,线也亮。所以我从 EditText 的 focus 入手考虑,有焦点的时候弹出软键盘,没焦点的时候,隐藏软键盘。
我尝试了 EditText 的 clearFocus 和 其他 View requestFocus 属性来达到焦点变换的目的使 EditText 失去焦点从而让光标消失,但是这俩种办法都没有什么用,同样,我给其他 View 设置 onClickListener 同样没有达到我想要的效果。不过最终有两个属性帮助我解决了这个问题。请继续看:
et1.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (!hasFocus) {
InputMethodManager im = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
im.hideSoftInputFromWindow(v.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
}
}
});
我给我的 EditText 加了如上代码,点击 EditText 弹出软键盘,然后点击了 EditText 之外的空白区域,没反应。再点击一下 Button,软键盘还是没有收起。
(没有收起来就对了)
因为无论是界面中的空白区域,还是 button 它们都没能力去抢夺走 EditText 的焦点,这个时候我给界面的根布局设置两个属性达到了目的:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/content_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true"
android:focusableInTouchMode="true"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context="com.blog.melo.buzzerbeater.MainActivity"
tools:showIn="@layout/activity_main">
<EditText
android:id="@+id/et1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="et1" />
<EditText
android:id="@+id/et2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="et2" />
android:clickable="true"
android:focusableInTouchMode="true"
没错就是这两个属性,无论是设置给根布局,还是 button,都能做到将焦点获取,并隐藏软键盘的效果。到目前为止,我们的 bug 算是解决了。
另外多说一个我遇到的坑。当我的编译版本为 23.0.0 的时候,我给最外层的 CoordinatorLayout 设置 clickable
和 focusableInTouchMode
属性的时候,程序直接崩溃了,去 SO 上搜了搜,换了编译版本为 23.0.4 之后,崩溃解决了,但是 CoordinatorLayout 依然无法获取焦点,我退而求其次,给我的 content_main 布局设置属性,此时生效。为了让我点击 Toolbar 之后,软键盘也消失,我又给 Toobar 的布局设置了这俩属性,终于达到了我要的效果。(非常不优雅的解决办法)
继续我们的寻找最优解之路,下面来看看第二个方法:
public void setupUI(View view) {
if (!(view instanceof EditText)) {
view.setOnTouchListener(new View.OnTouchListener() {
public boolean onTouch(View v, MotionEvent event) {
hideSoftKeyboard(MainActivity.this);
return false;
}
});
}
if (view instanceof ViewGroup) {
for (int i = 0; i < ((ViewGroup) view).getChildCount(); i++) {
View innerView = ((ViewGroup) view).getChildAt(i);
setupUI(innerView);
}
}
}
public static void hideSoftKeyboard(Activity activity) {
InputMethodManager inputMethodManager = (InputMethodManager) activity.getSystemService(Activity.INPUT_METHOD_SERVICE);
inputMethodManager.hideSoftInputFromWindow(activity.getCurrentFocus().getWindowToken(), 0);
}
新增两个方法,给整个 View 树中所有的 View 设置 onTouchListener ,然后我们把 RootView 传进去:
LinearLayout contentMain = (LinearLayout) findViewById(R.id.content_main);
setupUI(contentMain);
先来说说这个方法的问题,我们给界面中所有的 View 设置的触摸监听,当我触摸的不是 EditText 的时候,把软键盘隐藏。如果我没有给其它 view 设置android:clickable="true"
android:focusableInTouchMode="true"
属性,那么焦点依然是在 EditText 上的,光标自然也不会消失了。
(在魅族手机上测试光标居然消失了…原因不得而知,我突然间觉得第一次国产的 rom 帮了我优化,但是 nexus 上是不行的,总之还是需要我想办法去处理。)
既然有了第二种办法,回过头来看看第一种方法,第一种解决方法的问题在哪里呢?相信你也能感知到,如果我的界面复杂,难道我要给每一个 View 设置可点击的属性来达到目的吗?而且我需要给每个 EditText 都设置 onFocusChangeListener,无疑会增加代码量,让我们的代码可读性变差,并且极有可能出错。
前两种方法结合起来使用,确实可以解决大部分问题出现的场景了。我相信如果你对目前这解决方案心存不满的理由一定是:我需要对每个 EditText 都处理,或者对每个根布局都进行处理。这显然不够合理,所以来看下面这个方法。
创建一个 BaseActivity,完整代码如下:
public class BaseActivity extends AppCompatActivity {
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
// 获得当前得到焦点的View,一般情况下就是EditText(特殊情况就是轨迹求或者实体案件会移动焦点)
View v = getCurrentFocus();
if (isShouldHideInput(v, ev)) {
hideSoftInput(v.getWindowToken());
}
}
return super.dispatchTouchEvent(ev);
}
/**
* 根据EditText所在坐标和用户点击的坐标相对比,来判断是否隐藏键盘,因为当用户点击EditText时没必要隐藏
*
* @param v
* @param event
* @return
*/
private boolean isShouldHideInput(View v, MotionEvent event) {
if (v != null && (v instanceof EditText)) {
int[] l = {0, 0};
v.getLocationInWindow(l);
int left = l[0], top = l[1], bottom = top + v.getHeight(), right = left
+ v.getWidth();
if (event.getX() > left && event.getX() < right && event.getY() > top && event.getY() < bottom) {
// 点击EditText的事件,忽略它。
return false;
} else {
return true;
}
}
// 如果焦点不是EditText则忽略,这个发生在视图刚绘制完,第一个焦点不在EditView上,和用户用轨迹球选择其他的焦点
return false;
}
/**
* 多种隐藏软件盘方法的其中一种
*
* @param token
*/
private void hideSoftInput(IBinder token) {
if (token != null) {
InputMethodManager im = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
im.hideSoftInputFromWindow(token, InputMethodManager.HIDE_NOT_ALWAYS);
}
}
}
目前的第三个解决方案是在 Activity 的 dispatchTouchEvent 方法中进行一系列判断,此刻我点击界面中的任何非 EditText 部分,软键盘都会收起来,并且我不需要在具体的对每一个 EditText 进行处理。
研究到这里心情好了很多,理清思路,目前我们还差最后一步了,目前实现了软键盘的隐藏,只要再把焦点给其他 View,EditText 的光标自然就消失了。相信你肯定没忘记,此刻需要给 View 设置 android:clickable="true"
android:focusableInTouchMode="true"
属性
目前这种情况足够解决大部分问题,而我确实遇到了一个无法解决的。因为我需要对一个 TextView 的 enable
属性进行动态的管理,这个属性明显影响到了 clickable
和 focusableInTouchMode
属性,这个时候怎么办呢?看起来我只能对这种场景进行特殊处理了:
当我点击这个 TextView 的时候,我使用 et.setFocusable(false)
,移除它的焦点来消除 EditText 的光标,然后:
et.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
et.setFocusableInTouchMode(true);
return false;
}
});
让 EditText 在触摸事件中,再次获得焦点。
OK,研究到了这里的解决方案基本上我可以接受了。如果有优雅的解决办法,欢迎来骚扰我~
有些朋友说,我想监听系统软键盘的事件,通过它的弹出或者收起来做某些我的需求,可是系统并没有提供出来相应的办法,应该怎么解决?
这里推荐一个网上我认为是最好的方案:
/**
* 监听软键盘事件
*
* @param rootView
* @return
*/
private boolean isKeyboardShown(View rootView) {
final int softKeyboardHeight = 100;
Rect r = new Rect();
rootView.getWindowVisibleDisplayFrame(r);
DisplayMetrics dm = rootView.getResources().getDisplayMetrics();
int heightDiff = rootView.getBottom() - r.bottom;
return heightDiff > softKeyboardHeight * dm.density;
}
其原理是通过监听可见根布局的尺寸大小,来判断是否认为系统弹出了软键盘。
重写根布局的 View ,在 onMeasure 中使用这个方法。
public class CommonLinearLayout extends LinearLayout {
public CommonLinearLayout(Context context) {
this(context, null);
}
public CommonLinearLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CommonLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (isKeyboardShown(this)) {
Log.e("CommonLinearLayout","show");
}else {
Log.e("CommonLinearLayout","hide");
}
}
/**
* 监听软键盘事件
*
* @param rootView
* @return
*/
private boolean isKeyboardShown(View rootView) {
final int softKeyboardHeight = 100;
Rect r = new Rect();
rootView.getWindowVisibleDisplayFrame(r);
DisplayMetrics dm = rootView.getResources().getDisplayMetrics();
int heightDiff = rootView.getBottom() - r.bottom;
return heightDiff > softKeyboardHeight * dm.density;
}
}
测试结果:
可以看到系统正确判断了软键盘的弹起和隐藏。可以根据它来做你想要的操作。
长舒一口气,本文到这里也要结束了,这就是一次我对软键盘和 EditText 的研究,如果有更好的办法,欢迎告知哦~
祝大家周末愉快,天冷添衣服。