1. 程式人生 > >Android 點選外部軟鍵盤隱藏尋找最優解

Android 點選外部軟鍵盤隱藏尋找最優解

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

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