- 原文地址:Windows Insets + Fragment Transitions: A tale of woe
- 原文作者:Chris Banes
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者:LeeSniper
- 校對者:Starrier
一個悲傷的故事
這篇文章是我寫的關於 fragment 過渡動畫的小系列中的第二篇。第一篇可以通過下面的連結檢視,裡面寫了如何讓 fragment 過渡動畫開始工作。
在我開始進一步探討之前,我會假設你知道什麼是 WindowsInsets 以及它們是如何分發的。如果你不知道,我建議你先看這個演講(是的,這是我的演講 ?)
我需要坦白。當我在寫本系列第一篇部落格文章的時候,我對視訊做了點手腳。實際上我遇到了 WindowInsets 的問題,也就是說我實際上最終得到的是以下結果:
過渡動畫破壞了狀態列的效果。
Woops,跟我在第一篇文章中展示的效果不太一樣 ?。我不想讓第一篇文章變得太複雜,所以決定單獨寫這篇文章。無論如何,你可以看到當新增過渡動畫之後,我們突然失去了所有狀態列的效果,而且檢視被推到狀態列的下面。
問題
這兩個 fragment 為了在系統欄下面進行繪製都大量使用了 WindowInsets。Fragment A 使用了 CoordinatorLayout 和 AppBarLayout,而 Fragment B 使用自定義 WindowInsets 來處理(通過一個 OnApplyWindowInsetsListener)。無論它們是如何實現的,過渡動畫都會混淆兩者。
那麼為什麼會這樣呢?其實當你在使用 fragment 過渡動畫時,退出(Fragment A)和進入(Fragment B)的內容檢視實際上經歷了以下幾個過程:
- 過渡動畫開始。
- 因為我們對 Fragment A 使用了一個退出的過渡動畫,所以 View A 還留在原來的位置,過渡動畫在上面執行。
- View B 被新增到內容檢視裡面,並且被立即設定成不可見。
- Fragment B 的進入動畫和“共享元素進入”過渡動畫開始執行。
- View B 被設定成可見的。
- 當 Fragment A 的退出動畫結束的時候,View A 從容器檢視中移除。
這一切聽起來都很好,那為什麼會突然影響到 WindowInsets 的效果呢?這是因為在過渡的過程中,兩個 fragment 的檢視都存在於容器中。
但是這聽起來完全 OK 啊,不是嗎?然而在我的場景中,這兩個 fragment 的檢視都想要處理和消費 WindowInsets,因為它們都期望在螢幕上顯示唯一的“主”檢視。可是隻有其中的一個檢視會收到 WindowInsets:也就是第一個子 view。這取決於 ViewGroup 是如何分發 WindowInsets 的,也就是通過按順序遍歷它的子節點直到其中的一個消費了 WindowInsets。 如果第一個子 view(就是這裡的 Fragment A)消費了 WindowInsets,任何後續的子 view(就是這裡的 Fragment B)都不會得到它們,我們最終就會得到這種情況。
讓我們再來一步一步檢查一遍,只是這一次加上分發 windowinsets 的時機:
- 過渡動畫開始。
- 因為我們對 Fragment A 使用了一個退出的過渡動畫,所以 View A 還留在原來的位置,過渡動畫在上面執行。
- View B 被新增到內容檢視裡面,並且被立即設定成不可見。
- 分發 WindowInsets。我們希望 View B(child 1)拿到它們,但是 View A(child 0)又一次拿到了 WindowInsets。
- Fragment B 的進入動畫和‘共享元素進入’過渡動畫開始執行。
- View B 被設定成可見的。
- 當 Fragment A 的退出動畫結束的時候,View A 從容器檢視中移除。
修復
這個修復實際上相對簡單:我們只需要確保兩個檢視都能夠拿到 WindowInsets。
我實現這一點的方法是通過在容器檢視(在這個例子中就是在宿主 activity)裡新增一個 OnApplyWindowInsetsListener,它會手動分發 WindowInsets 給所有的子 view,直到其中一個子 view 消費掉這個 WindowInsets。
fragment_container.setOnApplyWindowInsetsListener { view, insets ->
var consumed = false
(view as ViewGroup).forEach { child ->
// Dispatch the insets to the child
val childResult = child.dispatchApplyWindowInsets(insets)
// If the child consumed the insets, record it
if (childResult.isConsumed) {
consumed = true
}
}
// If any of the children consumed the insets, return
// an appropriate value
if (consumed) insets.consumeSystemWindowInsets() else insets
}
複製程式碼
在我們應用這個修復之後,這兩個 fragment 都會收到 WindowInsets,然後我們就會得到第一篇文章中實際顯示的結果:
額外部分 ?: 一定要進行請求
還有一件我差點忘了寫的小事。如果你要在 fragment 裡面處理 WindowInsets,無論是隱式(通過使用 AppBarLayout 等)還是顯式,你需要確保請求了一些 WindowInsets。只需要調通過 requestApplyInsets() 就能很容易做到:
override fun onViewCreated(view: View, icicle: Bundle) {
super.onViewCreated(view, savedInstanceState)
// yadda, yadda
ViewCompat.requestApplyInsets(view)
}
複製程式碼
你必須這樣做是因為視窗只有在整個檢視層級總體的系統 UI 可見性的值發生改變的時候才會自動分發 WindowInsets。 由於有時你的兩個 fragment 可能提供完全相同的值,總體的值不會改變,因此係統將忽略這個“改變”。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。