Im trying to properly apply a bottom window inset on the BottomNavigation composable. I have an edge to edge scaffold it looks like that:
When using:
modifier.navigationBarsPadding()
On the bottom navigation im getting the following:
I'm trying to achieve the following:
Scaffold(
modifier = Modifier
.background(brush = NanitColors.blueGradient),
topBar = {
topBar()
},
bottomBar = {
if (shouldShowBottomBar) {
BottomNavigationBar(navController)
}
},
scaffoldState = scaffoldState,
drawerContent = {
Drawer { route ->
scope.launch {
scaffoldState.drawerState.close()
}
navController.navigate(route.route) {
popUpTo(navController.graph.startDestinationId)
launchSingleTop = true
}
currentDestination = route
}
},
drawerGesturesEnabled = scaffoldState.drawerState.isOpen,
) { innerPadding ->
NavigationHost(navController = navController, modifier = Modifier.padding(innerPadding))
}
val topBar: #Composable () -> Unit = {
MainToolbar(
modifier = modifier.statusBarsPadding(),
title = title ?: "",
onMenuClicked = {
scope.launch {
scaffoldState.drawerState.open()
}
}
)
}
BottomNavigation(
modifier = modifier.navigationBarsPadding()
) {
items.forEach { item ->
BottomNavigationBarItem(
item = item,
isSelected = selectedItem?.route == item.route,
onSelectedChange = { onSelectedItemChange(item) }
)
}
}
You can use accompanist systemuicontroller to set color to navigation bar, use default color for other screens and add color just for the screen you need:
#Composable
fun AppTheme(
systemBarColor: Color = MaterialTheme.colors.background,
content: #Composable () -> Unit,
) {
...
SideEffect {
systemUiController.setNavigationBarColor(
color = systemBarColor,
darkIcons = useDarkIcons
)
}
}
I currently display a Bottom Sheet through a BottomSheetScaffold and want to collapse it when the user clicks outside the Bottom Sheet. Is there a way to detect the click outside of the Bottom Sheet?
This is the screen with my BottomSheetScaffold:
#ExperimentalMaterialApi
#ExperimentalMaterial3Api
#Composable
fun HomeScreen() {
val bottomSheetScaffoldState = rememberBottomSheetScaffoldState(
bottomSheetState = BottomSheetState(BottomSheetValue.Collapsed)
)
val coroutineScope = rememberCoroutineScope()
BottomSheetScaffold(
scaffoldState = bottomSheetScaffoldState,
sheetContent = {
Box(
Modifier
.fillMaxWidth()
.fillMaxHeight(0.9f)
) {
Text("Hello from Sheet")
}
},
sheetShape = RoundedCornerShape(
topStart = Spacing.l,
topEnd = Spacing.l
),
sheetPeekHeight = LocalConfiguration.current.screenHeightDp.dp * 0.15f,
sheetBackgroundColor = MaterialTheme.colorScheme.surface,
) {
Scaffold() {
Button(
onClick = {
coroutineScope.launch {
if (bottomSheetScaffoldState.bottomSheetState.isCollapsed) {
bottomSheetScaffoldState.bottomSheetState.expand()
} else {
bottomSheetScaffoldState.bottomSheetState.collapse()
}
}
},
) {
Text("Toggle Sheet")
}
}
}
}
This is a visualization of the area in which I want to detect the click if the Bottom Sheet is expanded.
You can add the pointerInput modifier with detectTapGestures to your Scaffold:
Scaffold( modifier =
Modifier.pointerInput(Unit) {
detectTapGestures(onTap = {
coroutineScope.launch {
if (bottomSheetScaffoldState.bottomSheetState.isCollapsed) {
bottomSheetScaffoldState.bottomSheetState.expand()
} else {
bottomSheetScaffoldState.bottomSheetState.collapse()
}
}
})
}){
//.....
}
I had the same problem using the BottomSheetScaffold, and I changed to the ModalBottomSheetLayout that already has that behavior by default.
I hope this could help anybody with the same problem
I have 2 screens which both have their own Scaffold and TopAppBar. When I navigate between them using the Jetpack Navigation Compose library, the app bar flashes. Why does it happen and how can I get rid of this?
Code:
Navigation:
#Composable
fun TodoNavHost(
navController: NavHostController,
modifier: Modifier = Modifier
) {
NavHost(
navController = navController,
startDestination = TodoScreen.TodoList.name,
modifier = modifier
) {
composable(TodoScreen.TodoList.name) {
TodoListScreen(
onTodoEditClicked = { todo ->
navController.navigate("${TodoScreen.AddEditTodo.name}?todoId=${todo.id}")
},
onFabAddNewTodoClicked = {
navController.navigate(TodoScreen.AddEditTodo.name)
}
)
}
composable(
"${TodoScreen.AddEditTodo.name}?todoId={todoId}",
arguments = listOf(
navArgument("todoId") {
type = NavType.LongType
defaultValue = -1L
}
)
) {
AddEditTodoScreen(
onNavigateUp = {
navController.popBackStack()
},
onNavigateBackWithResult = { result ->
navController.navigate(TodoScreen.TodoList.name)
}
)
}
}
}
Todo list screen Scaffold with TopAppBar:
#Composable
fun TodoListBody(
todos: List<Todo>,
todoExpandedStates: Map<Long, Boolean>,
onTodoItemClicked: (Todo) -> Unit,
onTodoCheckedChanged: (Todo, Boolean) -> Unit,
onTodoEditClicked: (Todo) -> Unit,
onFabAddNewTodoClicked: () -> Unit,
onDeleteAllCompletedConfirmed: () -> Unit,
modifier: Modifier = Modifier,
errorSnackbarMessage: String = "",
errorSnackbarShown: Boolean = false
) {
var menuExpanded by remember { mutableStateOf(false) }
var showDeleteAllCompletedConfirmationDialog by rememberSaveable { mutableStateOf(false) }
Scaffold(
modifier,
topBar = {
TopAppBar(
title = { Text("My Todos") },
actions = {
IconButton(
onClick = { menuExpanded = !menuExpanded },
modifier = Modifier.semantics {
contentDescription = "Options Menu"
}
) {
Icon(Icons.Default.MoreVert, contentDescription = "Show menu")
}
DropdownMenu(
expanded = menuExpanded,
onDismissRequest = { menuExpanded = false }) {
DropdownMenuItem(
onClick = {
showDeleteAllCompletedConfirmationDialog = true
menuExpanded = false
},
modifier = Modifier.semantics {
contentDescription = "Option Delete All Completed"
}) {
Text("Delete all completed")
}
}
}
)
},
[...]
Add/edit screen Scaffold with TopAppBar:
#Composable
fun AddEditTodoBody(
todo: Todo?,
todoTitle: String,
setTitle: (String) -> Unit,
todoImportance: Boolean,
setImportance: (Boolean) -> Unit,
onSaveClick: () -> Unit,
onNavigateUp: () -> Unit,
modifier: Modifier = Modifier
) {
Scaffold(
modifier,
topBar = {
TopAppBar(
title = { Text(todo?.let { "Edit Todo" } ?: "Add Todo") },
actions = {
IconButton(onClick = onSaveClick) {
Icon(Icons.Default.Save, contentDescription = "Save Todo")
}
},
navigationIcon = {
IconButton(onClick = onNavigateUp) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
}
}
)
},
) { innerPadding ->
BodyContent(
todoTitle = todoTitle,
setTitle = setTitle,
todoImportance = todoImportance,
setImportance = setImportance,
modifier = Modifier.padding(innerPadding)
)
}
}
The flashing is caused by the default cross-fade animation in more recent versions of the navigation-compose library. The only way to get rid of it right now (without downgrading the dependency) is by using the Accompanist animation library:
implementation "com.google.accompanist:accompanist-navigation-animation:0.20.0"
And then replace the normal NavHost with Accompanist's AnimatedNavHost, replace rememberNavController() with rememberAnimatedNavController() and disable the transitions animations:
AnimatedNavHost(
navController = navController,
startDestination = bottomNavDestinations[0].fullRoute,
enterTransition = { _, _ -> EnterTransition.None },
exitTransition = { _, _ -> ExitTransition.None },
popEnterTransition = { _, _ -> EnterTransition.None },
popExitTransition = { _, _ -> ExitTransition.None },
modifier = modifier,
) {
[...}
}
I think I found an easy solution for that issue (works on Compose version 1.4.0).
My setup - blinking
All of my screens have their own toolbar wrapped in the scaffold:
// Some Composable screnn
Scaffold(
topBar = { TopAppBar(...) }
) {
ScreenContent()
}
Main activity which holds the nav host is defined like that:
// Activity with NavHost
setContent {
AppTheme {
NavHost(...) { }
}
}
Solution - no blinking!
Wrap you NavHost in activity in a Surface:
setContent {
AppTheme {
Surface {
NavHost(...) { }
}
}
}
Rest of the screens stay the same. No blinking and transition animation between destinations is almost the same like it was with fragments (subtle fade in/fade out). So far I haven't found any negative side effects of that.
It is the expected behaviour. You are constructing two separate app bars for both the screens so they are bound to flash. This is not the correct way. The correct way would be to actually put the scaffold in your main activity and place the NavHost as it's content. If you wish to modify the app bar, create variables to hold state. Then modify them from the Composables. Ideally, store then in a viewmodel. That is how it is done in compose. Through variables.
Thanks
I got the same issue having a "scaffold-per-screen" architecture. What helped, to my surprise, was lowering androidx.navigation:navigation-compose version to 2.4.0-alpha04.
With the newer library implementation "com.google.accompanist:accompanist-navigation-animation:0.24.1-alpha"
you need to have the AnimatedNavHost like this
AnimatedNavHost(
navController = navController,
startDestination = BottomNavDestinations.TimerScreen.route,
enterTransition = { EnterTransition.None },
exitTransition = { ExitTransition.None },
popEnterTransition = { EnterTransition.None },
popExitTransition = { ExitTransition.None },
modifier = Modifier.padding(innerPadding)
Also
Replace rememberNavController() with rememberAnimatedNavController()
Replace NavHost with AnimatedNavHost
Replace import androidx.navigation.compose.navigation with import com.google.accompanist.navigation.animation.navigation
Replace import androidx.navigation.compose.composable with import com.google.accompanist.navigation.animation.composable
In order not to blink (or to slide if you have AnimatedNavHost) you should put the TopAppBar in the activity and outside the NavHost, otherwise the TopAppBar is just part of the screen and makes transitions like every other screen element:
// Activity with your navigation host
setContent {
MyAppTheme {
Scaffold(
topBar = { TopAppBar(...) }
) { padding ->
TodoNavHost(padding, ...) { }
}
}
}
From the Scaffold containing the TopAppBar comes the padding parameter, that you should pass to the NavHost and use it in the screen like you have done in your example
Alternative to removing Animation you can change animations for example:
#Composable
private fun ScreenContent() {
val navController = rememberAnimatedNavController()
val springSpec = spring<IntOffset>(dampingRatio = Spring.DampingRatioMediumBouncy)
val tweenSpec = tween<IntOffset>(durationMillis = 2000, easing = CubicBezierEasing(0.08f, 0.93f, 0.68f, 1.27f))
...
) { innerPadding ->
AnimatedNavHost(
navController = navController,
startDestination = BottomNavDestinations.TimerScreen.route,
enterTransition = { slideInHorizontally(initialOffsetX = { 1000 }, animationSpec = springSpec) },
exitTransition = { slideOutHorizontally(targetOffsetX = { -1000 }, animationSpec = springSpec) },
popEnterTransition = { slideInHorizontally(initialOffsetX = { 1000 }, animationSpec = tweenSpec) },
popExitTransition = { slideOutHorizontally(targetOffsetX = { -1000 }, animationSpec = tweenSpec) },
modifier = Modifier.padding(innerPadding)
) {}
I'm trying to design a layout using Compose that consists of:
TopAppBar
Body (content)
BottomAppBar
A Bottom Sheet that represents a menu when clicked (Modal Bottom Sheet)
-------TopAppBar-------
------MainContent------
------BottomAppBar-----
----ModalBottomSheet---
Compose offers 3 components:
Scaffold
BottomSheetScaffold
ModalBottomSheetLayout
Scaffold has no bottom sheet property
BottomSheetScaffold has no BottomAppBar property
ModalBottomSheetLayout has only content and sheetContent
Which of these components should I combine and in what **structure** to achieve what I want?
Scaffold(
topBar = { TopBar() },
content = { innerPadding -> Body(innerPadding) },
bottomAppbar = { BottomAppBar() }
)
ModalBottomSheetLayout(
sheetState = rememberModalBottomSheetState(
initialValue = ModalBottomSheetValue.Hidden
),
sheetContent = { SheetContent() },
)
BottomSheetScaffold(
scaffoldState = ...,
sheetContent = { SheetContent() },
content = { ScreenContent() },
)
You can use something like:
val bottomState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
ModalBottomSheetLayout(
sheetState = bottomState,
sheetContent = {
//. sheetContent
}
) {
Scaffold(
scaffoldState = scaffoldState,
topBar = {
TopAppBar(
title = {
Text(text = "TopAppBar")
}
)
},
bottomBar = {
BottomAppBar(modifier = Modifier) {
IconButton(
onClick = {
coroutineScope.launch { bottomState.show() }
}
) {
Icon(Icons.Filled.Menu, contentDescription = "Localized description")
}
}
},
content = { innerPadding ->
//...main content
}
)
}
I want to know what is Scaffold in jetpack compose with a BottomAppBar example can anyone help me
Scaffold
Scaffold allows you to implement a UI with the basic Material Design layout structure. Scaffold provides slots for the most common top-level Material components, such as TopAppBar, BottomAppBar, FloatingActionButton, and Drawer.
Something like:
val scaffoldState = rememberScaffoldState()
// Create a coroutineScope for the animation
val coroutineScope = rememberCoroutineScope()
Scaffold(
scaffoldState = scaffoldState,
drawerContent = { Text("Drawer content") },
bottomBar = {
BottomAppBar(cutoutShape = CircleShape) {
IconButton(
onClick = {
coroutineScope.launch { scaffoldState.drawerState.open() }
}
) {
Icon(Icons.Filled.Menu, contentDescription = "....")
}
}
},
floatingActionButton = {
ExtendedFloatingActionButton(
text = { Text("Action") },
onClick = { /* .... */ }
)
},
floatingActionButtonPosition = FabPosition.Center,
isFloatingActionButtonDocked = true,
content = { innerPadding ->
//....
}
)