WindowInsets - 佈局的監聽器

若是您已經看過個人Becoming a Master Window Fitter談話,您就會知道處理窗口插件可能很複雜。 最近,我一直在改進幾個應用程序中的系統欄處理,使他們可以在狀態和導航欄後面繪製。 我想我已經提出了一些方法,可使處理插入更容易(但願如此)。原文html

在導航欄後面繪製

對於本文的其他部分,咱們將使用BottomNavigationView進行一個簡單的示例,該示例位於屏幕底部。 它的實現很是簡單:android

<BottomNavigationView
    android:layout_height="wrap_content"
    android:layout_width="match_parent" />
複製代碼

默認狀況下,您的Activity的內容將在系統提供的UI(導航欄等)中進行佈局,所以咱們的視圖與導航欄齊平。 咱們的設計師決定他們但願應用程序開始在導航欄後面繪製。 要作到這一點,咱們將使用適當的標誌調用setSystemUiVisibility()swift

rootView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
        View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
複製代碼

最後咱們將更新咱們的主題,以便咱們有一個半透明的導航欄,帶有黑色圖標:windows

<style name="AppTheme" parent="Theme.MaterialComponents.Light">
    <!-- Set the navigation bar to 50% translucent white -->
    <item name="android:navigationBarColor">#80FFFFFF</item>
    <!-- Since the nav bar is white, we will use dark icons -->
    <item name="android:windowLightNavigationBar">true</item>
</style>
複製代碼

如您所見,這只是咱們須要作的事情的開始。 因爲活動如今正在導航欄後面,咱們的BottomNavigationView也是如此。 這意味着用戶沒法實際點擊任何導航項。 爲了解決這個問題,咱們須要處理系統調度的任何WindowInsets,並使用這些值對視圖應用適當的填充或邊距。app

Handling經過填充進行插入

處理WindowInsets的經常使用方法之一是爲視圖添加填充,以便它們的內容不會顯示在system-ui後面。 爲此,咱們能夠設置OnApplyWindowInsetsListener,爲視圖添加必要的底部填充,確保其內容不被遮擋。ide

bottomNav.setOnApplyWindowInsetsListener { view, insets ->
    view.updatePadding(bottom = insets.systemWindowInsetBottom)
    insets
}
複製代碼

好的,咱們如今已經正確處理了底部系統窗口的插入。 但後來咱們決定在佈局中添加一些填充,多是出於審美緣由:函數

<BottomNavigationView
    android:layout_height="wrap_content"
    android:layout_width="match_parent"
    android:paddingVertical="24dp" />
複製代碼

Note: I’m not recommending using 24dp of vertical padding on a BottomNavigationView, I am using a large value here just to make the effect obvious.佈局

嗯,那不對。 你能看到問題嗎? 咱們從OnApplyWindowInsetsListener調用updatePadding()如今將從佈局中消除預期的底部填充。post

啊哈! 讓咱們一塊兒添加當前填充和插入:ui

bottomNav.setOnApplyWindowInsetsListener { view, insets ->
    view.updatePadding(
        bottom = view.paddingBottom + insets.systemWindowInsetsBottom
    )
    insets
}
複製代碼

咱們如今有一個新問題。 WindowInsets能夠在_any_時調度,_multiple_能夠在視圖的生命週期中調度。 這意味着咱們的新邏輯將在第一次運行時運行良好,可是對於每一個後續調度,咱們將添加愈來愈多的底部填充。 不是咱們想要的。🤦

我想出的解決方案是在通脹後記錄視圖的填充值,而後再參考這些值。 例:

// Keep a record of the intended bottom padding of the view
val bottomNavBottomPadding = bottomNav.paddingBottom

bottomNav.setOnApplyWindowInsetsListener { view, insets ->
    // We've got some insets, set the bottom padding to be the
    // original value + the inset value
    view.updatePadding(
        bottom = bottomNavBottomPadding + insets.systemWindowInsetBottom
    )
    insets
}
複製代碼

這很好用,意味着咱們從佈局中保持填充的意圖,咱們仍然根據須要插入視圖。 保持每一個填充值的對象級屬性是很是混亂的,咱們能夠作得更好......🤔

doOnApplyWindowInsets

