[译] 在 Android 上实现 Google Inbox 的样式动画

  • 它使用 CircularPropagation 来强制执行这样一条规则,即,当它们从屏幕上消失时,离中心远的视图过渡速度会地比离中心近的视图快。Explode 过渡的中心被设置为覆盖被点击的电子邮件项目的矩形。这解释了为什么未打开的电子邮件项目视图不会如上所述一起转换。
  • 电子邮件条目的上下距离和被点击的条目的上下距离是不一样的。在这种特定情况下,该距离被确定为从被点击项目的中心点到屏幕的每个角落的距离中最长的。

所以我决定编写自己的 Explode 过渡。我将它命名为 SlideExplode,因为它与 Slide 过渡非常相似,只是有 2 个部分在 2 个相反的方向上移动。

import android.animation.Animator
import android.animation.ObjectAnimator
import android.graphics.Rect
import android.transition.TransitionValues
import android.transition.Visibility
import android.view.View
import android.view.ViewGroup

private const val KEY_SCREEN_BOUNDS = “screenBounds”

/**

  • A simple Transition which allows the views above the epic centre to transition upwards and views
  • below the epic centre to transition downwards.
    */
    class SlideExplode : Visibility() {
    private val mTempLoc = IntArray(2)

private fun captureValues(transitionValues: TransitionValues) {
val view = transitionValues.view
view.getLocationOnScreen(mTempLoc)
val left = mTempLoc[0]
val top = mTempLoc[1]
val right = left + view.width
val bottom = top + view.height
transitionValues.values[KEY_SCREEN_BOUNDS] = Rect(left, top, right, bottom)
}

override fun captureStartValues(transitionValues: TransitionValues) {
super.captureStartValues(transitionValues)
captureValues(transitionValues)
}

override fun captureEndValues(transitionValues: TransitionValues) {
super.captureEndValues(transitionValues)
captureValues(transitionValues)
}

override fun onAppear(sceneRoot: ViewGroup, view: View,
startValues: TransitionValues?, endValues: TransitionValues?): Animator? {
if (endValues == null) return null

val bounds = endValues.values[KEY_SCREEN_BOUNDS] as Rect
val endY = view.translationY
val startY = endY + calculateDistance(sceneRoot, bounds)
return ObjectAnimator.ofFloat(view, View.TRANSLATION_Y, startY, endY)
}

override fun onDisappear(sceneRoot: ViewGroup, view: View,
startValues: TransitionValues?, endValues: TransitionValues?): Animator? {
if (startValues == null) return null

val bounds = startValues.values[KEY_SCREEN_BOUNDS] as Rect
val startY = view.translationY
val endY = startY + calculateDistance(sceneRoot, bounds)
return ObjectAnimator.ofFloat(view, View.TRANSLATION_Y, startY, endY)
}

private fun calculateDistance(sceneRoot: View, viewBounds: Rect): Int {
sceneRoot.getLocationOnScreen(mTempLoc)
val sceneRootY = mTempLoc[1]
return when {
epicenter == null -> -sceneRoot.height
viewBounds.top <= epicenter.top -> sceneRootY - epicenter.top
else -> sceneRootY + sceneRoot.height - epicenter.bottom
}
}
}

现在我已经为 SlideExplode 交换了 Explode,让我们再试一次。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这样就好多了!上面和下面的项目现在开始同时转换。请注意,由于插值器设置为 FastOutSlowIn,因此当 Email 4Email 6 分别靠近顶部和底部边缘时,它们会减慢速度。这表明 SlideExplode 过渡正常。

但是,Explode 转换和共享元素转换仍未同步。我们可以看到他们正在以不同的模式移动,这表明他们的插值器可能不同。前一个过渡开始非常快,最后减速,而后者一开始很慢,一段时间后加速。

但是怎么样?我确实在代码中将插值器设置相同了!

第三站:原来是 TransitionSet 的锅!

我再次深入研究源代码。这次我发现每当我将插值器设置为 TransitionSet 时,它都不会在过渡的时候将插值器分配给它。这仅在标准 TransitionSet中 发生。它的支持版本(android.support.transition.TransitionSet)正常工作。要解决此问题,我们可以切换到支持版本,或者使用下面的扩展函数将插值器明确地传递给包含的转换。

fun TransitionSet.setCommonInterpolator(interpolator: Interpolator): TransitionSet {
(0 until transitionCount)
.map { index -> getTransitionAt(index) }
.forEach { transition -> transition.interpolator = interpolator }

return this
}

