[譯] WindowsInsets 和 Fragment 過渡動畫

Android_開發者發表於2018-05-23

一個悲傷的故事

這篇文章是我寫的關於 fragment 過渡動畫的小系列中的第二篇。第一篇可以通過下面的連結檢視,裡面寫了如何讓 fragment 過渡動畫開始工作。


在我開始進一步探討之前,我會假設你知道什麼是 WindowsInsets 以及它們是如何分發的。如果你不知道,我建議你先看這個演講(是的,這是我的演講 ?)


我需要坦白。當我在寫本系列第一篇部落格文章的時候,我對視訊做了點手腳。實際上我遇到了 WindowInsets 的問題,也就是說我實際上最終得到的是以下結果:

[譯] WindowsInsets 和 Fragment 過渡動畫

過渡動畫破壞了狀態列的效果。

Woops,跟我在第一篇文章中展示的效果不太一樣 ?。我不想讓第一篇文章變得太複雜,所以決定單獨寫這篇文章。無論如何,你可以看到當新增過渡動畫之後,我們突然失去了所有狀態列的效果,而且檢視被推到狀態列的下面。

問題

這兩個 fragment 為了在系統欄下面進行繪製都大量使用了 WindowInsets。Fragment A 使用了 CoordinatorLayoutAppBarLayout,而 Fragment B 使用自定義 WindowInsets 來處理(通過一個 OnApplyWindowInsetsListener)。無論它們是如何實現的,過渡動畫都會混淆兩者。

那麼為什麼會這樣呢?其實當你在使用 fragment 過渡動畫時,退出(Fragment A)和進入(Fragment B)的內容檢視實際上經歷了以下幾個過程:

  1. 過渡動畫開始。
  2. 因為我們對 Fragment A 使用了一個退出的過渡動畫,所以 View A 還留在原來的位置,過渡動畫在上面執行。
  3. View B 被新增到內容檢視裡面,並且被立即設定成不可見。
  4. Fragment B 的進入動畫和“共享元素進入”過渡動畫開始執行。
  5. View B 被設定成可見的。
  6. 當 Fragment A 的退出動畫結束的時候,View A 從容器檢視中移除。

這一切聽起來都很好,那為什麼會突然影響到 WindowInsets 的效果呢?這是因為在過渡的過程中,兩個 fragment 的檢視都存在於容器中。

但是這聽起來完全 OK 啊,不是嗎?然而在我的場景中,這兩個 fragment 的檢視都想要處理和消費 WindowInsets,因為它們都期望在螢幕上顯示唯一的“主”檢視。可是隻有其中的一個檢視會收到 WindowInsets:也就是第一個子 view。這取決於 ViewGroup 是如何分發 WindowInsets 的,也就是通過按順序遍歷它的子節點直到其中的一個消費了 WindowInsets。 如果第一個子 view(就是這裡的 Fragment A)消費了 WindowInsets,任何後續的子 view(就是這裡的 Fragment B)都不會得到它們,我們最終就會得到這種情況。

讓我們再來一步一步檢查一遍,只是這一次加上分發 windowinsets 的時機:

  1. 過渡動畫開始。
  2. 因為我們對 Fragment A 使用了一個退出的過渡動畫,所以 View A 還留在原來的位置,過渡動畫在上面執行。
  3. View B 被新增到內容檢視裡面,並且被立即設定成不可見。
  4. 分發 WindowInsets。我們希望 View B(child 1)拿到它們,但是 View A(child 0)又一次拿到了 WindowInsets。
  5. Fragment B 的進入動畫和‘共享元素進入’過渡動畫開始執行。
  6. View B 被設定成可見的。
  7. 當 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,然後我們就會得到第一篇文章中實際顯示的結果:

[譯] WindowsInsets 和 Fragment 過渡動畫


額外部分 ?: 一定要進行請求

還有一件我差點忘了寫的小事。如果你要在 fragment 裡面處理 WindowInsets,無論是隱式(通過使用 AppBarLayout 等)還是顯式,你需要確保請求了一些 WindowInsets。只需要調通過 requestApplyInsets() 就能很容易做到:

override fun onViewCreated(view: View, icicle: Bundle) {
	super.onViewCreated(view, savedInstanceState)
	// yadda, yadda
	ViewCompat.requestApplyInsets(view)
}
複製程式碼

你必須這樣做是因為視窗只有在整個檢視層級總體的系統 UI 可見性的值發生改變的時候才會自動分發 WindowInsets。 由於有時你的兩個 fragment 可能提供完全相同的值,總體的值不會改變,因此係統將忽略這個“改變”。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章