Android軟鍵盤與佈局的協調-不同的效果與實現方案的探討

語言: CN / TW / HK

theme: smartblue highlight: agate


持續創作,加速成長!這是我參與「掘金日新計劃 · 10 月更文挑戰」的第7天,點選檢視活動詳情

前言

在之前軟鍵盤的高度的文章中,我們定義了一些相容性相對比較好的工具類。

我們是把佈局單獨提取出來,然後放在軟鍵盤上面跟隨動畫,那麼如果是佈局中的EditText,該如何適配軟鍵盤呢?

之前的文章我們提到過我們可以通過新增Flag,和替換滾動佈局等方式來適配軟鍵盤,但是這幾種方式都不是那麼完美,如果想要一些定製的效果,我們又該如何適配佈局中的 EditText或View 的軟鍵盤高度適配呢?

接下來我們看看各種不同的佈局與不同處理方式。

軟鍵盤的Flag方案

其實大家或多或少的都知道,一個Activity中當軟鍵盤彈起之後,我們的內容佈局要做怎樣的變化,是根據我們新增 windowSoftInputMode 屬性來決定的。

而我們常用的幾個 windowSoftInputMode 就是 adjustUnspecified adjustResize adjustPan adjustNoting 四種,其中更高頻的其實就是兩種 adjustResize 和 adjustPan。

兩者的區別:

image.png

在一個預設的佈局示例中,我們可以看看他們的區別:

佈局是為普通的固定佈局,頂部一個TextView,下面一個ImageView,一個EditText。

當清單檔案中設定為 android:windowSoftInputMode="adjustPan"時:

softinput_01.gif

當我們把清單檔案設定為 android:windowSoftInputMode="adjustResize"時:

softinput_02.gif

但是當我們把佈局調整為滾動佈局ScrollView之後,設定清單檔案配置為 android:windowSoftInputMode="adjustPan"時:

softinput_03.gif

這個效果就是剛剛好,所以我們通常得出一個結論:

想要EditText適配軟鍵盤,不能滾動的佈局中我們使用 adjustPan, 而在能滾動的佈局中,我們使用 adjustResize。

adjustPan 屬性為了空出軟鍵盤的位置,自動平移視窗的內容。而 adjustResize 會重新繪製佈局,如果能滾動則會滾動到對應的位置,相當的智慧。

所以老師也是教我們這麼使用的,固定搭配!那我們就只能這麼固定用了嗎?又有沒有其他別的方式呢?

約束在底部的方法

根據上面的效果圖我們知道 adjustSpan 是平移佈局,adjustResize 是重新繪製一個新的顯示區域。

那麼我們可以根據 adjustResize 這一個特性,我們把佈局固定在底部即可。 LinearLayout RelativeLayout FrameLayout ConstraintLayout 都可以做到這個效果。RelativeLayout FrameLayout ConstraintLayout 三者只需要把佈局約束在底部即可,而 LinearLayout 我們可以通過權重來實現這個效果。

如下的佈局,可以使用多種方式實現 ```xml

    <View
        android:layout_weight="1"
        android:layout_width="1dp"
        android:layout_height="0dp"/>
    <EditText
        android:id="@+id/editText"
        android:layout_gravity="center_horizontal|bottom"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentStart="true"
        android:layout_marginTop="0dp"
        android:layout_marginBottom="0dp"
        android:hint="輸入內容" />

```

預設的佈局如下:

image.png

當彈出軟鍵盤之後:

image.png

可以看到 adjustResize 之後我們的佈局為黑色框的部分了,如果是順序排列的,那麼就會出現上面那種沒有變化的效果,而我們把EditText永遠約束在佈局底部,就算Resize了,我們還是在底部,就能間接的實現軟鍵盤在鍵盤上面的效果。

image.png

那我的佈局不方便約束在底部,或者我的佈局就是順序排列的,那我就想實現頂部的圖片不動,讓EditText在軟鍵盤上面,能不能做?

