美文网首页小技巧AndroidAndroid
Android 软键盘隐藏寻找最优解

Android 软键盘隐藏寻找最优解

作者: MeloDev | 来源:发表于2016-11-25 21:59 被阅读3805次

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

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

    相关文章

      网友评论

      • _孑孓_:MIUI系统试了几种api 来隐藏软键盘都不起作用,判断软键盘是否显示的方法也失效
      • Li7tleMK:第三个解决方案,如果点击登录按钮,登录失败的情况:软键盘也隐藏了。这样体验不是很好,应该不让软键盘隐藏。
        MeloDev:我个人觉得登陆失败隐藏软键盘也没问题,毕竟导致登录失败的原因不一定是输入错了
      • 勿wang:楼主你好,我用你第三种方式 , 在键盘显示下点击ListView只隐藏键盘 , 但条目点击事件没有反应 , 需要再次点击才有响应是怎么回事?
        MeloDev:debug 看一下就行了吧,看看事件怎么传递的
      • 0d6eb4d5b9b6:https://github.com/qibin0506/AutoHideIME 点击其他区域 隐藏软键盘 比较优雅的方案 分享给你
        Yang_Bob:你这个我试了一下,正常是没问题的,在一种情况下无效:如果一进入页面,我设置软键盘自动弹出,这时候点击其他位置软键盘不会消失,要手动把软键盘按下后之后按其他位置就有用了。
      • fcbf553f4b03:楼主,有解决过横屏模式下监听软键盘隐藏的问题吗?用底部区域判断的方式,不会生效 :flushed:
        MeloDev:@慕子河_Hero 横屏点击EditText 一般都会进入一个新的界面输入
      • 该用户已冬:题主你好,最后判断键盘是否隐藏的地方为什么要和100比较,不应该是和0比较吗
        MeloDev: @该用户已冬 100是自己规定键盘像素的高度。
      • 咚咚淌淌:正好也要用到,多谢分享
        MeloDev:@咚咚淌淌 :wink:

      本文标题:Android 软键盘隐藏寻找最优解

      本文链接:https://www.haomeiwen.com/subject/xuuwpttx.html