Android 软键盘隐藏寻找最优解

96
MeloDev
2016.11.25 21:59* 字数 2068

Android 软键盘隐藏寻找最优解

本文原创,转载请注明出处。
欢迎关注我的 简书 ,关注我的专题 Android Class 我会长期坚持为大家收录简书上高质量的 Android 相关博文。

写在前面:
最近我自己的开发任务接近尾声,提交测试之后收到了一个 bug,这个 bug 描述起来是这个样子的:

希望当点击外部空白区域软键盘隐藏的时候,EditText 的光标也消失。

当我看到这个 bug 的时候,心里想,额...应该不难吧,隐藏软键盘大家都会,那当我隐藏软键盘的时候,让 EditText 的 Cursor 消失就不好了?
事实上解决这个问题确实不难,但是作为一个稍微有点追(jiao)求(qing)的程序员,其实解决这个问题,还是经历了一些思考过程的,所以我把它整理出来,分享给大家。

先来看看这个 bug 的描述:当软键盘隐藏,光标消失。

测试的这段描述直接对我这种心思单纯的程序猿造成了误导,因为它直接把我的思路引到了光标的处理上:

先不说软键盘了,直接看看处理 cursor 是什么效果:

et1 隐藏光标
et2 不作处理

这个 Demo 项目我目前有两个 EditText et1,et2,还有一个不做任何处理的 button,此时我仅仅给 et1 隐藏光标 cursor,调用 et1.setCursorVisible(false),可以看到上图的效果,et1 的光标消失了。

head da

是啊通常我们项目里面的 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 设置 clickablefocusableInTouchMode 属性的时候,程序直接崩溃了,去 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 属性进行动态的管理,这个属性明显影响到了 clickablefocusableInTouchMode 属性,这个时候怎么办呢?看起来我只能对这种场景进行特殊处理了:

当我点击这个 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 的研究,如果有更好的办法,欢迎告知哦~

祝大家周末愉快,天冷添衣服。

Android黑板报
Web note ad 1