[译] WindowsInsets 和 Fragment 过渡动画

一个悲伤的故事

这篇文章是我写的关于 fragment 过渡动画的小系列中的第二篇。第一篇能够经过下面的连接查看,里面写了如何让 fragment 过渡动画开始工做。html


在我开始进一步探讨以前,我会假设你知道什么是 WindowsInsets 以及它们是如何分发的。若是你不知道,我建议你先看这个演讲(是的,这是个人演讲 🙋)前端


我须要坦白。当我在写本系列第一篇博客文章的时候,我对视频作了点手脚。实际上我遇到了 WindowInsets 的问题,也就是说我实际上最终获得的是如下结果:java

过渡动画破坏了状态栏的效果。android

Woops,跟我在第一篇文章中展现的效果不太同样 🤐。我不想让第一篇文章变得太复杂,因此决定单独写这篇文章。不管如何,你能够看到当添加过渡动画以后,咱们忽然失去了全部状态栏的效果,并且视图被推到状态栏的下面。ios

问题

这两个 fragment 为了在系统栏下面进行绘制都大量使用了 WindowInsets。Fragment A 使用了 CoordinatorLayoutAppBarLayout,而 Fragment B 使用自定义 WindowInsets 来处理(经过一个 OnApplyWindowInsetsListener)。不管它们是如何实现的,过渡动画都会混淆二者。git

那么为何会这样呢?其实当你在使用 fragment 过渡动画时,退出(Fragment A)和进入(Fragment B)的内容视图实际上经历了如下几个过程:github

  1. 过渡动画开始。
  2. 由于咱们对 Fragment A 使用了一个退出的过渡动画,因此 View A 还留在原来的位置,过渡动画在上面运行。
  3. View B 被添加到内容视图里面,而且被当即设置成不可见。
  4. Fragment B 的进入动画和“共享元素进入”过渡动画开始执行。
  5. View B 被设置成可见的。
  6. 当 Fragment A 的退出动画结束的时候,View A 从容器视图中移除。

这一切听起来都很好,那为何会忽然影响到 WindowInsets 的效果呢?这是由于在过渡的过程当中,两个 fragment 的视图都存在于容器中。windows

可是这听起来彻底 OK 啊,不是吗?然而在个人场景中,这两个 fragment 的视图都想要处理和消费 WindowInsets,由于它们都指望在屏幕上显示惟一的“主”视图。但是只有其中的一个视图会收到 WindowInsets:也就是第一个子 view。这取决于 ViewGroup 是如何分发 WindowInsets 的,也就是经过按顺序遍历它的子节点直到其中的一个消费了 WindowInsets。 若是第一个子 view(就是这里的 Fragment A)消费了 WindowInsets,任何后续的子 view(就是这里的 Fragment B)都不会获得它们,咱们最终就会获得这种状况。后端

让咱们再来一步一步检查一遍,只是这一次加上分发 windowinsets 的时机:app

  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,而后咱们就会获得第一篇文章中实际显示的结果:


额外部分 💃: 必定要进行请求

还有一件我差点忘了写的小事。若是你要在 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前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索