UI: navigation with more natural animated transitions

This commit is contained in:
Han Yin 2025-04-14 07:46:30 -07:00
parent 511df35704
commit d60bba9b8f
2 changed files with 110 additions and 37 deletions

View File

@ -24,13 +24,13 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.example.llama.revamp.engine.InferenceEngine import com.example.llama.revamp.engine.InferenceEngine
import com.example.llama.revamp.navigation.AppDestinations import com.example.llama.revamp.navigation.AppDestinations
import com.example.llama.revamp.navigation.NavigationActions import com.example.llama.revamp.navigation.NavigationActions
import com.example.llama.revamp.ui.components.AnimatedNavHost
import com.example.llama.revamp.ui.components.AppNavigationDrawer import com.example.llama.revamp.ui.components.AppNavigationDrawer
import com.example.llama.revamp.ui.components.UnloadModelConfirmationDialog import com.example.llama.revamp.ui.components.UnloadModelConfirmationDialog
import com.example.llama.revamp.ui.screens.BenchmarkScreen import com.example.llama.revamp.ui.screens.BenchmarkScreen
@ -70,6 +70,13 @@ fun AppContent(
val lifecycleOwner = LocalLifecycleOwner.current val lifecycleOwner = LocalLifecycleOwner.current
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
// LLM Inference engine status
val engineState by mainVewModel.engineState.collectAsState()
val isModelLoading = engineState is InferenceEngine.State.LoadingModel
|| engineState is InferenceEngine.State.ProcessingSystemPrompt
val isModelLoaded = engineState !is InferenceEngine.State.Uninitialized
&& engineState !is InferenceEngine.State.LibraryLoaded
// Navigation // Navigation
val navController = rememberNavController() val navController = rememberNavController()
val navigationActions = remember(navController) { NavigationActions(navController) } val navigationActions = remember(navController) { NavigationActions(navController) }
@ -78,13 +85,12 @@ fun AppContent(
derivedStateOf { navBackStackEntry?.destination?.route ?: "" } derivedStateOf { navBackStackEntry?.destination?.route ?: "" }
} }
var pendingNavigation by remember { mutableStateOf<(() -> Unit)?>(null) } var pendingNavigation by remember { mutableStateOf<(() -> Unit)?>(null) }
LaunchedEffect(navController) {
// LLM Inference engine status navController.addOnDestinationChangedListener { _, destination, _ ->
val engineState by mainVewModel.engineState.collectAsState() // Log navigation for debugging
val isModelLoading = engineState is InferenceEngine.State.LoadingModel println("Navigation: ${destination.route}")
|| engineState is InferenceEngine.State.ProcessingSystemPrompt }
val isModelLoaded = engineState !is InferenceEngine.State.Uninitialized }
&& engineState !is InferenceEngine.State.LibraryLoaded
// Determine if current route requires model unloading // Determine if current route requires model unloading
val routeNeedsModelUnloading by remember(currentRoute) { val routeNeedsModelUnloading by remember(currentRoute) {
@ -94,7 +100,6 @@ fun AppContent(
|| currentRoute == AppDestinations.MODEL_LOADING_ROUTE || currentRoute == AppDestinations.MODEL_LOADING_ROUTE
} }
} }
// Model unloading confirmation // Model unloading confirmation
var showUnloadDialog by remember { mutableStateOf(false) } var showUnloadDialog by remember { mutableStateOf(false) }
val handleBackWithModelCheck = { val handleBackWithModelCheck = {
@ -111,6 +116,18 @@ fun AppContent(
} }
} }
// Determine if drawer gestures should be enabled based on route
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val drawerGesturesEnabled by remember(currentRoute, drawerState.currentValue) {
derivedStateOf {
// Always allow gesture dismissal when drawer is open
if (drawerState.currentValue == DrawerValue.Open) true
// Only enable drawer opening by gesture on these screens
else currentRoute == AppDestinations.MODEL_SELECTION_ROUTE
}
}
val openDrawer: () -> Unit = { coroutineScope.launch { drawerState.open() } }
// Register a system back handler for screens that need unload confirmation // Register a system back handler for screens that need unload confirmation
val backDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher val backDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
DisposableEffect(lifecycleOwner, backDispatcher, currentRoute, isModelLoaded) { DisposableEffect(lifecycleOwner, backDispatcher, currentRoute, isModelLoaded) {
@ -130,38 +147,14 @@ fun AppContent(
callback.remove() callback.remove()
} }
} }
// Added protection to handle Compose-based back navigation
// Determine if drawer gestures should be enabled based on route
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val drawerGesturesEnabled by remember(currentRoute, drawerState.currentValue) {
derivedStateOf {
// Always allow gesture dismissal when drawer is open
if (drawerState.currentValue == DrawerValue.Open) true
// Only enable drawer opening by gesture on these screens
else currentRoute == AppDestinations.MODEL_SELECTION_ROUTE
}
}
// Compose BackHandler for added protection (this handles Compose-based back navigation)
BackHandler( BackHandler(
enabled = routeNeedsModelUnloading && enabled = routeNeedsModelUnloading && isModelLoaded
isModelLoaded && && drawerState.currentValue == DrawerValue.Closed
drawerState.currentValue == DrawerValue.Closed
) { ) {
handleBackWithModelCheck() handleBackWithModelCheck()
} }
// Observe back button
LaunchedEffect(navController) {
navController.addOnDestinationChangedListener { _, destination, _ ->
// Log navigation for debugging
println("Navigation: ${destination.route}")
}
}
// Handle drawer state
val openDrawer: () -> Unit = { coroutineScope.launch { drawerState.open() } }
// Main Content with navigation drawer wrapper // Main Content with navigation drawer wrapper
AppNavigationDrawer( AppNavigationDrawer(
drawerState = drawerState, drawerState = drawerState,
@ -169,7 +162,7 @@ fun AppContent(
gesturesEnabled = drawerGesturesEnabled, gesturesEnabled = drawerGesturesEnabled,
currentRoute = currentRoute currentRoute = currentRoute
) { ) {
NavHost( AnimatedNavHost(
navController = navController, navController = navController,
startDestination = AppDestinations.MODEL_SELECTION_ROUTE startDestination = AppDestinations.MODEL_SELECTION_ROUTE
) { ) {

View File

@ -0,0 +1,80 @@
package com.example.llama.revamp.ui.components
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.tween
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.currentBackStackEntryAsState
@Composable
fun AnimatedNavHost(
navController: NavHostController,
startDestination: String,
modifier: Modifier = Modifier,
contentAlignment: Alignment = Alignment.Center,
route: String? = null,
builder: NavGraphBuilder.() -> Unit
) {
val currentNavBackStackEntry by navController.currentBackStackEntryAsState()
var previousNavBackStackEntry by remember { mutableStateOf<NavBackStackEntry?>(null) }
LaunchedEffect(currentNavBackStackEntry) {
previousNavBackStackEntry = currentNavBackStackEntry
}
NavHost(
navController = navController,
startDestination = startDestination,
modifier = modifier,
contentAlignment = contentAlignment,
route = route,
enterTransition = {
slideIntoContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Left,
animationSpec = tween(
durationMillis = 200,
easing = LinearOutSlowInEasing
)
)
},
exitTransition = {
slideOutOfContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Left,
animationSpec = tween(
durationMillis = 200,
easing = LinearOutSlowInEasing
)
)
},
popEnterTransition = {
slideIntoContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Right,
animationSpec = tween(
durationMillis = 200,
easing = LinearOutSlowInEasing
)
)
},
popExitTransition = {
slideOutOfContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Right,
animationSpec = tween(
durationMillis = 200,
easing = LinearOutSlowInEasing
)
)
},
builder = builder
)
}