WindowInsets - 獲取導航欄,狀態(tài)欄,鍵盤的高度和狀態(tài)

WindowInsets - 獲取導航欄,狀態(tài)欄,鍵盤的高度和狀態(tài)

背景

最新的 Android R(11) 推出了許多功能,有一個比較重要的功能(需梯子):
Synchronized IME transitions

A new set of APIs let you synchronize your app’s content with the IME (input method editor, aka soft keyboard) and system bars as they animate on and offscreen, making it much easier to create natural, intuitive and jank-free IME transitions. For frame-perfect transitions, a new insets animation listener notifies apps of per-frame changes to insets while the system bars or the IME animate. Additionally, apps can take control of the IME and system bar transitions through the WindowInsetsAnimationController API. For example, app-driven IME experiences let apps control the IME in response to overscrolling the app UI. Give these new IME transitions a try and let us know what other transitions are important to you.

按照文章的意思,可以監(jiān)聽鍵盤的高度變化,光介紹就非常讓人激動人心.

凡是搞過鍵盤的同學都知道,監(jiān)聽 Android 鍵盤的高度非常復雜,網(wǎng)上的一些黑科技也只對某些場景,有些場景就是無法處理。

而且有一個非常關鍵的點:鍵盤只有完全彈出來了才知道高度,當我們想根據(jù)鍵盤的上升做一個動畫時,就很難做到-<b>無法知道鍵盤動畫的時間和最終高度</b>

我們來看看官方給的效果圖:

image2.gif

話不多說,我們來測試一下

測試

引入

  1. 前期準備

    先將 Android Studio 和 Gradle, Android SDK 更新到最新, 我的版本分別是:
    Android Studio 3.6.1, Gradle 5.6.4, Android SDK R


有 Pixel 手機的直接更新到最新版本 R ,沒有的可以下載最新的 R 鏡像

  1. 更新 gradle 配置
android {
    compileSdkVersion 'android-R'
    buildToolsVersion "29.0.2"

    defaultConfig {
        applicationId "com.example.myapplication"
        minSdkVersion 'R'
        targetSdkVersion 'R'
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    //....

}

使用

  1. 先設置 FitSystemWindows 為 false:
 //非常重要,沒有這句話監(jiān)聽無法生效
window.setDecorFitsSystemWindows(false)
  1. 再對 view 設置監(jiān)聽:
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //非常重要,沒有這句話監(jiān)聽無法生效
        window.setDecorFitsSystemWindows(false)
        setContentView(R.layout.activity_main)
        val callback = object : WindowInsetsAnimation.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
            override fun onProgress(
                insets: WindowInsets,
                animations: MutableList<WindowInsetsAnimation>
            ): WindowInsets {
                Log.e("MainActivity", "ime:" + insets.getInsets(WindowInsets.Type.ime()).top +
                            " " + insets.getInsets(WindowInsets.Type.ime()).bottom)
                return insets
            }
        }
        content.setWindowInsetsAnimationCallback(callback)
    }
}

運行結果:

MainActivity: ime:0 0
MainActivity: ime:0 0
MainActivity: ime:0 9
MainActivity: ime:0 37
MainActivity: ime:0 98
MainActivity: ime:0 207
MainActivity: ime:0 351
MainActivity: ime:0 526
MainActivity: ime:0 684
MainActivity: ime:0 799
MainActivity: ime:0 895
MainActivity: ime:0 1020
MainActivity: ime:0 1062
MainActivity: ime:0 1095
MainActivity: ime:0 1117
MainActivity: ime:0 1134
MainActivity: ime:0 1146
MainActivity: ime:0 1152
MainActivity: ime:0 1155

可以看到很清晰的打印出了鍵盤的每一幀高度,這樣我們就可以根據(jù)高度回調(diào),實現(xiàn)文章開頭的效果

對比微信鍵盤彈出和 Android R 的鍵盤彈出,可以看到微信的上升和下降,都不是完全吻合,但是 Android R 一直是穩(wěn)穩(wěn)的貼著:

WechatIMG9.jpeg

未命名.gif

詳細代碼

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //非常重要,沒有這句話監(jiān)聽無法生效
        window.setDecorFitsSystemWindows(false)
        setContentView(R.layout.activity_main)
        val callback = object : WindowInsetsAnimation.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
            override fun onProgress(
                insets: WindowInsets,
                animations: MutableList<WindowInsetsAnimation>
            ): WindowInsets {
             val navigationBars = insets.getInsets(WindowInsets.Type.navigationBars())
                             val ime = insets.getInsets(WindowInsets.Type.ime())
                             Log.e(
                                 TAG, "ime:" + ime.top +
                                         " " + ime.bottom
                             )
                             val parmas = (content.layoutParams as ViewGroup.MarginLayoutParams)
                             parmas.bottomMargin = ime.bottom - navigationBars.bottom
                             content.layoutParams = parmas
                             return insets
                return insets
            }
        }
        content.setWindowInsetsAnimationCallback(callback)
    }
}

分析

