Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Jetpack Compose list diffs animation

Is there a way to get an animation effect on a list (column/row) changes in Compose that looks something like recyclerview animations with setItemAnimator?

like image 290
Ilya E Avatar asked Nov 19 '20 19:11

Ilya E


People also ask

What is animation in jetpack compose?

Jetpack Compose: Animation Jetpack Compose provides powerful and extensible APIs that make it easy to implement various animations in your app's UI. This document describes how to use these APIs as well as which API to use depending on your animation scenario.

How do I display a collection of items in jetpack compose?

Many apps need to display collections of items. This document explains how you can efficiently do this in Jetpack Compose. If you know that your use case does not require any scrolling, you may wish to use a simple Column or Row (depending on the direction), and emit each item’s content by iterating over a list like so:

How to animate a single value in compose?

See updateTransition for the details about Transition. The animate*AsState functions are the simplest animation APIs in Compose for animating a single value. You only provide the end value (or target value), and the API starts animation from the current value to the specified value.

What is animationspec in compose?

AnimationSpec in Compose allows us to handle these in a unified manner. Duration-based AnimationSpec operations (such as tween or keyframes) use Easing to adjust an animation's fraction. This allows the animating value to speed up and slow down, rather than moving at a constant rate.


4 Answers

There is not currently a way to do this with LazyColumn/LazyRow. This is something that is likely to be added eventually (though as always with predictions about the future: no promises), but it's currently a lower priority than getting more fundamental features working.

Note: I work on the team that implemented these components. I'll update this answer if the situation changes.

like image 118
Ryan M Avatar answered Oct 23 '22 06:10

Ryan M


At the moment, you'll need to manage the enter/exit transition of the changed items explicitly. You could use AnimatedVisibility for that like this example.

like image 28
Doris Liu Avatar answered Oct 23 '22 07:10

Doris Liu


The Modifier API called Modifier.animateItemPlacement() was implemented and merged and will probably be released in an upcoming Compose version. Tweet: https://twitter.com/CatalinGhita4/status/1455500904690552836?s=20

like image 6
Ji Sungbin Avatar answered Oct 23 '22 05:10

Ji Sungbin


Here's an example for dealing with item additions/removals at least:


@ExperimentalAnimationApi
@Suppress("UpdateTransitionLabel", "TransitionPropertiesLabel")
@SuppressLint("ComposableNaming", "UnusedTransitionTargetStateParameter")
/**
 * @param state Use [updateAnimatedItemsState].
 */
inline fun <T> LazyListScope.animatedItemsIndexed(
    state: List<AnimatedItem<T>>,
    enterTransition: EnterTransition = expandVertically(),
    exitTransition: ExitTransition = shrinkVertically(),
    noinline key: ((item: T) -> Any)? = null,
    crossinline itemContent: @Composable LazyItemScope.(index: Int, item: T) -> Unit
) {
    items(
        state.size,
        if (key != null) { keyIndex: Int -> key(state[keyIndex].item) } else null
    ) { index ->

        val item = state[index]
        val visibility = item.visibility

        androidx.compose.runtime.key(key?.invoke(item.item)) {
            AnimatedVisibility(
                visibleState = visibility,
                enter = enterTransition,
                exit = exitTransition
            ) {
                itemContent(index, item.item)
            }
        }
    }
}

@Composable
fun <T> updateAnimatedItemsState(
    newList: List<T>
): State<List<AnimatedItem<T>>> {

    val state = remember { mutableStateOf(emptyList<AnimatedItem<T>>()) }
    LaunchedEffect(newList) {
        if (state.value == newList) {
            return@LaunchedEffect
        }
        val oldList = state.value.toList()

        val diffCb = object : DiffUtil.Callback() {
            override fun getOldListSize(): Int = oldList.size
            override fun getNewListSize(): Int = newList.size
            override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
                oldList[oldItemPosition].item == newList[newItemPosition]

            override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
                oldList[oldItemPosition].item == newList[newItemPosition]

        }
        val diffResult = calculateDiff(false, diffCb)
        val compositeList = oldList.toMutableList()

        diffResult.dispatchUpdatesTo(object : ListUpdateCallback {
            override fun onInserted(position: Int, count: Int) {
                for (i in 0 until count) {
                    val newItem = AnimatedItem(visibility = MutableTransitionState(false), newList[position + i])
                    newItem.visibility.targetState = true
                    compositeList.add(position + i, newItem)
                }
            }

            override fun onRemoved(position: Int, count: Int) {
                for (i in 0 until count) {
                    compositeList[position + i].visibility.targetState = false
                }
            }

            override fun onMoved(fromPosition: Int, toPosition: Int) {
                // not detecting moves.
            }

            override fun onChanged(position: Int, count: Int, payload: Any?) {
                // irrelevant with compose.
            }
        })
        if (state.value != compositeList) {
            state.value = compositeList
        }
        val initialAnimation = Animatable(1.0f)
        initialAnimation.animateTo(0f)
        state.value = state.value.filter { it.visibility.targetState }
    }

    return state
}

data class AnimatedItem<T>(
    val visibility: MutableTransitionState<Boolean>,
    val item: T,
) {

    override fun hashCode(): Int {
        return item?.hashCode() ?: 0
    }

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as AnimatedItem<*>

        if (item != other.item) return false

        return true
    }
}

suspend fun calculateDiff(
    detectMoves: Boolean = true,
    diffCb: DiffUtil.Callback
): DiffUtil.DiffResult {
    return withContext(Dispatchers.Unconfined) {
        DiffUtil.calculateDiff(diffCb, detectMoves)
    }
}
like image 3
David Liu Avatar answered Oct 23 '22 05:10

David Liu