compose: Play short animation on removing items

Compose makes it easy to animate items. Play a short animation when
items are removed from downloads and favourites.

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
Aayush Gupta
2025-09-22 15:49:18 +08:00
parent e8a1e18750
commit a7a2d2f180
4 changed files with 136 additions and 72 deletions

View File

@@ -36,7 +36,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -53,8 +54,11 @@ import com.aurora.store.R
import com.aurora.store.compose.composables.app.AnimatedAppIconComposable
import com.aurora.store.compose.preview.AppPreviewProvider
import com.aurora.store.compose.preview.coilPreviewProvider
import com.aurora.store.data.model.DownloadStatus
import com.aurora.store.data.room.download.Download
import com.aurora.store.util.CommonUtil.getETAString
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.util.Date
/**
@@ -78,16 +82,20 @@ fun DownloadComposable(
val speed = "${Formatter.formatShortFileSize(LocalContext.current, download.speed)}/s"
val eta = getETAString(LocalContext.current, download.timeRemaining)
var isExpanded by rememberSaveable { mutableStateOf(false) }
val coroutineScope = rememberCoroutineScope()
var isVisible by remember { mutableStateOf(true) }
var isExpanded by remember { mutableStateOf(false) }
Column(
modifier = Modifier
.clickable(onClick = onClick)
.padding(
horizontal = dimensionResource(R.dimen.padding_medium),
vertical = dimensionResource(R.dimen.padding_small)
)
) {
fun requestClear() {
coroutineScope.launch {
isVisible = false
delay(300) // Let the animation play
onClear()
}
}
@Composable
fun SetupDownload() {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
@@ -100,7 +108,8 @@ fun DownloadComposable(
progress = download.progress.toFloat()
)
Column(
modifier = Modifier.padding(horizontal = dimensionResource(R.dimen.margin_small)),
modifier = Modifier
.padding(horizontal = dimensionResource(R.dimen.margin_small)),
) {
Text(
text = download.displayName,
@@ -112,10 +121,10 @@ fun DownloadComposable(
text = stringResource(download.downloadStatus.localized),
style = MaterialTheme.typography.bodySmall
)
AnimatedContent(targetState = download.isRunning) { isRunning ->
AnimatedContent(targetState = download.downloadStatus) { status ->
Text(
style = MaterialTheme.typography.bodySmall,
text = if (isRunning) {
text = if (status in DownloadStatus.running) {
"$progress$speed$eta"
} else {
DateUtils.getRelativeTimeSpanString(
@@ -142,7 +151,7 @@ fun DownloadComposable(
}
else -> {
SplitButtonDefaults.LeadingButton(onClick = onClear) {
SplitButtonDefaults.LeadingButton(onClick = { requestClear() }) {
Icon(
painter = painterResource(R.drawable.ic_delete_forever),
contentDescription = stringResource(R.string.action_clear)
@@ -168,7 +177,10 @@ fun DownloadComposable(
}
)
}
}
@Composable
fun SetupExpandedMenu() {
AnimatedVisibility(
visible = isExpanded,
enter = slideInVertically() + expandVertically() + fadeIn(),
@@ -209,6 +221,23 @@ fun DownloadComposable(
}
}
}
AnimatedVisibility(
visible = isVisible,
exit = shrinkVertically() + fadeOut()
) {
Column(
modifier = modifier
.clickable(onClick = onClick)
.padding(
horizontal = dimensionResource(R.dimen.padding_medium),
vertical = dimensionResource(R.dimen.padding_small)
)
) {
SetupDownload()
SetupExpandedMenu()
}
}
}
@Preview(showBackground = true)

View File

@@ -6,6 +6,9 @@
package com.aurora.store.compose.composables
import android.text.format.DateUtils
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -20,6 +23,11 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -40,6 +48,8 @@ import com.aurora.store.R
import com.aurora.store.compose.preview.AppPreviewProvider
import com.aurora.store.compose.preview.coilPreviewProvider
import com.aurora.store.data.room.favourite.Favourite
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
/**
* Composable to display a favourite app in a list
@@ -55,60 +65,76 @@ fun FavouriteComposable(
onClick: () -> Unit = {},
onClear: () -> Unit = {}
) {
Row(
modifier = modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(
horizontal = dimensionResource(R.dimen.padding_medium),
vertical = dimensionResource(R.dimen.padding_xsmall)
),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
val coroutineScope = rememberCoroutineScope()
var isVisible by remember { mutableStateOf(true) }
fun requestClear() {
coroutineScope.launch {
isVisible = false
delay(300) // Let the animation play
onClear()
}
}
AnimatedVisibility(
visible = isVisible,
exit = shrinkVertically() + fadeOut()
) {
Row(modifier = Modifier.weight(1F)) {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(favourite.iconURL)
.crossfade(true)
.build(),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.requiredSize(dimensionResource(R.dimen.icon_size_medium))
.clip(RoundedCornerShape(dimensionResource(R.dimen.radius_medium)))
)
Column(
modifier = Modifier.padding(horizontal = dimensionResource(R.dimen.margin_small))
) {
Text(
text = favourite.displayName,
style = MaterialTheme.typography.bodyMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
Row(
modifier = modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(
horizontal = dimensionResource(R.dimen.padding_medium),
vertical = dimensionResource(R.dimen.padding_xsmall)
),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Row(modifier = Modifier.weight(1F)) {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(favourite.iconURL)
.crossfade(true)
.build(),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.requiredSize(dimensionResource(R.dimen.icon_size_medium))
.clip(RoundedCornerShape(dimensionResource(R.dimen.radius_medium)))
)
Text(
text = favourite.packageName,
style = MaterialTheme.typography.bodySmall,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = DateUtils.formatDateTime(
LocalContext.current,
favourite.added,
DateUtils.FORMAT_SHOW_DATE
),
style = MaterialTheme.typography.bodySmall
Column(
modifier = Modifier.padding(horizontal = dimensionResource(R.dimen.margin_small))
) {
Text(
text = favourite.displayName,
style = MaterialTheme.typography.bodyMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = favourite.packageName,
style = MaterialTheme.typography.bodySmall,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = DateUtils.formatDateTime(
LocalContext.current,
favourite.added,
DateUtils.FORMAT_SHOW_DATE
),
style = MaterialTheme.typography.bodySmall
)
}
}
IconButton(onClick = { requestClear() }) {
Icon(
painter = painterResource(R.drawable.ic_favorite_checked),
contentDescription = stringResource(R.string.action_favourite)
)
}
}
IconButton(onClick = onClear) {
Icon(
painter = painterResource(R.drawable.ic_favorite_checked),
contentDescription = stringResource(R.string.action_favourite)
)
}
}
}

View File

@@ -151,6 +151,7 @@ private fun ScreenContent(
) { index ->
downloads[index]?.let { download ->
DownloadComposable(
modifier = Modifier.animateItem(),
download = download,
onClick = { onNavigateToAppDetails(download.packageName) },
onClear = { onClear(download) },

View File

@@ -13,6 +13,10 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
@@ -96,6 +100,13 @@ private fun ScreenContent(
onImportFavourites: () -> Unit = {},
onExportFavourites: () -> Unit = {}
) {
/*
* For some reason paging3 frequently out-of-nowhere invalidates the list which causes
* the loading animation to play again even if the keys are same causing a glitching effect.
*
* Save the initial loading state to make sure we don't replay the loading animation again.
*/
var initialLoad by rememberSaveable { mutableStateOf(true) }
@Composable
fun SetupMenu() {
@@ -122,18 +133,14 @@ private fun ScreenContent(
.fillMaxSize()
.padding(vertical = dimensionResource(R.dimen.padding_medium))
) {
when (favourites.loadState.refresh) {
is LoadState.Loading -> ProgressComposable()
is LoadState.Error -> {
ErrorComposable(
modifier = Modifier.padding(paddingValues),
icon = painterResource(R.drawable.ic_disclaimer),
message = stringResource(R.string.error)
)
when {
favourites.loadState.refresh is LoadState.Loading && initialLoad -> {
ProgressComposable()
}
else -> {
initialLoad = false
if (favourites.itemCount == 0) {
ErrorComposable(
modifier = Modifier.padding(paddingValues),
@@ -148,6 +155,7 @@ private fun ScreenContent(
) { index ->
favourites[index]?.let { favourite ->
FavouriteComposable(
modifier = Modifier.animateItem(),
favourite = favourite,
onClick = { onNavigateToAppDetails(favourite.packageName) },
onClear = { onRemoveFavourite(favourite.packageName) }