From 7aaf45f50e0d7c1d5e9115cb692c936b744b0331 Mon Sep 17 00:00:00 2001 From: ColbyCabrera Date: Wed, 27 May 2026 00:12:03 -0400 Subject: [PATCH 1/2] ui: animate download progress in SharedDecksDownloadScreen - Implemented `animateFloatAsState` to smooth out transitions in the download progress indicator. - Configured the animation using a `SpringSpec` with low stiffness and a low bouncy damping ratio. - Updated `CircularProgressIndicator` to use the animated progress value instead of the raw percentage. --- .../anki/shareddeck/SharedDecksDownloadScreen.kt | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/shareddeck/SharedDecksDownloadScreen.kt b/AnkiDroid/src/main/java/com/ichi2/anki/shareddeck/SharedDecksDownloadScreen.kt index e024ba95c0e3..7335ca59bd43 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/shareddeck/SharedDecksDownloadScreen.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/shareddeck/SharedDecksDownloadScreen.kt @@ -7,6 +7,9 @@ package com.ichi2.anki.shareddeck import android.text.format.DateUtils import android.text.format.Formatter import androidx.activity.compose.BackHandler +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.SpringSpec +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -255,12 +258,22 @@ private fun CircularProgressSection( sizeRangeText: String, modifier: Modifier = Modifier, ) { + val animatedProgress by animateFloatAsState( + targetValue = (percent / 100f).coerceIn(0f, 1f), + animationSpec = + SpringSpec( + dampingRatio = Spring.DampingRatioLowBouncy, + stiffness = Spring.StiffnessLow, + ), + label = "DownloadProgress", + ) + Box( modifier = modifier.fillMaxWidth(), contentAlignment = Alignment.Center, ) { CircularProgressIndicator( - progress = { (percent / 100f).coerceIn(0f, 1f) }, + progress = { animatedProgress }, modifier = Modifier.size(ProgressRingSize), strokeWidth = ProgressRingStroke, trackColor = MaterialTheme.colorScheme.surfaceVariant, From a112694107615511c03447906bc6249a5150e6f8 Mon Sep 17 00:00:00 2001 From: ColbyCabrera Date: Wed, 27 May 2026 11:21:53 -0400 Subject: [PATCH 2/2] ui: animate percentage text updates in SharedDecksDownloadScreen - Wrapped the download percentage text in `AnimatedContent` to animate transitions between value changes. - Defined a custom `transitionSpec` using a combination of fade and scale animations with specific `tween` durations. - Configured the animation to use a subtle scale effect (0.92x) for a smoother visual "pop" during updates. --- .../shareddeck/SharedDecksDownloadScreen.kt | 66 +++++++++++++------ 1 file changed, 45 insertions(+), 21 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/shareddeck/SharedDecksDownloadScreen.kt b/AnkiDroid/src/main/java/com/ichi2/anki/shareddeck/SharedDecksDownloadScreen.kt index 7335ca59bd43..a75607ae88eb 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/shareddeck/SharedDecksDownloadScreen.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/shareddeck/SharedDecksDownloadScreen.kt @@ -7,9 +7,16 @@ package com.ichi2.anki.shareddeck import android.text.format.DateUtils import android.text.format.Formatter import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedContent import androidx.compose.animation.core.Spring import androidx.compose.animation.core.SpringSpec import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.animation.togetherWith import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -319,30 +326,47 @@ private fun PercentageText( percent: Float, modifier: Modifier = Modifier, ) { - val displayText = remember(percent) { formatPercent(percent) } val baseStyle = MaterialTheme.typography.displayLarge - val annotated = - remember(displayText, baseStyle.fontSize) { - buildAnnotatedString { - append(displayText) - val firstPercent = displayText.indexOf('%') - if (firstPercent >= 0) { - addStyle( - SpanStyle(fontSize = baseStyle.fontSize * 0.6f), - firstPercent, - firstPercent + 1, - ) + + AnimatedContent(targetState = percent, label = "PercentTextAnimation", transitionSpec = { + ( + fadeIn(animationSpec = tween(220)) + + scaleIn( + initialScale = 0.92f, + animationSpec = tween(220), + ) + ).togetherWith( + fadeOut(animationSpec = tween(180)) + + scaleOut( + targetScale = 0.92f, + animationSpec = tween(180), + ), + ) + }) { targetPercent -> + val displayText = remember(targetPercent) { formatPercent(targetPercent) } + val annotated = + remember(displayText, baseStyle.fontSize) { + buildAnnotatedString { + append(displayText) + val firstPercent = displayText.indexOf('%') + if (firstPercent >= 0) { + addStyle( + SpanStyle(fontSize = baseStyle.fontSize * 0.6f), + firstPercent, + firstPercent + 1, + ) + } } } - } - Text( - text = annotated, - style = baseStyle, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onSurface, - textAlign = TextAlign.Center, - modifier = modifier, - ) + Text( + text = annotated, + style = baseStyle, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + modifier = modifier, + ) + } } private fun formatPercent(percent: Float): String {