From 2e813feecd1eec7812ddf3296e597da53edc64e7 Mon Sep 17 00:00:00 2001 From: "ts-syyam.noor" Date: Tue, 30 Jun 2026 16:38:32 +0900 Subject: [PATCH] feat: add spring-bounce and title-fade animations on task completion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a task is marked done the check icon springs in with a medium-bouncy spring (scale 0→1) and the title fades to 45% alpha over 300 ms, giving clear visual feedback that the task has been resolved. Unchecking reverses both animations. Closes #48 --- .../home_screen/components/TaskComponent.kt | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/vishal2376/snaptick/presentation/home_screen/components/TaskComponent.kt b/app/src/main/java/com/vishal2376/snaptick/presentation/home_screen/components/TaskComponent.kt index eed67a56..34f03636 100755 --- a/app/src/main/java/com/vishal2376/snaptick/presentation/home_screen/components/TaskComponent.kt +++ b/app/src/main/java/com/vishal2376/snaptick/presentation/home_screen/components/TaskComponent.kt @@ -2,6 +2,9 @@ package com.vishal2376.snaptick.presentation.home_screen.components import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween import androidx.compose.ui.platform.LocalDensity import kotlinx.coroutines.coroutineScope @@ -76,6 +79,22 @@ fun TaskComponent( // Optimistic flip on tap; resets when upstream task id or isCompleted lands. var localCompleted by remember(task.id, task.isCompleted) { mutableStateOf(task.isCompleted) } + // Spring-bounce on the check icon: 0 → 1 when first completed, 1 → 0 when unchecked. + val checkScale by animateFloatAsState( + targetValue = if (localCompleted) 1f else 0f, + animationSpec = spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessMedium + ), + label = "checkScale" + ) + // Fade title slightly when done so it reads as "resolved". + val titleAlpha by animateFloatAsState( + targetValue = if (localCompleted) 0.45f else 1f, + animationSpec = tween(durationMillis = 300), + label = "titleAlpha" + ) + // Onboarding-style entrance: 28dp slide-up + fade, FastOutSlowInEasing, // matching WelcomePage.Staggered exactly. Caller signals scroll-in via a // negative animDelay so we snap to final state instead of running the tween; @@ -155,7 +174,9 @@ fun TaskComponent( painter = painterResource(id = R.drawable.ic_check_circle), contentDescription = null, tint = if (isSystemInDarkTheme()) LightGreen else DarkGreen, - modifier = Modifier.size(20.dp) + modifier = Modifier + .size(20.dp) + .graphicsLayer { scaleX = checkScale; scaleY = checkScale } ) } else { Box( @@ -182,7 +203,8 @@ fun TaskComponent( Text( modifier = Modifier .fillMaxWidth() - .basicMarquee(delayMillis = 1000), + .basicMarquee(delayMillis = 1000) + .graphicsLayer { alpha = titleAlpha }, text = task.title, style = taskTextStyle, fontWeight = FontWeight.Bold,