0

I use AnimatedVisibility to animate deleting an item from a LazyColumn together with MutableTransitionState to catch the end of animation to delete this item from LazyColumn's list.

I composed a handy extension function for this:

@ExperimentalTransitionApi
fun MutableTransitionState<Boolean>.transitionState(): TransitionState =
         if (this.isIdle && this.currentState)  TransitionState.Visible
    else if (this.isIdle && !this.currentState) TransitionState.Invisible
    else if (!this.isIdle && this.currentState) TransitionState.Disappearing
    else TransitionState.Appearing


enum class TransitionState(private val whatever: Int) {
    Visible(1),
    Appearing(2),
    Invisible(-1),
    Disappearing(-2)
}

It is correct in a sense that it returns correct values (tested), but currentState appears to be false only initially, so I can't catch the only event I'm interested in - Invisible.

Here is my LazyColumn:

val items by viewModel.itemsFlow.collectAsState()

LazyColumn(
    verticalArrangement = Arrangement.spacedBy(10.dp),
    contentPadding = PaddingValues(horizontal = 15.dp, vertical = 15.dp)
) {
    items(items) { item ->
        val animationState = remember {
            MutableTransitionState(false) // The only time currentState is false!
        }.apply { targetState = true }
        AnimatedVisibility(
            visibleState = animationState,
            enter = slideInVertically() + fadeIn(),
            exit = slideOutHorizontally(
                targetOffsetX = { it*2 },
                animationSpec = tween(
                    durationMillis = 700
                )
            )
        ) {
            ItemCard(
                item = item,
                viewModel = viewModel,
                animationState = animationState
            )
        }
    }
}

My ItemCard has a Button that changes animationState.tagetValue to false, and the state is logged inside the card:

Card {
    Log.v("ANIMATION", "View ${item.name} is now ${animationState.transitionState().name}")
    Log.v("ANIMATION", "View ${item.name} has values: isIdle = ${animationState.isIdle}, currentState = ${animationState.currentState}")
        /*...*/
        Button(
            /*...*/
            onClick = {
                animationState.targetState = false
            }
        ) {/*...*/}
 }

My logs, quite unfortunately, are like this:

V/ANIMATION: View name is now Appearing
V/ANIMATION: View name has values: isIdle = false, currentState = false
V/ANIMATION: View name is now Appearing
V/ANIMATION: View name has values: isIdle = false, currentState = false
V/ANIMATION: View name is now Visible
V/ANIMATION: View name has values: isIdle = true, currentState = true
V/ANIMATION: View name is now Visible
V/ANIMATION: View name has values: isIdle = true, currentState = true
// After I click the button:
V/ANIMATION: View name is now Disappearing
V/ANIMATION: View name has values: isIdle = false, currentState = true
V/ANIMATION: View name is now Disappearing
V/ANIMATION: View name has values: isIdle = false, currentState = true
V/ANIMATION: View name is now Disappearing
V/ANIMATION: View name has values: isIdle = false, currentState = true

So where's the invisible state, i.e. false currentState? Have I done something wrong?

Phil Dukhov
  • 67,741
  • 15
  • 184
  • 220
Calamity
  • 700
  • 7
  • 23

2 Answers2

1

Each time you change state, it triggers related views recomposition. And you set animationState to true on each recomposition with .apply { targetState = true }.

Most likely you want to show it animated at the beginning, then you need to use LaunchedEffect: it will be called only once when the view appears.

val animationState = remember {
    MutableTransitionState(false)
}
LaunchedEffect(Unit) {
    animationState.targetState = true
}

Read more about recompositions in Thinking in Compose and about state in Compose in state documentation.

Phil Dukhov
  • 67,741
  • 15
  • 184
  • 220
  • I believe this is not the problem. The state emits correct values on `enter` animation either way, and does not emit `Invisible` state on `exit` finished. – Calamity Oct 17 '21 at 09:46
0

After some testing...

I tested this type of animation on a separate project and figured out that the AnimatedVisibility content gets disposed before the animationState emits its final value, so you can't possibly catch it from inside the view you are animating.

Following @Philip Dukhov advice, my LazyColumns item is now like this:

            val animationState by remember {
                mutableStateOf(MutableTransitionState(false))
            }
            LaunchedEffect(Unit) {
                animationState.targetState = true
            }
            when(animationState.transitionState()) {
                TransitionState.Invisible -> viewModel.deleteInvisibleItems()
                else -> {}
            }
            Log.v("ANIMATION", "View ${item.name} is now ${animationState.transitionState().name}")

This catches emitted values perfectly.

However, it has significant performance issues, which logcat shows:

I/Choreographer: Skipped 39 frames!  The application may be doing too much work on its main thread.
I/OpenGLRenderer: Davey! duration=992ms; Flags=0, IntendedVsync=189347804710717, Vsync=189348454710691, OldestInputEvent=9223372036854775807, NewestInputEvent=0, HandleInputStart=189348467819123, AnimationStart=189348467823342, PerformTraversalsStart=189348779946237, DrawStart=189348780275977, SyncQueued=189348792306499, SyncStart=189348792749051, IssueDrawCommandsStart=189348793026916, SwapBuffers=189348794352801, FrameCompleted=189348797854572, DequeueBufferDuration=181354, QueueBufferDuration=287916, GpuCompleted=188965731175579, 

And I can't really figure out how to avoid it.

Calamity
  • 700
  • 7
  • 23