除了上面例子用到的 onProgress() 方法,WindowInsetsAnimation.Callback 還有其他的屬性和方法值得關注:

  1. 分發(fā)方式

構造 WindowInsetsAnimation.Callback(int) 傳入一個int 值表示分發(fā)方式,目前有兩個值:

  • DISPATCH_MODE_CONTINUE_ON_SUBTREE :繼續(xù)分發(fā)動畫事件
  • DISPATCH_MODE_STOP :不再分發(fā)

這兩個值和 view 的事件分發(fā)很類似,這里就不多解釋了。

  1. 鍵盤彈出的開始和結束

假設需求僅僅是想獲取鍵盤的高度,不需要實時獲取高度變化,可以重寫 start() 方法

val callback = object : WindowInsetsAnimation.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
            override fun onStart(
                animation: WindowInsetsAnimation,
                bounds: WindowInsetsAnimation.Bounds
            ): WindowInsetsAnimation.Bounds {
                Log.e(TAG,"start lowerBound:" + bounds.lowerBound.top + " " + bounds.lowerBound.bottom)
                Log.e( TAG,"start upperBound:" + bounds.upperBound.top + " " + bounds.upperBound.bottom)
                Log.e(TAG, "start time:" + animation.durationMillis)
                return super.onStart(animation, bounds)
            }
        }

其中 bounds 表示目標對象,從里面可以拿到動畫結束后鍵盤有多高。

animation 中可以獲取動畫的執(zhí)行時間,透明度等等。

導航欄,狀態(tài)欄高度和狀態(tài)

獲取高度之前先來了解一下 WindowInsets.Type 有什么類型,上面我們用到了 ime() 是鍵盤,除了鍵盤,還有其他的類型,包括:

android.view.WindowInsets.Type.STATUS_BARS, //狀態(tài)欄
android.view.WindowInsets.Type.NAVIGATION_BARS, //導航欄
android.view.WindowInsets.Type.CAPTION_BAR,
android.view.WindowInsets.Type.IME, //鍵盤
android.view.WindowInsets.Type.WINDOW_DECOR,
android.view.WindowInsets.Type.SYSTEM_GESTURES,
android.view.WindowInsets.Type.MANDATORY_SYSTEM_GESTURES,
and android.view.WindowInsets.Type.TAPPABLE_ELEMENT

類型很多,我們通常關心鍵盤,狀態(tài)欄和導航欄

獲取高度和狀態(tài)

在 Android R 之前,獲取狀態(tài)欄高度通常是通過反射獲取。但是有了 WindowInsets 就不用這么麻煩了:

content.setOnApplyWindowInsetsListener { view, windowInsets ->
    //狀態(tài)欄
    val statusBars = windowInsets.getInsets(WindowInsets.Type.statusBars())
    //導航欄
    val navigationBars = windowInsets.getInsets(WindowInsets.Type.navigationBars())
    //鍵盤
    val ime = windowInsets.getInsets(WindowInsets.Type.ime())
    windowInsets
}

上面代碼可以獲取導航欄和狀態(tài)欄的高度,假設要獲取隱藏和顯示,可以通過:

//注意:setOnApplyWindowInsetsListener 一設置監(jiān)聽就會回調(diào),此時獲取的 navigationBars 是否可見是 false
//等繪制完成再去獲取就是 true,這個稍微比較坑一點
windowInsets.isVisible(WindowInsets.Type.navigationBars())

控制各種狀態(tài)欄的顯示和隱藏

在 R 之前,控制導航欄和狀態(tài),需要用上各種謎之屬性:

view.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
                          View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
                          View.SYSTEM_UI_FLAG_HIDE_NAVIGATION

新版是這樣的:

//對狀態(tài)欄和鍵盤也可以同樣控制
content.windowInsetsController?.show(WindowInsets.Type.navigationBars())
content.windowInsetsController?.hide(WindowInsets.Type.navigationBars())

其他

一番測試下來,新版的 API 對于之前來說,可以說是非常好用了。

目前存在以下幾個問題:

  1. 不支持舊版

    如果僅是在 Android R 上使用,那這個工具就沒這么香了,希望能通過 androidx 或 support 方式支持

  2. FitSystemWindows

    在有虛擬導航欄的手機上,F(xiàn)itSystemWindows 設置為 false,會強制改變 Activity 與導航欄的關系。

    默認情況下, Activity 在導航欄的上面,它們處于同一層,但是設置為 false 之后,導航欄會直接覆蓋在 Activity 的上面。
    不過這可以通過給 Activity 的 parent 設置一個 padding 來解決。

  3. 等 R 發(fā)布后,還需要測試國產(chǎn) ROM,以及第三方鍵盤的兼容性

總結

總的來說這個工具的出現(xiàn),使獲取,管理鍵盤等 APP 以外的裝飾都變得非常友好。

由于 Android R 現(xiàn)在還是預覽版,還沒有公布源碼,暫時不知道其內(nèi)部實現(xiàn)原理,等后期公布了源碼再分析一下原理

Demo

https://github.com/siyehua/WindowInsetsAnimation

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容