在 -View- 上使用挂起函数,app 开发面试题及答案
// 将该视图设置为不可见,再设置一些文字 titleView.isInvisible = truetitleView.text = "Hi everyone!"
// 等待下一次布局事件的任务,然后才可以获取该视图的高度 titleView.awaitNextLayout()
// 布局任务被执行// 现在,我们可以将视图设置为可见,并其向上平移,然后执行向下的动画 titleView.isVisible = truetitleView.translationY = -titleView.height.toFloat()titleView.animate().translationY(0f)}复制代码
我们为 View 的布局创建了一个 await 函数。用同样的方法可以替代很多常见的回调,比如?doOnPreDraw(),它是在 View 得到绘制时调用的方法;再比如?postOnAnimation(),在动画的下一帧开始时调用的方法,等等。
作用域
不知道您有没有发现这样一个问题,在上面的例子中,我们使用了?lifecycleScope?来启动协程,为什么要这样做呢?
为了避免发生内存泄漏,在我们操作 UI 的时候,选择合适的作用域来运行协程是极其重要的。幸运的是,我们的 View 有一些范围合适的?Lifecycle。我们可以使用扩展属性?lifecycleScope?来获得一个绑定生命周期的?CoroutineScope。
LifecycleScope 被包含在 AndroidX 的 lifecycle-runtime-ktx 依赖库中,可以在这里找到 更多信息
我们最常用的生命周期的持有者 (lifecycle owner) 就是 Fragment 中的?viewLifecycleOwner),只要加载了 Fragment 的视图,它就会处于活跃状态。一旦 Fragment 的视图被移除,与之关联的?lifecycleScope?就会自动被取消。又由于我们已经为挂起函数中添加了对取消操作的支持,所以?lifecycleScope?被取消时,所有与之关联的协程都会被清除。
等待 Animator 执行完成
我们再来看一个例子来加深理解,这次是等待?Animator?执行结束:
suspend fun Animator.awaitEnd() = suspendCancellableCoroutine<Unit> { cont ->
// 增加一个处理协程取消的监听器,如果协程被取消,// 同时执行动画监听器的 onAnimationCancel() 方法,取消动画 cont.invokeOnCancellation { cancel() }
addListener(object : AnimatorListenerAdapter() {private var endedSuccessfully = true
override fun onAnimationCancel(animation: Animator) {// 动画已经被取消,修改是否成功结束的标志 endedSuccessfully = false}
override fun onAnimationEnd(animation: Animator) {
// 为了在协程恢复后的不发生泄漏,需要确保移除监听 animation.removeListener(this)if (cont.isActive) {
// 如果协程仍处于活跃状态 if (endedSuccessfully) {// 并且动画正常结束,恢复协程 cont.resume(Unit)} else {// 否则动画被取消,同时取消协程 cont.cancel()}}}})}复制代码
这个方法支持两个维度的取消,我们可以分别取消动画或者协程:
#1:?在 Animator 运行的时候,协程被取消 。我们可以通过?invokeOnCancellation?回调方法来监听协程何时被取消,这能让我们同时取消动画。
#2:?在协程被挂起的时候,Animator 被取消 。我们通过?onAnimationCancel())?回调来监听动画被取消的事件,通过调用协程的 cancel() 方法来取消挂起的协程。
这就是使用挂起函数等待方法执行来封装回调的基本使用了。??
组合使用
到这里,您可能有这样的疑问,"看起来不错,但是我能从中收获什么呢?" 单独使用其中某个方法,并不会产生多大的作用,但是如果把它们组合起来,便能发挥巨大的威力。
下面是一个使用?Animator.awaitEnd()?来依次运行 3 个动画的示例:
viewLifecycleOwner.lifecycleScope.launch {ObjectAnimator.ofFloat(imageView, View.ALPHA, 0f, 1f).run {start()awaitEnd()}
ObjectAnimator.ofFloat(imageView, View.TRANSLATION_Y, 0f, 100f).run {start()awaitEnd()}
ObjectAnimator.ofFloat(imageView, View.TRANSLATION_X, -100f, 0f).run {start()awaitEnd()}}复制代码
这是一个很常见的使用案例,您可以把这些动画放进?AnimatorSet?中来实现同样的效果。
但是这里使用的方法适用于不同类型的异步操作:?我们使用一个?ValueAnimator,一个?RecyclerView?的平滑滚动,以及一个?Animator?来举例:
viewLifecycleOwner.lifecycleScope.launch {// #1: ValueAnimatorimageView.animate().run {alpha(0
f)start()awaitEnd()}
// #2: RecyclerView smooth scrollrecyclerView.run {smoothScrollToPosition(10)// 该方法和其他方法类似,等待当前的滑动完成,我们不需要刻意关注实现// 代码可以在文末的引用中找到 awaitScrollEnd()}
// #3: ObjectAnimatorObjectAnimator.ofFloat(textView, View.TRANSLATION_X, -100f, 0f).run {start()awaitEnd()}}复制代码
试着用?AnimatorSet?实现一下吧??!如果不用协程,那就意味着我们要监听每一个操作,在回调中执行下一个操作,这回调层级想想都可怕。
通过把不同的异步操作转换为协程的挂起函数,我们获得了简洁明了地编排它们的能力。
我们还可以更进一步...
**如果我们希望?ValueAnimator?和平滑滚动同时开始,然后在两者都完成之后启动?ObjectAnimator,该怎么做呢?**那么在使用了协程之后,我们可以使用?async()?来并发地执行我们的代码:
viewLifecycleOwner.lifecycleScope.launch {val anim1 = async {imageView.animate().run {alpha(0f)start()awaitEnd()}}
val scroll = async {recyclerView.run {smoothScrollToPosition(10)awaitScrollEnd()}}
// 等待以上两个操作全部完成 anim1.await()scroll.await()
// 此时,anim1 和滑动都完成了,我们开始执行 ObjectAnimatorObjectAnimator.ofFloat(textView, View.TRANSLATION_X, -100f, 0f).run {start()awaitEnd()}}复制代码
但是如果您还想让滚动延迟执行怎么办呢? (类似?Animator.startDelay)?方法) 那么使用协程也有很好的实现,我们可以用?delay()?方法:
viewLifecycleOwner.lifecycleScope.launch {val anim1 = async {// ...}
val scroll = async {// 我们希望在 anim1 完成后,延迟 200ms 执行滚动 delay(200)
recyclerView.run {smoothScrollToPosition(10)awaitScrollEnd()}
评论