让我们在更新插值器的设置后再试一次。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

YAYYYY!现在看起来很正确。但反向过渡怎么样?

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

没有达到我想要的结果!Explode 过渡似乎有效。但是,共享元素过渡没有。

第四站:推迟进入转换

反向过渡动画不起作用的原因是它发挥得太早。对于任何过渡的工作,它需要捕获目标视图的开始和结束状态(大小,位置,范围),在这种情况下,它们是 Email Details 视图和 Email 5 item 项。如果在 Email 5 item 的状态可用之前启动了反向转换,则它将无法像我们所看到的那样正常运行。

这里的解决方案是推迟反向转换,直到 items 都被绘制完。幸运的是,transition 框架提供了一对 postponeEnterTransition 方法,它向系统标记输入过渡应该被推迟,startPostponedEnterTransition 表示它可以启动。请注意,必须在调用 startPostponedEnterTransition 后的某个时间调用 postponeEnterTransition。否则,将永远不会执行过渡动画,并且 fragment 也不会弹出。

根据我们的设置,每当从 Email Details fragment 重新进入 Email List fragment 时,它会从 view model 中获取最新状态并立即呈现电子邮件列表。因此,如果我们推迟过渡动画,直到呈现电子邮件列表,等待时间不会太长(从死进程中恢复并弹出是一个不同的情况。这将在后面的帖子中介绍)。

更新后的代码如下所示。我们推迟了 onViewCreated 中的 enter 转换。

override fun onViewCreated(view: View, savedState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
postponeEnterTransition()

}

并在渲染状态后开始推迟过渡。这是使用 doOnPreDraw 完成的。

is Success -> {

(view?.parent as? ViewGroup)?.doOnPreDraw {
startPostponedEnterTransition()
}
}

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在它成功了!但当方向变换时这个过度效果还会存在吗?

第五站:位置方向改变

转换后,Email List fragment 并没有发生反转过渡动画。经过一些调试后,我发现当 fragment 的方向发生改变时,过渡动画也被销毁了。因此,应在 fragment 被销毁后重新创建过渡动画。此外,由于屏幕尺寸和 UI 差异,Explode 的过渡中心在纵向和横向模式下通常是不相同的。因此我们也需要更新中心区域。

这要求我们跟踪点击项目的位置并在方向更改时重新记录,这将导致更新的代码如下。

override fun onViewCreated(view: View, savedState: Bundle?) {
super.onViewCreated(view, savedState)
tapPosition = savedState?.getInt(TAP_POSITION, NO_POSITION) ?: NO_POSITION
postponeEnterTransition()

}

private fun render(state: State) {
when (state) {

is Success -> {

(view?.parent as? ViewGroup)?.doOnPreDraw {
if (exitTransition == null) {
exitTransition = SlideExplode().apply {
duration = TRANSITION_DURATION
interpolator = transitionInterpolator
}
}

val layoutManager = emailList.layoutManager as LinearLayoutManager
layoutManager.findViewByPosition(tapPosition)?.let { view ->
view.getGlobalVisibleRect(viewRect)
(exitTransition as Transition).epicenterCallback =
object : Transition.EpicenterCallback() {
override fun onGetEpicenter(transition: Transition) = viewRect
}
}

startPostponedEnterTransition()
}
}
}
}

override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt(TAP_POSITION, tapPosition)
}

第六站:处理 Activity 被销毁和进程被杀死的情况

过渡动画现在可以在方向变化中存活,但在 activity 被销毁或者进程被杀死时又会有什么样的效果呢?在我们的特定方案中,电子邮件列表 viewModel 在任何一种情况下都不存活,因此电子邮件数据也不存在。我们的转换取决于所点击的电子邮件项目的位置,因此如果数据丢失则无法使用。

奇怪的是,我查看了几个著名的应用程序,看看它们在这种情况下如何处理转换:

  • Google Inbox:有趣的是,它不需要处理这种情况,因为它会在活动被销毁后重新加载电子邮件列表(而不是电子邮件详细信息)。
  • Google Play:活动销毁或处理死亡后没有反向共享元素转换。
  • Plaid (不是一个真正的应用程序,但却是 Android 上的一个优秀的 material design 的 demo):即使在方向改变之后(截至编写时),也没有反向共享元素过渡。

