I'm currently experiencing an issue with cards in my lazy list in the app I'm developing:
when the lazy list reloads the items (because I trigger some MutableStateFlow change and so the items in my list change), I have some weird flickering / clipping effect on the shadow of the cards (defined via elevation property on ElevatedCard in TaskCard.kt).
This is definitely related to the animateItem() in the Box's modifier, instruction
Box(modifier = Modifier.animateItem())
inside MainScreen.kt , since if I remove it, no flickering. The problem is that I need the animateItem for the removal animation of the card from the list after swipe to delete (you can see it in my other question)
At the same time, animateItem gives me the weird flickering artifact when the card list reloads, see attached gif:
<img src="https://i.sstatic.net/XIRImrfc.gif" width="300" />
Is there a solution to this?
code from my app:
MainScreen.kt
@Composable fun MainScreen( tasksInWeek: TasksInPeriod, selectedWeek: DateInterval, selectedDate: LocalDate, onDateEvent: (DateEvent) -> Unit, onTaskEvent: (TaskEvent) -> Unit, navigateTo: (route: Destinations) -> Unit, ) { val locale = ConfigurationCompat.getLocales(LocalConfiguration.current).get(0) val dates = selectedWeek.getDatesBetween() Scaffold( content = { paddingValues -> Column( modifier = Modifier.padding( start = Constants.screenPadding, end = Constants.screenPadding, top = paddingValues.calculateTopPadding() + 10.dp, bottom = paddingValues.calculateBottomPadding() ) ) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(9.dp), modifier = Modifier.weight(1f) ) { Text( text = SimpleDateFormat("MMM", locale) .format( Date.from( selectedDate.atStartOfDay(ZoneId.systemDefault()).toInstant() ) ) .toString().uppercase(Locale.getDefault()), fontWeight = FontWeight.SemiBold, fontSize = 12.sp, style = TextStyle( platformStyle = PlatformTextStyle( includeFontPadding = false ) ), modifier = Modifier.rotate(-90f) ) for (date in dates) CircularDayProgressBar( 0f, day = date, isSelectedDay = date == selectedDate, isCurrentDay = date == LocalDate.now(), strokeColor = MaterialTheme.colorScheme.primary, backgroundColor = MaterialTheme.colorScheme.surface, textColor = MaterialTheme.colorScheme.outline, onClick = { onDateEvent(DateEvent.ChangeSelectedDate(date)) } ) } Spacer(modifier = Modifier.weight(0.1f)) Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.weight(1f) ) { Text( text = "1/10", fontWeight = FontWeight.SemiBold, fontSize = 16.sp, ) Text( text = stringResource(R.string.done_tasks), fontWeight = FontWeight.Medium, fontSize = 16.sp, ) Spacer(modifier = Modifier.weight(1f)) Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.width(100.dp) ) { ElevatedButton( onClick = { onDateEvent(DateEvent.ChangeToPrevWeek) }, modifier = Modifier.size(width = 40.dp, height = 30.dp), contentPadding = PaddingValues(0.dp), shape = RoundedCornerShape(8.dp), elevation = ButtonDefaults.buttonElevation(defaultElevation = 2.dp) ) { Icon(Icons.Filled.ChevronLeft, contentDescription = null) } Spacer(modifier = Modifier.weight(1f)) ElevatedButton( onClick = { onDateEvent(DateEvent.ChangeToNextWeek) }, modifier = Modifier.size(width = 40.dp, height = 30.dp), contentPadding = PaddingValues(0.dp), shape = RoundedCornerShape(8.dp), elevation = ButtonDefaults.buttonElevation(defaultElevation = 2.dp) ) { Icon(Icons.Filled.ChevronRight, contentDescription = null) } } } Spacer(modifier = Modifier.weight(0.2f)) Box( modifier = Modifier.weight(10f), contentAlignment = Alignment.Center ) { LazyColumn( contentPadding = PaddingValues(bottom = 10.dp, top = 10.dp), verticalArrangement = Arrangement.spacedBy(32.dp), modifier = Modifier.fillMaxSize() ) { items( items = tasksInWeek.getForDate(selectedDate), key = { it.taskId.toString() + it.date.toString() } ) { task -> Box(modifier = Modifier.animateItem()) { SwipeToDeleteContainer( item = task, onDelete = { onTaskEvent(TaskEvent.DeleteTask(task)) } ) { TaskCard( taskCardDto = task, onCheckedChange = { id, done -> onTaskEvent( TaskEvent.SetDone( id, done, selectedDate ) ) } ) } } } } } } }, bottomBar = { BottomAppBar( modifier = Modifier.graphicsLayer { shadowElevation = 80f }, containerColor = MaterialTheme.colorScheme.background, contentColor = MaterialTheme.colorScheme.outline, actions = { IconButton(onClick = { /* do something */ }) { Icon( Icons.Outlined.Settings, contentDescription = "description" ) } IconButton(onClick = { /* do something */ }) { Icon( Icons.Outlined.Search, contentDescription = "description", ) } }, floatingActionButton = { FloatingActionButton( onClick = { navigateTo(Destinations.CreateTask) }, containerColor = MaterialTheme.colorScheme.primary, contentColor = MaterialTheme.colorScheme.background, elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation() ) { Icon(Icons.Filled.Add, "description") } } ) } ) }
TaskCard.kt
@Composable fun TaskCard( taskCardDto: TaskCardDto, onCheckedChange: (Long, Boolean) -> Unit ) { ElevatedCard( elevation = CardDefaults.cardElevation( defaultElevation = 6.dp ), modifier = Modifier .height(70.dp) .fillMaxWidth(), shape = Constants.cardShape ) { Row( modifier = Modifier .padding(vertical = 12.dp, horizontal = 20.dp) .fillMaxWidth() .fillMaxHeight(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween ) { Image( painter = painterResource(id = R.drawable.postit), contentDescription = "default task icon", modifier = Modifier.weight(0.7f) ) Text( text = taskCardDto.title, fontWeight = FontWeight.Medium, style = if (taskCardDto.done) { LocalTextStyle.current.copy(textDecoration = TextDecoration.LineThrough) } else LocalTextStyle.current.copy(), modifier = Modifier .weight(4f) .padding(horizontal = 20.dp) ) RoundedCornerCheckbox( isChecked = taskCardDto.done, onValueChange = { isChecked -> onCheckedChange(taskCardDto.taskId, isChecked) }, checkedColor = MaterialTheme.colorScheme.primary, uncheckedColor = Color.White ) } } }
SwipeToDeleteContainer.kt
@Composable fun <T> SwipeToDeleteContainer( item: T, onDelete: (T) -> Unit, content: @Composable (T) -> Unit ) { val state = rememberSwipeToDismissBoxState( initialValue = SwipeToDismissBoxValue.Settled, positionalThreshold = { distance -> distance * 0.5f }) LaunchedEffect(state.currentValue) { if (state.currentValue == SwipeToDismissBoxValue.EndToStart) { onDelete(item) } } SwipeToDismissBox( state = state, backgroundContent = { DeleteBackground(swipeDismissState = state) }, content = { content(item) }, enableDismissFromEndToStart = true, enableDismissFromStartToEnd = false ) } @Composable private fun DeleteBackground( swipeDismissState: SwipeToDismissBoxState ) { val color = if (swipeDismissState.dismissDirection == SwipeToDismissBoxValue.EndToStart) { Color.Red } else Color.Transparent Box( modifier = Modifier .clip(Constants.cardShape) .background(color) .padding(16.dp) .fillMaxSize(), contentAlignment = Alignment.CenterEnd ) { Icon( imageVector = Icons.Default.Delete, contentDescription = null, tint = Color.White ) } }
If you only need animations for item insertion and deletion, and don’t want animations on every list update, you can disable fade animations and keep only placement animation.
Using the default Modifier.animateItem() may cause flickering when combined with elevation/shadows, because fade animations introduce additional layer changes.
Instead, you can do:
Modifier.animateItem(
placementSpec = spring(
stiffness = Spring.StiffnessMediumLow,
visibilityThreshold = IntOffset.VisibilityThreshold,
),
fadeInSpec = null,
fadeOutSpec = null,
)
This approach limits animations to positional changes only, which is more stable in LazyList when using elevated composables.
Sychev Denis