博客 / 詳情

返回

Android 監聽軟鍵盤的高度並解決其覆蓋輸入框的問題

1、前言
在某些項目中,我們常常需要自定義一個輸入框,軟鍵盤彈出時就把輸入框頂上去,關閉時輸入框再回到原位(比如下方的效果圖,實際上各種 App 中的聊天界面和發佈評論的界面大體都是這樣)。在這個過程中,除了輸入框以外的其他界面的元素不受影響,比如效果圖中的背景圖片不會上移也不會被壓縮。但在實際使用中發現軟鍵盤在彈出時常常把輸入框蓋住,導致輸入框顯示不完全。有什麼方法可以解決呢?

圖片

2、思路分析
2.1 獲取軟鍵盤的高度
網上常見的思路是這樣的:在輸入框的下面放置一個 View​,當軟鍵盤彈出時,獲取軟鍵盤高度,然後在代碼中動態將該 View​ 的高度設置成跟軟鍵盤的一樣,這樣輸入框就被它頂上去了。從視覺上來看,就像是被軟鍵盤頂上去一樣。
這個思路的難點在於準確獲取軟鍵盤的動態高度。Android 系統沒有提供直接獲取軟鍵盤高度的 api,好在我們可以曲線救國:軟鍵盤的高度其實就是屏幕高度減去軟鍵盤上方的可見區域(即沒有被軟鍵盤擋住的區域)高度,也就是:

軟鍵盤高度 = 屏幕高度 - 可見區域高度

此外,還需要考慮狀態欄和虛擬導航欄高度,所以我們可以得出以下的計算公式:

軟鍵盤高度 = 屏幕高度 - 可見區域高度 - 頂部狀態欄高度 - 底部導航欄高度

不過有兩點需要注意:

Activity 為全屏時是沒有狀態欄的,不必扣除高度;
橫屏時虛擬狀態欄是在側邊的,這時也不必扣除它的高度了。

最後我們的公式可以修正為:

軟鍵盤高度 = 屏幕高度 - 可見區域高度 - 頂部狀態欄高度(非全屏時) - 底部導航欄高度(豎屏時)

這個公式中的屏幕高度、狀態欄高度和導航欄高度都可以通過 Android 的 api 獲取,所以,現在問題的難點轉換成了準確獲取可見區域的動態高度。
2.2 獲取可見區域高度
準確獲取可見區域的動態高度,何為準確,何為動態呢?要想準確,我們必須要準確獲取可見區域的對象,要想動態,那必須監聽可見區域的高度變化,也即是:

獲取可見區域(對應準確);
監聽可見區域的高度變化(對應動態)。

首先來看第一步,View​ 類中為我們提供了一個方法 getWindowVisibleDisplayFrame()​,它可以獲取某個 View​ 所在窗口(Window​)的可見區域(注意:是窗口的可見區域,不是 View​ 的可見區域!)。它需要傳入一個 Rect​ 對象,從 Rect​ 對象中,我們就可以獲取到可見區域的信息,比如可見區域頂部距離父佈局頂部的距離 top​ 和可見區域底部部距離父佈局頂部的距離 bottom​,兩者一相減就是我們需要的可見區域高度了。
那麼用哪一個 View​ 來獲取可見區域呢?當前 Activity​ 或者 Fragment​ 上面的佈局或者控件嗎?答案是不行的。因為 Activity​(或 Fragment​)跟軟鍵盤是位於同一個窗口的,也就是説,軟鍵盤也在這個窗口的可見區域內,無論軟鍵盤彈出還是關閉,可見區域的大小都不會變化!
既然如此,那麼我們就需要另外一個窗口了。有沒有辦法創建一個不屬於軟鍵盤所在窗口的 View​ 呢?當然可以,Dialog​ 和 PopupWindow​ 就可以辦到。我們需要這個 View​ 一直存在,便於監聽,所以 PopupWindow​ 無疑是最合適的。
第一步解決後,接下來就是監聽可見區域的變化了這個比較簡單,可以通過繼承接口 ViewTreeObserver.OnGlobalLayoutListener​ 來,在 onGlobalLayout()​ 中監聽來實現。
3、代碼實踐
思路已經捋清楚了,現在是代碼時間。創建一個 KeyboardStatusWatcher​ 類,繼承於 PopupWindow​ 和 ViewTreeObserver.OnGlobalLayoutListener​ 接口:
class KeyboardStatusWatcher(

private val activity: FragmentActivity,
private val lifecycleOwner: LifecycleOwner,
private val listener: (isKeyboardShowed: Boolean, keyboardHeight: Int) -> Unit

) : PopupWindow(activity), ViewTreeObserver.OnGlobalLayoutListener {

private val rootView by lazy { activity.window.decorView.rootView }

private val TAG = "Keyboard-Tag"

/**
 * 可見區域高度
 */
private var visibleHeight = 0

/**
 * 軟鍵盤是否顯示
 */
var isKeyboardShowed = false
    private set

/**
 * 最近一次彈出的軟鍵盤高度
 */
var keyboardHeight = 0
    private set

/**
 * PopupWindow 佈局
 */
private val popupView by lazy {
    FrameLayout(activity).also {
        it.layoutParams = FrameLayout.LayoutParams(
            ViewGroup.LayoutParams.WRAP_CONTENT,
            ViewGroup.LayoutParams.MATCH_PARENT
        )
        //監聽佈局大小變化
        it.viewTreeObserver.addOnGlobalLayoutListener(this)
    }
}

init {
    //初始化 PopupWindow
    contentView = popupView
    //軟鍵盤彈出時,PopupWindow 要調整大小
    softInputMode =
        WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE or
            WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE
    inputMethodMode = INPUT_METHOD_NEEDED
    //寬度設為0,避免遮擋界面
    width = 0
    height = ViewGroup.LayoutParams.MATCH_PARENT
    setBackgroundDrawable(ColorDrawable(0))
    rootView.post { showAtLocation(rootView, Gravity.NO_GRAVITY, 0, 0) }

    //activity 銷燬時或者 Fragment onDestroyView 時必須關閉 popupWindow ,避免內存泄漏
    lifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
        override fun onDestroy(owner: LifecycleOwner) {
            super.onDestroy(owner)
            dismiss()
        }
    })
}

