Zoltan Demant
02/19/2025, 10:31 AMLazyColumn
with a regular Column
?
My layout consists of several cards, each with their own column that grows/shrinks. I get pretty far by just using Modifier.animateContentSize()
but if I remove the first item in the list, the second one just takes its spot immediately (followed by the card shrinking in size since there are fewer items).Skaldebane
02/19/2025, 1:31 PMSkaldebane
02/19/2025, 1:32 PMZoltan Demant
02/19/2025, 1:54 PMKashismails
02/21/2025, 3:55 PMSkaldebane
02/21/2025, 4:06 PMKashismails
02/21/2025, 4:10 PMZoltan Demant
02/21/2025, 4:11 PMKashismails
02/21/2025, 4:14 PMZoltan Demant
02/25/2025, 9:50 AMZoltan Demant
02/25/2025, 9:51 AM@Composable
fun Today(activity: Activity) {
val displayItems = remember { mutableStateListOf<DisplayItem>() }
val coroutineScope = rememberCoroutineScope()
// React to changes in the immutable list
LaunchedEffect(activity.entries) {
val currentEntries = activity.entries
val currentKeys = currentEntries.map { it.first to it.second }.toSet()
// Handle removals
displayItems.filter { it.plan to it.performance !in currentKeys }.forEach { item ->
coroutineScope.launch {
// Animate out (shrink and fade)
item.animation.animateTo(
targetValue = 0f,
animationSpec = tween(durationMillis = 300, easing = LinearEasing)
)
displayItems.remove(item) // Remove after animation
}
}
// Handle additions
currentEntries.forEach { (plan, performance) ->
if (displayItems.none { it.plan == plan && it.performance == performance }) {
val anim = Animatable(0f) // Start invisible
displayItems.add(DisplayItem(plan, performance, anim))
coroutineScope.launch {
// Animate in
anim.animateTo(
targetValue = 1f,
animationSpec = tween(durationMillis = 300, easing = LinearEasing)
)
}
}
}
}
Card(
modifier = Modifier.fillMaxWidth(),
content = {
Header {
Text("Today")
}
Column(
modifier = Modifier.animateContentSize() // Smoothly resize the Column
) {
displayItems.forEach { item ->
// Use key for stable identity
key(item.plan, item.performance) {
val scale by item.animation.asState() // Get animated value
if (scale > 0f) { // Only render if not fully animated out
Box(
modifier = Modifier.graphicsLayer {
scaleX = scale
scaleY = scale
alpha = scale // Fade out as it shrinks
}
) {
Pending(
plan = item.plan,
performance = item.performance
)
}
}
}
}
}
Add() // Hypothetical "Add" composable
}
)
}
// Placeholder composables (replace with your actual implementations)
@Composable
fun Card(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
Column(modifier = modifier.padding(16.dp)) { content() }
}
@Composable
fun Header(content: @Composable () -> Unit) {
content()
}
@Composable
fun Pending(plan: String, performance: String) {
Row(modifier = Modifier.padding(8.dp)) {
Text("$plan - $performance")
}
}
@Composable
fun Add() {
Button(onClick = { /* Add logic */ }) {
Text("Add")
}
}
data class Activity(val entries: List<Pair<String, String>>)
Skaldebane
02/25/2025, 9:31 PMAnimatedVisibility
, alongside with DisposableEffect
. The animated visibility uses the original list (not the diffed one), but the for each loop uses a diffed copy, where additions were reflected immediately, while removals were delayed (until onDispose
was called within the AnimatedVisibility
, signalling that the animation was over and the composable left the composition).
Their usage of Animatable
is quite nice there, makes the diffing code much simpler and all in one place, but the usage is more manual compared to AnimatedVisibility
(though it's probably more powerful that way!).
Thanks for sharing, I'll definitely experiment with this!Zoltan Demant
02/27/2025, 12:13 PM