手動偏移的方法

可以的,我們同時也需要設定 adjustResize 模式,在重新繪製的時候,我們動態的計算當前父容器的高度,父容器的當前位置,指定View的位置等資訊,我們就能計算當前View需要偏移的位置,手動的位移指定的佈局。

首先我們需要拿到需要適配的View和它的父佈局,為了適配,我們需要拿到底部導航欄的高度

```java public void adjustETWithSoftInput(final View anyView, final ISoftInputChanged listener) { if (anyView == null || listener == null) return;

    //根View
    final View rootView = anyView.getRootView();
    if (rootView == null) return;

    getNavigationBarHeight(anyView, new NavigationBarCallback() {
        @Override
        public void onHeight(int height, boolean hasNav) {

            SoftInputUtil.this.navigationHeight = height;

            //anyView為需要調整高度的View,理論上來說可以是任意的View
            SoftInputUtil.this.anyView = anyView;
            SoftInputUtil.this.rootView = rootView;
            SoftInputUtil.this.listener = listener;
            SoftInputUtil.this.isNavigationBarShow = hasNav;
            SoftInputUtil.this.myListener = new myListener();

            rootView.addOnLayoutChangeListener(myListener);

        }
    });

}

```

getNavigationBarHeight 方法具體的實現在我們之前的文章中有講到過。不熟悉的可以看這:Android導航欄的處理

拿到導航欄高度之後,我們通過監聽父佈局的 LayoutChangeListener 監聽,由於我們使用 adjustResize 模式,我們的佈局會重新佈局,所以每次軟鍵盤彈起和收回的時候都會回撥到這個方法,我們的邏輯判斷則寫到對應的監聽中。

```java //RootView的監聽回撥 class myListener implements View.OnLayoutChangeListener {

    @Override
    public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {

        int rootHeight = rootView.getHeight();

        Rect rect = new Rect();
        //獲取當前可見部分,預設可見部分是除了狀態列和導航欄剩下的部分
        rootView.getWindowVisibleDisplayFrame(rect);

        //還是要每次回撥的時候判斷是否有導航欄
        if (rootHeight - rect.bottom == navigationHeight) {
            //如果可見部分底部與螢幕底部剛好相差導航欄的高度,則認為有導航欄
            isNavigationBarShow = true;
        } else if (rootHeight - rect.bottom == 0) {
            //如果可見部分底部與螢幕底部平齊,說明沒有導航欄
            isNavigationBarShow = false;
        }

        //判斷軟鍵盤是否展示並計算軟鍵盤的高度
        boolean isSoftInputShow = false;
        int softInputHeight = 0;
        //如果有導航欄,則要去除導航欄的高度
        int mutableHeight = isNavigationBarShow ? navigationHeight : 0;
        if (rootHeight - mutableHeight > rect.bottom) {
            //除去導航欄高度後,可見區域仍然小於螢幕高度,則說明鍵盤彈起了
            isSoftInputShow = true;
            //鍵盤高度
            softInputHeight = rootHeight - mutableHeight - rect.bottom;
            if (SoftInputUtil.this.softInputHeight != softInputHeight) {
                softInputHeightChanged = true;
                SoftInputUtil.this.softInputHeight = softInputHeight;
            } else {
                softInputHeightChanged = false;
            }
        }

        //獲取目標View的位置座標
        int[] location = new int[2];
        anyView.getLocationOnScreen(location);

        if (isSoftInputShowing != isSoftInputShow || (isSoftInputShow && softInputHeightChanged)) {
            if (listener != null) {
                //第三個引數為該View需要調整的偏移量
                //此處的座標都是相對螢幕左上角(0,0)為基準的
                listener.onChanged(isSoftInputShow, softInputHeight, location[1]- rect.bottom  + anyView.getHeight() );
            }

            isSoftInputShowing = isSoftInputShow;
        }

    }
}

```

