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
?
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.
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:
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.
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.
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.
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.
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
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)
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With