虽然上面的列表没有足够的结论来处理 Android 应用程序在这种情况下处理转换的模式,但它至少显示了一些观点。

回到我们的具体问题,通常有两种可能性取决于每个应用程序处理此类情况的方法:(1)忽略丢失的数据并重新获取数据,以及(2)保留数据并恢复数据。由于这篇文章主要是关于过渡动画,所以我不打算讨论在什么情况下哪种方法更好以及为什么等。如果采用方法(1),则不应该进行反向转换,因为我们不知道先前被点击的电子邮件项目是否会被取回,即使知道,我们不知道它在列表中的位置。如果采用方法(2),我们可以像定向改变方案那样进行转换。

方法(1)是我在这种特定情况下的偏好,因为新的电子邮件可能每分钟都会出现,因此在活动销毁或处理死亡之后重新加载过时的电子邮件列表是没有用的,这通常发生在用户离开应用程序一段时间之后。在我们的设置中,当activity 被销毁或进程被杀死后后重新创建电子邮件列表片段时,将自动获取电子邮件数据,因此不需要做太多工作。我们只需要确保在呈现 InProgress 状态时调用 startPostponedEnterTransition

is InProgress -> {

(view?.parent as? ViewGroup)?.doOnPreDraw {
startPostponedEnterTransition()
}
}

第七站:让过渡动画更加平滑

到目前为止,我们已经有了一个基本的 “Inbox style” 过渡。有很多方法实现平滑。一个例子是在展开细节时呈现淡入效果,类似于收件箱应用程序的功能。这可以通过以下方式实现:

class EmailDetailsFragment : Fragment() {

override fun onViewCreated(view: View, savedState: Bundle?) {
super.onViewCreated(view, savedState)

val content = view.findViewById(R.id.content).also { it.alpha = 0f }

ObjectAnimator.ofFloat(content, View.ALPHA, 0f, 1f).apply {
startDelay = 50
duration = 150
start()
}
}
}

过渡动画现在看起来如下。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

他已经被完全复制了吗?

基本上是。唯一缺少的是能够垂直滑动电子邮件详细信息视图以显示电子邮件列表中的其他电子邮件,并通过释放手指触发反向过渡,就和下面的 GIF 图所展示的效果一样。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
这样的动画对我来说很有意义,因为如果用户可以点击电子邮件项目来打开/展开它,他自然会拖下电子邮件详细信息来隐藏/折叠它。目前我正在探索实现这种效果的几个选项,它们将在下一篇文章中讨论。


那就这样吧。实现动画是 Android 开发中一个具有挑战性但又有趣的部分。我希望你喜欢和我一样喜欢动画。源代码可以在这里找到。欢迎提出反馈/意见/讨论!

如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。


最后

感觉现在好多人都在说什么安卓快凉了,工作越来越难找了。又是说什么程序员中年危机啥的,为啥我这年近30的老农根本没有这种感觉,反倒觉得那些贩卖焦虑的都是瞎j8扯谈。当然,职业危机意识确实是要有的,但根本没到那种草木皆兵的地步好吗?

Android凉了都是弱者的借口和说辞。虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,很多高级职位给的薪资真的特别高(钱多也不一定能找到合适的),所以努力让自己成为高级工程师才是最重要的。

所以,最后这里放上我耗时两个月,将自己8年Android开发的知识笔记整理成的Android开发者必知必会系统学习资料笔记,上述知识点在笔记中都有详细的解读,里面还包含了腾讯、字节跳动、阿里、百度2019-2021面试真题解析,并且把每个技术点整理成了视频和PDF(知识脉络 + 诸多细节)。

以上全套学习笔记面试宝典,吃透一半保你可以吊打面试官,只有自己真正强大了,有核心竞争力,你才有拒绝offer的权力,所以,奋斗吧!骚年们!千里之行,始于足下。种下一颗树最好的时间是十年前,其次,就是现在。

最后,赠与大家一句诗,共勉!

不驰于空想,不骛于虚声。不忘初心,方得始终。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!
NSY6Nht-1715697949413)]

以上全套学习笔记面试宝典,吃透一半保你可以吊打面试官,只有自己真正强大了,有核心竞争力,你才有拒绝offer的权力,所以,奋斗吧!骚年们!千里之行,始于足下。种下一颗树最好的时间是十年前,其次,就是现在。

最后,赠与大家一句诗,共勉!

不驰于空想,不骛于虚声。不忘初心,方得始终。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

  • 17
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值