獲取到父佈局的高度和可見矩陣,我們就可以計算是否有導航欄和軟鍵盤的高度。獲取到當前Viewd的座標之後,拿到Y座標加上當前View的高度,減去當前可見矩陣的高度,就是我們需要的偏移量。

image.png

使用起來也很簡單。

```kotlin override fun init() {

    val etInput = findViewById<EditText>(R.id.et_input)
    etInput.bringToFront()

    softInputUtil.adjustETWithSoftInput(etInput) { isSoftInputShow, softInputHeight, viewOffset ->

        if (isSoftInputShow) {
            etInput.translationY = etInput.translationY - viewOffset
        } else {
            etInput.translationY = 0f;
        }

    }

}

override fun onDestroy() {
    super.onDestroy()

    softInputUtil.releaseETWithSoftInput()
}

```

我們手動的設定偏移 translationY 即可實現

現在我們設定 windowSoftInputMode 為 adjustResize 然後我們可以對比一下 adjustPan 的效果:

adjustPan效果:

softinput_01.gif

adjustResize + 自定義偏移效果:

softinput_05.gif

可以看到不同點就是頂部的圖片和標題欄不會往上滾動,缺點是使用起來相對麻煩一點,需要自己計算和位移。

列表中自動定位邏輯

在之前的演示中,我們設定預設的 windowSoftInputMode 或者指定為 adjustResize 的時候,在我們滾動佈局中是可以很好的支援的。那麼在列表中使用軟鍵盤會怎麼樣?

當然了我們一般不會在列表的Item中直接使用EditText,有複用問題,就算我們解決了效能也沒有那麼好,我們通常的做法是像微信朋友圈一樣,使用一個按鈕觸發一個輸入彈窗,在彈窗中使用軟鍵盤。

那麼這種輸入框佈局下面是軟鍵盤的做法,其實有幾種做法,我們之前的文章講軟鍵盤的高度的一文中我們介紹了一種佈局附著在軟鍵盤的做法,而另一種做法就是使用滾動佈局來實現了。

由於滾動佈局天然就能很好的支援軟鍵盤和EditText的聯動,所以我們直接在Dialog的佈局中使用滾動佈局即可完成指定的效果。

```xml

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <View
        android:layout_width="1dp"
        android:layout_height="0dp"
        android:layout_weight="1" />

    <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:background="#d2d2d2">

    </View>

    <LinearLayout
        android:id="@+id/dialog_layout_comment"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/white">

        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="1" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="確認" />

    </LinearLayout>


</LinearLayout>

```

Dialog中我們就能使用佈局了

```java @SuppressLint("ClickableViewAccessibility") public class ReviewDialog extends Dialog {

public ReviewDialog(Context context) {
    this(context, R.style.quick_option_dialog);
}

//兩個引數的構造方法實現具體的邏輯
public ReviewDialog(Context context, int themeResId) {
    super(context, themeResId);

    View view = LayoutInflater.from(context).inflate(R.layout.dialog_review, null);


    requestWindowFeature(Window.FEATURE_NO_TITLE);  //設定沒有標題
    //設定觸控一下整個View.讓其可取消。觸控(不是點選)對話方塊任意地方取消對話方塊
    view.setOnTouchListener((View view1,  MotionEvent motionEvent) -> {
        dismiss();
        return true;  //消費掉此次觸控事件
    });

    //View設定完成,賦值給dialog對話方塊
    super.setContentView(view);

}


/**
 * 對話方塊被建立呼叫的方法
 */
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    //設定對話方塊顯示的位置.在底部顯示
    getWindow().setGravity(Gravity.BOTTOM);
    //設定對話方塊的寬度,填充螢幕
    WindowManager wm = getWindow().getWindowManager();
    Display display = wm.getDefaultDisplay();
    int width = display.getWidth();
    //獲取對話方塊的屬性
    WindowManager.LayoutParams params = getWindow().getAttributes();
    params.width = width; //和螢幕一樣寬
    getWindow().setAttributes(params);
}

} ```

