So I read this great Medium post by Chris Banes here which explains how coroutines or specifically suspend functions can be used to coordinate animations without the "callback hell" that you can fall into when chaining animations together. I managed to get the onAnimationEnd
listener extension working as per his example but I can't seem to do the same for the onAnimationStart
listener, here is my awaitEnd
method
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()
}
}
}
})
}
In the code below I'm using this in an async block and awaiting first the animation to finish and then the coroutine to finish
val anim = async {
binding.splash.circleReveal(null, startAtX = x, startAtY = y).run {
start()
//...doStuff()
awaitEnd()
}
}
anim.await()
This works beautifully, adding logs to the correct functions shows me that everything is being called exactly as expected, now to add a started extension in the same way...
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()
}
}
}
})
}
and call it from the same async block after the start method (this may have something to do with things)
val anim = async {
binding.splash.circleReveal(null, startAtX = x, startAtY = y).run {
start()
started()
//...doStuff()
awaitEnd()
}
}
anim.await()
now what happens is the coroutine for started gets suspended but never resumes, if I add some logging statements I can see that it calls start()
, it then calls started()
but then doesn't go any further, obviously I've tried changing the order of operations to no avail can anyone see what I'm doing wrong here?
many thanks
EDIT
I also tried this for adding a sharedElementEnter transition but again it just will not work for me,
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)
}
and again trying to use the await method
viewLifecycleOwner.lifecycleScope.launch {
val sharedElementEnterTransitionAsync = async {
sharedElementEnterTransition = TransitionInflater.from(context)
.inflateTransition(R.transition.shared_element_transition)
(sharedElementEnterTransition as TransitionSet).awaitTransitionEnd()
}
sharedElementEnterTransitionAsync.await()
}
You are correct - coroutine is never resumed.
Why?
By the time you call started()
method animation has already started. It means onAnimationStart
defined in a listener within started()
will not be called (as it has been already called) placing the underlying coroutine in a forever waiting state.
The same will happen if you call started()
before start()
: underlying coroutine will be waiting for onAnimationStart
to be called but it will never happen because start()
method call is blocked by the coroutine created by started()
method.
That is almost a dead-lock.
Call start()
before started()
returns:
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()
}
Pass in a function (that optionally takes a CancellableContinuation<Unit>
argument):
suspend fun Animator.started(executeBeforeReturn: (CancellableContinuation<Unit>) -> Unit) = suspendCancellableCoroutine<Unit> { continuation ->
continuation.invokeOnCancellation { cancel() }
this.addListener(object : AnimatorListenerAdapter() {
...
})
executeBeforeReturn(continuation)
}
It will allow you to:
Example:
val anim = async {
ObjectAnimator.ofFloat(view, View.ALPHA, 0f, 1f).run {
started { continuation ->
if (anything) {
continuation.cancel()
}
start()
}
awaitEnd()
}
}
anim.await()