輸入doOnApplyWindowInsets()擴展名方法。 這是[setOnApplyWindowInsetsListener()](developer.android.com/reference/a…

fun View.doOnApplyWindowInsets(f: (View, WindowInsets, InitialPadding) -> Unit) {
    // Create a snapshot of the view's padding state
    val initialPadding = recordInitialPaddingForView(this)
    // Set an actual OnApplyWindowInsetsListener which proxies to the given
    // lambda, also passing in the original padding state
    setOnApplyWindowInsetsListener { v, insets ->
        f(v, insets, initialPadding)
        // Always return the insets, so that children can also use them
        insets
    }
    // request some insets
    requestApplyInsetsWhenAttached()
}

data class InitialPadding(val left: Int, val top: Int, val right: Int, val bottom: Int) private fun recordInitialPaddingForView(view: View) = InitialPadding( view.paddingLeft, view.paddingTop, view.paddingRight, view.paddingBottom) 複製代碼

當咱們須要一個視圖來處理insets時,咱們如今能夠執行如下操做:

bottomNav.doOnApplyWindowInsets { view, insets, padding ->
    // padding contains the original padding values after inflation
    view.updatePadding(
        bottom = padding.bottom + insets.systemWindowInsetBottom
    )
}
複製代碼

好多了!😏

requestApplyInsetsWhenAttached()

您可能已經注意到上面的requestApplyInsetsWhenAttached()。 這不是絕對必要的,但確實能夠解決WindowInsets的分派方式。 若是視圖在未附加到視圖層次結構時調用requestApplyInsets(),則會將調用放在地板上並忽略。

這是在[Fragment.onCreateView()](developer.android.com/reference/a… 修復方法是確保簡單地調用[onStart()](developer.android.com/reference/a… 如下擴展函數處理兩種狀況:

fun View.requestApplyInsetsWhenAttached() {
    if (isAttachedToWindow) {
        // We're already attached, just request as normal
        requestApplyInsets()
    } else {
        // We're not attached to the hierarchy, add a listener to
        // request when we are
        addOnAttachStateChangeListener(object : OnAttachStateChangeListener {
            override fun onViewAttachedToWindow(v: View) {
                v.removeOnAttachStateChangeListener(this)
                v.requestApplyInsets()
            }

            override fun onViewDetachedFromWindow(v: View) = Unit
        })
    }
}
複製代碼

在綁定中包裝它

在這一點上,咱們已經大大簡化了如何處理窗口插入。 咱們實際上在一些即將推出的應用程序中使用此功能,包括即將舉行的會議apps。 它仍然有一些缺點。 首先,邏輯遠離咱們的佈局,這意味着它很容易被遺忘。 其次,咱們可能須要在許多地方使用它,致使大量的near-identical副本在整個應用程序中傳播。 我知道咱們能夠作得更好。

到目前爲止,整個帖子只關注代碼,並經過設置監聽器來處理insets。 咱們在這裏討論的是視圖,因此在理想的世界中咱們會聲明咱們打算在佈局文件中處理插圖。

輸入data binding adapters! 若是您之前從未使用它們,它們會讓咱們將代碼映射到佈局屬性(當您使用數據綁定時)。 所以,讓咱們爲咱們建立一個屬性:

@BindingAdapter("paddingBottomSystemWindowInsets")
fun applySystemWindowBottomInset(view: View, applyBottomInset: Boolean) {
    view.doOnApplyWindowInsets { view, insets, padding ->
        val bottom = if (applyBottomInset) insets.systemWindowInsetBottom else 0
        view.updatePadding(bottom = padding.bottom + insets.systemWindowInsetBottom)
    }
}
複製代碼

在咱們的佈局中,咱們能夠簡單地使用咱們新的paddingBottomSystemWindowInsets屬性,該屬性將自動更新任何插入。

<BottomNavigationView
    android:layout_height="wrap_content"
    android:layout_width="match_parent"
    android:paddingVertical="24dp"
    app:paddingBottomSystemWindowInsets="@{ true }" />
複製代碼

但願您可以看到與單獨使用OnApplyWindowListener相比,它是如何符合人體工程學且易於使用的。🌠

但等等,綁定適配器硬編碼只設置底部尺寸。 若是咱們還須要處理頂部插圖怎麼辦? 仍是左邊? 仍是對嗎? 幸運的是,綁定適配器讓咱們能夠很好地歸納全部維度的模式:

@BindingAdapter(
    "paddingLeftSystemWindowInsets",
    "paddingTopSystemWindowInsets",
    "paddingRightSystemWindowInsets",
    "paddingBottomSystemWindowInsets",
    requireAll = false
)
fun applySystemWindows(
    view: View,
    applyLeft: Boolean,
    applyTop: Boolean,
    applyRight: Boolean,
    applyBottom: Boolean
) {
    view.doOnApplyWindowInsets { view, insets, padding ->
        val left = if (applyLeft) insets.systemWindowInsetLeft else 0
        val top = if (applyTop) insets.systemWindowInsetTop else 0
        val right = if (applyRight) insets.systemWindowInsetRight else 0
        val bottom = if (applyBottom) insets.systemWindowInsetBottom else 0

        view.setPadding(
            padding.left + left,
            padding.top + top,
            padding.right + right,
            padding.bottom + bottom
        )
    }
}
複製代碼

這裏咱們已經聲明瞭一個具備多個屬性的適配器,每一個屬性都映射到相關的方法參數。 須要注意的一點是使用requireAll = false,這意味着適配器能夠處理所設置屬性的任意組合。 這意味着咱們能夠執行如下操做,例如設置左側和底部:

<BottomNavigationView
    android:layout_height="wrap_content"
    android:layout_width="match_parent"
    android:paddingVertical="24dp"
    app:paddingBottomSystemWindowInsets="@{ true }"
    app:paddingLeftSystemWindowInsets="@{ true }" />
複製代碼

易用性等級:💯

android:fitSystemWindows

你可能已經閱讀過這篇文章,並想到了_「Why hasn’t he mentioned the fitSystemWindows attribute?"_。 緣由是由於屬性帶來的功能一般不是咱們想要的。

若是您正在使用AppBarLayoutCoordinatorLayoutDrawerLayout和朋友,那麼按照指示使用。 構建這些視圖是爲了識別屬性,並以與這些視圖相關的固定方式應用窗口插入。

android:fitSystemWindows的默認View實現意味着使用insets填充每一個維度,但不適用於上面的示例。 有關更多信息,請參閱此blog post,它仍然很是相關。

相關文章
相關標籤/搜索