我們定義RV和Item的佈局,然後填充一些假資料,看看效果試試:

```kotlin override fun init() {

    val datas = mutableListOf<String>()
    for (i in 1..20) {
        datas.add(i.toString())
    }

    findViewById<RecyclerView>(R.id.rv_list)
        .vertical()
        .bindData(datas, R.layout.item_soft_input_demo) { holder, t, position ->
            holder.getView<TextView>(R.id.tv_review).click {
                showReviewDialog(it,position)
            }
        }

}

private fun showReviewDialog(view: View, position: Int) {
    ReviewDialog(mActivity)
        .show()
}

```

效果:

softinput_06.gif

我們點選評論,使用滾動佈局的彈窗來控制軟鍵盤與EditText,彈出彈窗之後軟鍵盤和輸入框完美的契合。當然如果你想Dialog彈出的時候自動出現軟鍵盤,那麼直接在Dialog的onCreate中給EidtText開啟軟鍵盤即可。

雖然能實現效果了,但是現在還有問題!什麼問題? 我點選評論按鈕,彈框和軟鍵盤的高度加起來把我的評論按鈕擋住了,微信朋友圈的做法是會自動滾動列表,讓評論按鈕在輸入框的上面。

我們結合之前手動偏移的方法稍微修改一下,讓RV滾動一下即可。

image.png

我們讓軟鍵盤自動彈起,並延時350毫秒獲取軟鍵盤彈出之後的高度,為什麼是350毫秒,因為我的Dialog動畫theme是300毫秒,我們讓軟鍵盤展示出來再處理就相對簡單一點。否則還需要做佈局變化的監聽相對麻煩一點。

```kotlin private fun showReviewDialog(view: View, position: Int) { val rvReviewY = getY(view) val rvReviewHeight = view.height

    val dialog = ReviewDialog(mActivity)
    dialog.show()


 view.postDelayed({
    //等待彈窗彈起自後再獲取到Y的高度,就是加上了軟鍵盤之後的高度了
    val etReviewY = getY(dialog.findViewById<LinearLayout>(R.id.dialog_layout_comment))

    val offsetY = rvReviewY - etReviewY + rvReviewHeight

    rvList.smoothScrollBy(0, offsetY)

  }, 350)

}

private fun getY(view: View): Int {
    val rect = IntArray(2)
    view.getLocationOnScreen(rect)
    return rect[1]
}

```

這樣的效果就和微信朋友圈的效果比較類似了:

softinput_07.gif

總結

很多方案都是網上現有的方案,這裡我也是做了一些歸納與整理。

本文也只是記錄了應用層的設定,如果對原始碼感興趣可以去搜索檢視 ViewRootImpl 類,在其中的 public void handleMessage(Message msg) 方法中有對應的Flag處理,其中一些重點的方法 dispatchApplyInsets performTraversals dispatchOnPreDraw scrollToRectOrFocus 等,如果大家有興趣可以自行查閱。

常用的幾種效果大致就是這些了,一般的效果我們使用 固定佈局+ adjustPan 或 滾動佈局 + adjustResize 即可實現預設的效果了。

如果想要一些特殊的效果,我們就能設定 adjustResize 之後自行實現一些一些位移和定製的效果,計算位移的一些公式都是相對固定和簡單的一些用法。

而在列表中我們可以通過View依附軟鍵盤的方式,也可以使用 SrollView+EditText 的方式來實現效果。都可以滿足需求效果是一致的。

Ok,本文的全部程式碼已經開源,想檢視效果可以點選原始碼執行測試哦。

本文的環境與裝置都是基於API30實現,由於隔離在家了沒有那麼多的裝置參與測試,如果有相容性問題歡迎大家反饋呀。

慣例,如有錯漏還請指出,如果有更好的方案也歡迎留言區交流。

如果感覺本文對你有一點點的啟發,還望你能點贊支援一下,你的支援是我最大的動力。

Ok,這一期就此完結。

「其他文章」