2024年最新[译] 在 Android 上实现 Google Inbox 的样式动画(3),2024年最新技术总监都拍手叫好

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

设置

为了复制动画,我构建了一个简单的带有 2 个 fragment 的应用程序 ,如下所示分别是 Email List fragment 和 Email Details fragment。

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

电子邮件列表 InProgress 状态(左)- 电子邮件列表 Success 状态(中)- 电子邮件详细信息(右)

为了模拟电子邮件获取网络请求,我为 Email List fragment 创建了一个 [ViewModel]( ),它生成了 2 个状态,InProgress 表示正在获取电子邮件,Success 表示电子邮件数据已成功获取并准备好呈现(网络请求被模拟为 2 秒)。

sealed class State {
object InProgress : State()
data class Success(val data: List) : State()
}

Email List fragment 有一种方法来呈现这些状态,如下所示。

private fun render(state: State) {
when (state) {
is InProgress -> {
emailList.visibility = GONE
progressBar.visibility = VISIBLE
}

is Success -> {
emailList.visibility = VISIBLE
progressBar.visibility = GONE
emailAdapter.setData(state.data)
}
}

每当 Email List fragment 被新加载时,都会获取电子邮件数据并呈现 InProgress 状态,直到电子邮件数据可用(Success 状态)。点击电子邮件列表中的任何电子邮件项目将使用户进入 Email Details fragment,并将用户从电子邮件详细信息中带回电子邮件列表。

现在开始我们的旅程吧…

第一站 - 那是什么样的动画?

有一点是可以立刻确定的就是他是一种 [Explode]( ) 过渡动画,因为在被点击的 item 上下的 item 有过度。但是等一下,电子邮件详细信息 view 也会从点击的电子邮件项目进行转换和扩展。这意味着还有一个共享元素转换。结合我说的,下面是我做出的第一次尝试。

override fun onBindViewHolder(holder: EmailViewHolder, position: Int) {
fun onViewClick() {
val viewRect = Rect()
holder.itemView.getGlobalVisibleRect(viewRect)

exitTransition = Explode().apply {
duration = TRANSITION_DURATION
interpolator = transitionInterpolator
epicenterCallback = object : Transition.EpicenterCallback() {
override fun onGetEpicenter(transition: Transition) = viewRect
}
}

val sharedElementTransition = TransitionSet()
.addTransition(ChangeBounds())
.addTransition(ChangeTransform())
.addTransition(ChangeImageTransform()).apply {
duration = TRANSITION_DURATION
interpolator = transitionInterpolator
}

val fragment = EmailDetailsFragment().apply {
sharedElementEnterTransition = sharedElementTransition
sharedElementReturnTransition = sharedElementTransition
}

activity!!.supportFragmentManager
.beginTransaction()
.setReorderingAllowed(true)
.replace(R.id.container, fragment)
.addToBackStack(null)
.addSharedElement(holder.itemView, getString(R.string.transition_name))
.commit()
}

holder.bindData(emails[position], ::onViewClick)
}

这是我得到的(电子邮件详细信息视图的背景设置为蓝色,以便清楚地演示过渡效果)…

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

当然这不是我想要的。这里有两个问题。

  1. 电子邮件项目不会同时开始转换。远离被点击条目的 items 过度的更快。
  2. 被点击的电子邮件项目上的共享元素转换与其他项目的转换不同步,即,当分别展开时,Email 4Email 6 应始终粘贴在蓝色矩形的顶部和底部边缘。但他们没有!

所以究竟哪里出了问题?

第二站:开箱即用的 Explode 效果不是我想要的。

在深入研究 Explode 源代码后,我发现了两个有趣的事实:

  • 它使用 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()

}

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

8)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值