若是您已經看過個人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
處理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()
擴展名方法。 這是[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()
。 這不是絕對必要的,但確實能夠解決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 }" />
複製代碼
易用性等級:💯
你可能已經閱讀過這篇文章,並想到了_「Why hasn’t he mentioned the fitSystemWindows attribute?"_。 緣由是由於屬性帶來的功能一般不是咱們想要的。
若是您正在使用AppBarLayout,CoordinatorLayout,DrawerLayout和朋友,那麼按照指示使用。 構建這些視圖是爲了識別屬性,並以與這些視圖相關的固定方式應用窗口插入。
android:fitSystemWindows
的默認View實現意味着使用insets填充每一個維度,但不適用於上面的示例。 有關更多信息,請參閱此blog post,它仍然很是相關。