/**
 * 監聽佈局大小變化
 */
override fun onGlobalLayout() {
    val rect = Rect()
    //獲取當前可見區域
    popupView.getWindowVisibleDisplayFrame(rect)
    if (visibleHeight == (rect.bottom - rect.top)) {
        //可見區域高度不變時不必執行下面代碼,避免重複監聽
        return
    } else {
        visibleHeight = (rect.bottom - rect.top)
    }
    //粗略計算高度的變化值,後面會根據狀態欄和導航欄修正
    val heightDiff = rootView.height - visibleHeight
    //這裏取了一個大概值,當窗口高度變化值超過屏幕的 1/3 時,視為軟鍵盤彈出
    if (heightDiff > activity.screenHeight / 3) {
        isKeyboardShowed = true
        //非全屏時減去狀態欄高度
        keyboardHeight =
            if (activity.isFullScreen) heightDiff else heightDiff - activity.statusBarHeight
        //導航欄顯示時減去其高度,但橫屏時導航欄在側邊,故不必扣除高度
        if (activity.hasNavBar && activity.isNavBarShowed && activity.isPortrait) {
            keyboardHeight -= activity.navBarHeight
        }
    } else {
        //軟鍵盤隱藏時鍵盤高度為0
        isKeyboardShowed = false
        keyboardHeight = 0
    }
    listener.invoke(isKeyboardShowed, keyboardHeight)
}

}

代碼都是遵循前面的思路分析編寫的,註釋也比較詳細,就不過多分析了。只要關注一下 PopupWindow​ 存在時軟鍵盤的交互。PopupWindow​ 與軟鍵盤分屬於不同的窗口,軟鍵盤彈出時,默認會被 PopupWindow​ 覆蓋的(你可以通過修改上面的代碼,給 PopupWindow​ 設置顏色且寬度不為 0 來驗證),這樣 PopupWindow​ 的高度不發生變化,就無法達到監聽的目的。所以我們需要設置 softInputMode​ 和 inputMethodMode​ 兩個屬性,讓 PopupWindow​ 的高度隨着軟鍵盤的彈出和關閉而調整。
然後簡單看看 MainActivity 佈局:
<androidx.constraintlayout.widget.ConstraintLayout
    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/clRoot"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/watermelon"
tools:context=".MainActivity">

<View
    android:id="@+id/vKeyboardBg"
    android:layout_width="match_parent"
    android:layout_height="60dp"
    android:background="@android:color/white"
    app:layout_constraintBottom_toBottomOf="parent" />

<androidx.appcompat.widget.AppCompatEditText
    android:imeOptions="flagNoExtractUi"
    android:id="@+id/editText"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_marginHorizontal="15dp"
    android:layout_marginVertical="8dp"
    android:background="@drawable/shape_edit_bg"
    android:hint="請輸入"
    android:paddingHorizontal="10dp"
    app:layout_constraintBottom_toBottomOf="@id/vEditBg"
    app:layout_constraintTop_toTopOf="@id/vEditBg" />

</androidx.constraintlayout.widget.ConstraintLayout>

注意:這裏 EditText​ 要加上 android:imeOptions="flagNoExtractUi"​ 屬性,不然橫屏時樣式發生會變化。
還有,別忘了在清單文件中給 Activity 加上 android:windowSoftInputMode="adjustNothing|stateHidden"​,否則軟鍵盤彈出時佈局會整體上移的。
最後當然是在 Activity​ 中調用了:

    KeyboardStatusWatcher(this,this) { isKeyboardShowed: Boolean, keyboardHeight: Int ->
        vKeyboardBg.updateLayoutParams<ConstraintLayout.LayoutParams> {
            bottomMargin = keyboardHeight
        }
        Log.d("Tag", "isShowed = $isKeyboardShowed,keyboardHeight = $keyboardHeight")
    }
}

4、項目地址
文章到此就結束了,項目地址如下:Gitee。
項目還有一個不足之處:實現需求了,但是使用體驗上跟微信相比差很多,微信的輸入框在軟鍵盤彈出和收起時上下移動非常順滑,沒有什麼閃爍。
如果你有更好的實現方法或者有其他的批評建議,歡迎留言和我交流。
5、參考文章
android EditText 橫屏顯示問題 - 簡書
Android 動態獲取軟鍵盤的高度,監聽軟鍵盤顯示或則隱藏。 - 掘金
Android 獲取窗口可視區域大小: getWindowVisibleDisplayFrame()_ccpat 的專欄-CSDN 博客
Android 全面解析之 Window 機制_一隻修仙的猿-CSDN 博客

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.