From 5919194471703c0b19b38ff92b505665e095bd44 Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Thu, 15 Jan 2026 23:42:52 +0500 Subject: [PATCH] feat: Optimize animations in OnboardingScreen and improve MessageInputBar visibility transitions --- .../messenger/ui/chats/ChatDetailScreen.kt | 4 +- .../ui/onboarding/OnboardingScreen.kt | 276 +++++++++--------- 2 files changed, 148 insertions(+), 132 deletions(-) diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt index 60af4ef..64348e4 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt @@ -2393,8 +2393,8 @@ private fun MessageInputBar( // REPLY PANEL AnimatedVisibility( visible = hasReply, - enter = fadeIn(tween(150)) + expandVertically(), - exit = fadeOut(tween(100)) + shrinkVertically() + enter = fadeIn(tween(100)) + expandVertically(animationSpec = tween(100)), + exit = fadeOut(tween(0)) + shrinkVertically(animationSpec = tween(0)) ) { Row( modifier = Modifier diff --git a/app/src/main/java/com/rosetta/messenger/ui/onboarding/OnboardingScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/onboarding/OnboardingScreen.kt index 291fc0f..484db3e 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/onboarding/OnboardingScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/onboarding/OnboardingScreen.kt @@ -246,19 +246,17 @@ fun OnboardingScreen( state = pagerState, modifier = Modifier .fillMaxWidth() - .height(150.dp), + .height(150.dp) + .graphicsLayer { + // Hardware acceleration for entire pager + compositingStrategy = androidx.compose.ui.graphics.CompositingStrategy.Offscreen + }, // Pre-load adjacent pages for smooth swiping beyondBoundsPageCount = 2, flingBehavior = PagerDefaults.flingBehavior( state = pagerState, - lowVelocityAnimationSpec = tween( - durationMillis = 250, - easing = FastOutSlowInEasing - ), - snapAnimationSpec = spring( - dampingRatio = Spring.DampingRatioNoBouncy, - stiffness = Spring.StiffnessMedium - ) + lowVelocityAnimationSpec = snap(), // Instant! + snapAnimationSpec = snap() // No animation! ) ) { page -> OnboardingPageContent( @@ -374,142 +372,160 @@ fun AnimatedRosettaLogo( bookComposition: Any?, modifier: Modifier = Modifier ) { - // Use derivedStateOf for optimized reads + // Use derivedStateOf for optimized reads - prevents unnecessary recompositions val currentPage by remember { derivedStateOf { pagerState.currentPage } } + // Pre-calculate all animation progress states ONCE (they stay in memory) + val ideaLottieComp = ideaComposition as? com.airbnb.lottie.LottieComposition + val moneyLottieComp = moneyComposition as? com.airbnb.lottie.LottieComposition + val lockLottieComp = lockComposition as? com.airbnb.lottie.LottieComposition + val bookLottieComp = bookComposition as? com.airbnb.lottie.LottieComposition + + // All animations are always "playing" but only visible one shows + val ideaProgress by animateLottieCompositionAsState( + composition = ideaLottieComp, + iterations = 1, + isPlaying = currentPage == 1, + speed = 1.5f, + restartOnPlay = true + ) + val moneyProgress by animateLottieCompositionAsState( + composition = moneyLottieComp, + iterations = 1, + isPlaying = currentPage == 2, + speed = 1.5f, + restartOnPlay = true + ) + val lockProgress by animateLottieCompositionAsState( + composition = lockLottieComp, + iterations = 1, + isPlaying = currentPage == 3, + speed = 1.5f, + restartOnPlay = true + ) + val bookProgress by animateLottieCompositionAsState( + composition = bookLottieComp, + iterations = 1, + isPlaying = currentPage == 4, + speed = 1.5f, + restartOnPlay = true + ) + + // Pulse animation for logo (always running, cheap) + val pulseScale by rememberInfiniteTransition(label = "pulse").animateFloat( + initialValue = 1f, + targetValue = 1.1f, + animationSpec = infiniteRepeatable( + animation = tween(800, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse + ), + label = "pulseScale" + ) + Box( modifier = modifier, contentAlignment = Alignment.Center ) { - // Rosetta icon (page 0) with pulse animation like splash screen - androidx.compose.animation.AnimatedVisibility( - visible = currentPage == 0, - enter = fadeIn(animationSpec = tween(200)), - exit = fadeOut(animationSpec = tween(200)) + // === PRE-RENDERED LAYERS - All always exist, just alpha changes === + + // Page 0: Rosetta Logo + Box( + modifier = Modifier + .fillMaxSize() + .graphicsLayer { + alpha = if (currentPage == 0) 1f else 0f + }, + contentAlignment = Alignment.Center ) { - Box(contentAlignment = Alignment.Center) { - val pulseScale by rememberInfiniteTransition(label = "pulse").animateFloat( - initialValue = 1f, - targetValue = 1.1f, - animationSpec = infiniteRepeatable( - animation = tween(800, easing = FastOutSlowInEasing), - repeatMode = RepeatMode.Reverse - ), - label = "pulseScale" - ) - - // Glow effect behind logo - Box( - modifier = Modifier - .size(180.dp) - .scale(pulseScale) - .background( - color = Color(0xFF54A9EB).copy(alpha = 0.2f), - shape = CircleShape - ) - ) - - // Main logo - Image( - painter = painterResource(id = R.drawable.rosetta_icon), - contentDescription = "Rosetta Logo", - modifier = Modifier - .size(150.dp) - .clip(CircleShape) - ) - } + // Glow effect + Box( + modifier = Modifier + .size(180.dp) + .scale(if (currentPage == 0) pulseScale else 1f) + .background( + color = Color(0xFF54A9EB).copy(alpha = 0.2f), + shape = CircleShape + ) + ) + // Main logo + Image( + painter = painterResource(id = R.drawable.rosetta_icon), + contentDescription = "Rosetta Logo", + modifier = Modifier + .size(150.dp) + .clip(CircleShape) + ) } - // Idea animation (page 1) - androidx.compose.animation.AnimatedVisibility( - visible = currentPage == 1, - enter = fadeIn(animationSpec = tween(200)), - exit = fadeOut(animationSpec = tween(200)) - ) { - ideaComposition?.let { comp -> - val lottieComp = comp as? com.airbnb.lottie.LottieComposition - val progress by animateLottieCompositionAsState( - composition = lottieComp, - iterations = 1, - isPlaying = true, - speed = 1.5f - ) - LottieAnimation( - composition = lottieComp, - progress = { progress }, - modifier = Modifier.fillMaxSize(), - maintainOriginalImageBounds = true - ) - } + // Page 1: Idea animation (always in memory!) + if (ideaLottieComp != null) { + LottieAnimation( + composition = ideaLottieComp, + progress = { ideaProgress }, + modifier = Modifier + .fillMaxSize() + .graphicsLayer { + alpha = if (currentPage == 1) 1f else 0f + // Hardware layer optimization + compositingStrategy = androidx.compose.ui.graphics.CompositingStrategy.Offscreen + // Disable clipping for performance + clip = false + }, + maintainOriginalImageBounds = true, + // Disable dynamic properties for max performance + enableMergePaths = false + ) } - // Money animation (page 2) - androidx.compose.animation.AnimatedVisibility( - visible = currentPage == 2, - enter = fadeIn(animationSpec = tween(200)), - exit = fadeOut(animationSpec = tween(200)) - ) { - moneyComposition?.let { comp -> - val lottieComp = comp as? com.airbnb.lottie.LottieComposition - val progress by animateLottieCompositionAsState( - composition = lottieComp, - iterations = 1, - isPlaying = true, - speed = 1.5f - ) - LottieAnimation( - composition = lottieComp, - progress = { progress }, - modifier = Modifier.fillMaxSize(), - maintainOriginalImageBounds = true - ) - } + // Page 2: Money animation + if (moneyLottieComp != null) { + LottieAnimation( + composition = moneyLottieComp, + progress = { moneyProgress }, + modifier = Modifier + .fillMaxSize() + .graphicsLayer { + alpha = if (currentPage == 2) 1f else 0f + compositingStrategy = androidx.compose.ui.graphics.CompositingStrategy.Offscreen + clip = false + }, + maintainOriginalImageBounds = true, + enableMergePaths = false + ) } - // Lock animation (page 3) - androidx.compose.animation.AnimatedVisibility( - visible = currentPage == 3, - enter = fadeIn(animationSpec = tween(200)), - exit = fadeOut(animationSpec = tween(200)) - ) { - lockComposition?.let { comp -> - val lottieComp = comp as? com.airbnb.lottie.LottieComposition - val progress by animateLottieCompositionAsState( - composition = lottieComp, - iterations = 1, - isPlaying = true, - speed = 1.5f - ) - LottieAnimation( - composition = lottieComp, - progress = { progress }, - modifier = Modifier.fillMaxSize(), - maintainOriginalImageBounds = true - ) - } + // Page 3: Lock animation + if (lockLottieComp != null) { + LottieAnimation( + composition = lockLottieComp, + progress = { lockProgress }, + modifier = Modifier + .fillMaxSize() + .graphicsLayer { + alpha = if (currentPage == 3) 1f else 0f + compositingStrategy = androidx.compose.ui.graphics.CompositingStrategy.Offscreen + clip = false + }, + maintainOriginalImageBounds = true, + enableMergePaths = false + ) } - // Book animation (page 4) - androidx.compose.animation.AnimatedVisibility( - visible = currentPage == 4, - enter = fadeIn(animationSpec = tween(200)), - exit = fadeOut(animationSpec = tween(200)) - ) { - bookComposition?.let { comp -> - val lottieComp = comp as? com.airbnb.lottie.LottieComposition - val progress by animateLottieCompositionAsState( - composition = lottieComp, - iterations = 1, - isPlaying = true, - speed = 1.5f - ) - LottieAnimation( - composition = lottieComp, - progress = { progress }, - modifier = Modifier.fillMaxSize(), - maintainOriginalImageBounds = true - ) - } + // Page 4: Book animation + if (bookLottieComp != null) { + LottieAnimation( + composition = bookLottieComp, + progress = { bookProgress }, + modifier = Modifier + .fillMaxSize() + .graphicsLayer { + alpha = if (currentPage == 4) 1f else 0f + compositingStrategy = androidx.compose.ui.graphics.CompositingStrategy.Offscreen + clip = false + }, + maintainOriginalImageBounds = true, + enableMergePaths = false + ) } } }