【问题标题】:Replace animation callbacks with Kotlin cancellable coroutine extensions (Suspending over views)用 Kotlin 可取消的协程扩展替换动画回调(暂停视图)
【发布时间】:2021-02-01 02:20:29
【问题描述】:

因此,我阅读了 Chris Banes here 的这篇很棒的 Medium 帖子,其中解释了如何使用协程或特别挂起函数来协调动画,而不会在将动画链接在一起时陷入“回调地狱”。我设法让onAnimationEnd 侦听器扩展按照他的示例工作,但我似乎无法为onAnimationStart 侦听器做同样的事情,这是我的awaitEnd 方法

suspend fun Animator.awaitEnd() = suspendCancellableCoroutine<Unit> { continuation ->

    continuation.invokeOnCancellation { cancel() }

    this.addListener(object : AnimatorListenerAdapter() {
        private var endedSuccessfully = true

        override fun onAnimationCancel(animation: Animator) {

            endedSuccessfully = false
        }

        override fun onAnimationEnd(animation: Animator) {

            animation.removeListener(this)

            if (continuation.isActive) {
                // If the coroutine is still active...
                if (endedSuccessfully) {
                    // ...and the Animator ended successfully, resume the coroutine
                    continuation.resume(Unit)
                } else {
                    // ...and the Animator was cancelled, cancel the coroutine too
                    continuation.cancel()
                }
            }
        }
    })

}

在下面的代码中,我在异步块中使用它,首先等待动画完成,然后等待协程完成

val anim = async {
    binding.splash.circleReveal(null, startAtX = x, startAtY = y).run {
        start()
        //...doStuff()
        awaitEnd()
    }
}
anim.await()

这很好用,将日志添加到正确的函数向我表明,一切都被完全按预期调用,现在以相同的方式添加一个启动的扩展......

suspend fun Animator.started() = suspendCancellableCoroutine<Unit> { continuation ->

    continuation.invokeOnCancellation { cancel() }

    this.addListener(object : AnimatorListenerAdapter() {

        private var endedSuccessfully = true

        override fun onAnimationCancel(animation: Animator?) {
            endedSuccessfully = false
        }

        override fun onAnimationStart(animation: Animator?) {
            Log.d("DETAIL", "Animator.started() onAnimationStart")

            animation?.removeListener(this)

            if (continuation.isActive) {
                // If the coroutine is still active...
                if (endedSuccessfully) {
                    // ...and the Animator ended successfully, resume the coroutine
                    continuation.resume(Unit)
                } else {
                    // ...and the Animator was cancelled, cancel the coroutine too
                    continuation.cancel()
                }
            }
        }
    })
}

并在启动方法之后从同一个异步块中调用它(这可能与事情有关)

val anim = async {
    binding.splash.circleReveal(null, startAtX = x, startAtY = y).run {
        start()
        started()
        //...doStuff()
        awaitEnd()
    }
}
anim.await()

现在发生的情况是,启动的协程被暂停但从未恢复,如果我添加一些日志记录语句,我可以看到它调用start(),然后它调用started(),但随后不再继续,显然我'已经尝试更改操作顺序无济于事,谁能看到我在这里做错了什么?

非常感谢

编辑

我也试过这个来添加一个 sharedElementEnter 过渡,但它再次对我不起作用,

suspend fun TransitionSet.awaitTransitionEnd() = suspendCancellableCoroutine<Unit> { continuation ->

    val listener = object : TransitionListenerAdapter() {
        private var endedSuccessfully = true

        override fun onTransitionCancel(transition: Transition) {
            super.onTransitionCancel(transition)
            endedSuccessfully = false
        }

        override fun onTransitionEnd(transition: Transition) {
            super.onTransitionEnd(transition)
            Log.d("DETAIL","enterTransition onTransitionEnd")
            transition.removeListener(this)

            if (continuation.isActive) {
                // If the coroutine is still active...
                if (endedSuccessfully) {
                    // ...and the Animator ended successfully, resume the coroutine
                    continuation.resume(Unit)
                } else {
                    // ...and the Animator was cancelled, cancel the coroutine too
                    continuation.cancel()
                }
            }
        }
    }
    continuation.invokeOnCancellation { removeListener(listener) }
    this.addListener(listener)

}

再次尝试使用 await 方法

viewLifecycleOwner.lifecycleScope.launch {
    val sharedElementEnterTransitionAsync = async {
        sharedElementEnterTransition = TransitionInflater.from(context)
            .inflateTransition(R.transition.shared_element_transition)
        (sharedElementEnterTransition as TransitionSet).awaitTransitionEnd()
    }
    sharedElementEnterTransitionAsync.await()
}

【问题讨论】:

    标签: android kotlin android-animation extension-methods kotlin-coroutines


    【解决方案1】:

    你是对的 - 协程永远不会恢复。

    为什么?

    当你调用started() 方法时动画已经开始了。这意味着在started() 内的侦听器中定义的onAnimationStart 将不会被调用(因为它已经被调用过)将底层协程置于永远等待状态。

    如果你在start() 之前调用started(),也会发生同样的情况:底层协程将等待onAnimationStart 被调用,但它永远不会发生,因为start() 方法调用被@987654328 创建的协程阻塞@方法。

    这几乎是一个死锁。

    解决方案 1

    started() 返回之前调用start()

    suspend fun Animator.started() = suspendCancellableCoroutine<Unit> { continuation ->
    
        continuation.invokeOnCancellation { cancel() }
    
        this.addListener(object : AnimatorListenerAdapter() {
            ...
        })
    
        // Before the coroutine is even returned we should start the animation
        start()
    }
    

    解决方案 2

    传入一个函数(可选地接受CancellableContinuation&lt;Unit&gt; 参数):

    suspend fun Animator.started(executeBeforeReturn: (CancellableContinuation<Unit>) -> Unit) = suspendCancellableCoroutine<Unit> { continuation ->
    
        continuation.invokeOnCancellation { cancel() }
    
        this.addListener(object : AnimatorListenerAdapter() {
            ...
        })
        executeBeforeReturn(continuation)
    }
    

    它将允许您:

    • 在开始动画之前使用协程(例如取消);
    • 避开锁。

    例子:

    val anim = async { 
        ObjectAnimator.ofFloat(view, View.ALPHA, 0f, 1f).run { 
            started { continuation ->
                if (anything) {
                    continuation.cancel()
                }
                start()
            }
            awaitEnd()
        }
    }
    anim.await()
    

    【讨论】:

    • 这行得通,非常感谢现在使用了解决方案 1,但解决方案 2 可能有助于解决我遇到的另一个问题,我已经接受了您的回答,很可能会奖励您
    • @martinseal1987,我很高兴它有帮助!欢迎提出任何问题。
    • 另一个问题实际上是对使用它进行共享元素转换的问题的编辑,问题是我必须启动我无法从该范围调用的延迟转换,因此传入 lambda 非常很好,再次感谢
    • 想想我对这个大声笑有点生气了,几乎每个听众现在都是一个扩展,阅读起来更加干净,删除了很多异步块,因为我认为它们是不必要的
    • @martinseal1987 “更清晰地阅读删除了很多异步块” - 很棒。只是提醒一下 - 不要过于复杂。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-12-06
    • 1970-01-01
    • 2021-10-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多