From b1a32f513080d1b0f7ffcd458d4a62f6fa8f4c9d Mon Sep 17 00:00:00 2001 From: Honey Date: Sat, 25 Jul 2026 10:24:42 +0200 Subject: [PATCH] feat(android): directional tab transitions in the bottom nav MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tab switches slid in a fixed direction (and sometimes flipped between push/pop feel, so the direction felt random). Now the slide follows the target tab's position in the bottom bar: tapping a tab to the right slides in from the right, a tab to the left slides in from the left — applied across all four enter/exit/popEnter/popExit transitions so it's consistent regardless of how Compose classifies the move. Detail screen push/pop transitions are unchanged. Co-Authored-By: Claude Opus 4.8 --- .../com/novasonic/app/ui/AppNavigation.kt | 38 +++++++++++++++++-- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/android/app/src/main/kotlin/com/novasonic/app/ui/AppNavigation.kt b/android/app/src/main/kotlin/com/novasonic/app/ui/AppNavigation.kt index 929bb89..3019a27 100644 --- a/android/app/src/main/kotlin/com/novasonic/app/ui/AppNavigation.kt +++ b/android/app/src/main/kotlin/com/novasonic/app/ui/AppNavigation.kt @@ -53,6 +53,10 @@ private enum class TopLevel(val route: String, val icon: ImageVector, val label: private val NavBg = Color(0xF7080814) +// Position of a route among the top-level tabs (in bottom-bar order), or -1 if it isn't one. +// Used to slide tab switches toward the side the target tab sits on, instead of a push/pop feel. +private fun topLevelIndex(route: String?): Int = TopLevel.entries.indexOfFirst { it.route == route } + @Composable fun AppNavigation() { val context = LocalContext.current @@ -245,10 +249,36 @@ fun AppNavigation() { navController = navController, startDestination = TopLevel.HOME.route, modifier = Modifier.padding(innerPadding), - enterTransition = { slideInHorizontally(initialOffsetX = { it }, animationSpec = tween(280, easing = FastOutSlowInEasing)) + fadeIn(tween(200)) }, - exitTransition = { slideOutHorizontally(targetOffsetX = { -it / 3 }, animationSpec = tween(280, easing = FastOutSlowInEasing)) + fadeOut(tween(180)) }, - popEnterTransition = { slideInHorizontally(initialOffsetX = { -it / 3 }, animationSpec = tween(280, easing = FastOutSlowInEasing)) + fadeIn(tween(200)) }, - popExitTransition = { slideOutHorizontally(targetOffsetX = { it }, animationSpec = tween(280, easing = FastOutSlowInEasing)) + fadeOut(tween(180)) }, + // Direction of a tab switch: +1 if the target tab sits to the right of the current + // one, -1 if to the left, 0 if this isn't a tab-to-tab move (detail push/pop). + enterTransition = { + val f = topLevelIndex(initialState.destination.route) + val t = topLevelIndex(targetState.destination.route) + val dir = if (f >= 0 && t >= 0 && f != t) (if (t > f) 1 else -1) else 0 + if (dir != 0) slideInHorizontally(tween(280, easing = FastOutSlowInEasing)) { dir * it } + fadeIn(tween(200)) + else slideInHorizontally(tween(280, easing = FastOutSlowInEasing)) { it } + fadeIn(tween(200)) + }, + exitTransition = { + val f = topLevelIndex(initialState.destination.route) + val t = topLevelIndex(targetState.destination.route) + val dir = if (f >= 0 && t >= 0 && f != t) (if (t > f) 1 else -1) else 0 + if (dir != 0) slideOutHorizontally(tween(280, easing = FastOutSlowInEasing)) { -dir * it } + fadeOut(tween(180)) + else slideOutHorizontally(tween(280, easing = FastOutSlowInEasing)) { -it / 3 } + fadeOut(tween(180)) + }, + popEnterTransition = { + val f = topLevelIndex(initialState.destination.route) + val t = topLevelIndex(targetState.destination.route) + val dir = if (f >= 0 && t >= 0 && f != t) (if (t > f) 1 else -1) else 0 + if (dir != 0) slideInHorizontally(tween(280, easing = FastOutSlowInEasing)) { dir * it } + fadeIn(tween(200)) + else slideInHorizontally(tween(280, easing = FastOutSlowInEasing)) { -it / 3 } + fadeIn(tween(200)) + }, + popExitTransition = { + val f = topLevelIndex(initialState.destination.route) + val t = topLevelIndex(targetState.destination.route) + val dir = if (f >= 0 && t >= 0 && f != t) (if (t > f) 1 else -1) else 0 + if (dir != 0) slideOutHorizontally(tween(280, easing = FastOutSlowInEasing)) { -dir * it } + fadeOut(tween(180)) + else slideOutHorizontally(tween(280, easing = FastOutSlowInEasing)) { it } + fadeOut(tween(180)) + }, ) { composable(TopLevel.HOME.route) { HomeScreen(