Reduce padding in NavigationBar with Material 3 - android

I'm using the new Material 3 NavigationBar and NavigationBarItem components, I want the NavigationBar to be thinner, because the default one is too large. I want one similar to the one Gmail or Drive has (see picture at the end for the comparison). Making the icon smaller doesn't work, and neither changing all the available paddings (Icon, NavigationBar and NavigationBarItem).
This is the Composable code, if I change the NavigationBar heigh (using Modifier) then this happens:
I primarly want to remove the space between the label and the bottom, and the one between the top and the icon.
#Composable
fun MyAppBottomBar(navController: NavController, tabs: Array<MenuBottom>) {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route ?: MenuBottom.INICIO.route
val rutas = remember { MenuBottom.values().map { it.route } }
if (currentRoute in rutas) {
NavigationBar(containerColor = elevation01) {
tabs.forEachIndexed { index, item ->
NavigationBarItem(
selected = currentRoute == item.route,
onClick = {
if (item.route != currentRoute) {
navController.navigate(item.route) {
popUpTo(navController.graph.startDestinationId) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}
},
label = { Text(stringResource(id = item.title)) },
icon = {
if (item.route == currentRoute) {
Icon(item.selectedIcon, contentDescription = null, tint = Color.Black)
} else {
Icon(item.unselectedIcon, contentDescription = null)
}
},
colors = NavigationBarItemDefaults.colors(
selectedIconColor = Color.Black,
unselectedIconColor = Color.Black,
indicatorColor = Greenyellow,
selectedTextColor = Color.Black,
unselectedTextColor = Color.Black
)
)
}
}
}
}

NavigationBar does not use padding. It uses a fixed height. How do I know that? While working with MUI (which is a React implementation of Material design) I found that they use a fixed height, and margin: auto to center items vertically. Then I figured jetpack compose NavigationBar might do the same and the idea was right. So whats the solution?
NavigationBar(
modifier = Modifier.height(64.dp)
) {...}
Or change it to your taste. It will shrink and grow the space taken up.

This most likely has to do with the bottom navigation. The grey bar you see has its own padding by default.
Have a look at this documentation on how to remove those System intents.
Here's what you need to do in a nutshell:
implementation "com.google.accompanist:accompanist-insets:<version>"
Then in your MainActivity call WindowCompat.setDecorFitsSystemWindows(window, false) like this:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {...}
}
Now your App should use the entire Screen and your NavigationBar should have the same height as the one from Gmail.
To apply single ones of them again e.g. StatusBar call a modifier:
Modifier.statusBarsPadding()

Related

Scroll to bottom listener Jetpack Compose

I have a page that I want to be scrollable (mostly text only, no list). At the bottom of that page I have a disabled button, but when I reach the bottom of the page I want that button to activate. How can I do this with JetpackCompose Kotlin androidx.compose.ui version 1.3.2?
Much appreciation in advance!
I can't use ScrollableColumn, Scrollable, LazyColumn, LazyRow because of the compose library.
It's not clear why you can't use these components since they are all part of the Compose library. However, this can be done using Column composable with verticalScroll modifier.
val scrollState = rememberScrollState()
Column(Modifier.verticalScroll(scrollState)) {
//Text
}
State for enabling Button, false by default:
val isButtonEnabled by remember {mutableStateOf(false)}
ScrollState has canScrollForward Boolean property, false means we cannot scroll forward (we are the end of the list). Once we have false here, button should be enabled
LaunchedEffect(scrollState.canScrollForward) {
if (!scrollState.canScrollForward) isButtonEnabled = true
}
And button:
Button(enabled = isButtonEnabled, ...)
#Composable
override fun Build() {
val scrollState = rememberScrollState()
val isButtonEnabled by remember {mutableStateOf(false)}
LaunchedEffect(scrollState.canScrollForward) {
if (!scrollState.canScrollForward) isButtonEnabled = true
}
ScreenScaffold(
header = {
TopBar(
title = R.string.insurance_choose_insurance_travel,
onBack = { sendNavEffect.invoke(Effect.Navigation.OnBack) },
actions = {},
backgroundColor = white
)
},
page = {
Column(
Modifier
.fillMaxSize()
.padding(20.dp)
.verticalScroll(scrollState)
){
repeat(100){
Text(
"Text",
color = Color.Black
)
}
}
},
footer = {
Row(modifier = Modifier.padding(20.dp)) {
PrimaryActionButton(
text = R.string.accept_terms_and_conditions_sca.getString(),
buttonState = if(isButtonEnabled) ButtonState.Active else ButtonState.Disabled,
onClick = {}
)
}
})
}
}

How to prevent a jetpack compose scaffold from taking the whole height of the screen when it is shown in a dialog

I am having an android activity which has Base.Theme.AppCompat.Light.Dialog as a theme on tablets and therefore is shown in a dialog.
When I used XML layouts I could set a min height with wrap content for the layout so it would
Be at least x dp high when there was no or very little content.
High as the content when the content was higher than the min height
High as the screen when the content wouldnt fit inside the height of the screen, in that case I could scroll
Using a Jetpack Compose's Scaffold I am struggling recreating that behaviour. The dialog is alsways as high as the screen allows, even when there is no content.
I tried the following modifiers as a parameter to the scaffold:
Modifier.wrapContentHeight() - the dialog is still as high as the screen
Modifier.defaultMinSize(minHeight = 250.dp) - the dialog is still as high as the screen
Modifier.height(height = 250.dp) - now it is smaller but it is fixed to that size, not growing when there is more content
Modifier.requiredHeightIn(min = 250.dp, max = getMyScreenHeightinDp()) - the dialog is still as high as the screen
Since you can reduce the height of the scaffold with the .height(...) modifier I think it should also be possible to recreate a wrap content behaviour. But how?
Using requiredHeightIn does work, as tested in the code below. In this list, the height varies from 150.dp to 400.dp. If you only provide 10 items, it will be 150.dp. As you add more items, it increases in height but only to a maximumo of 400.dp.
However, there is a catch. The navigation drawer slides out from the left side of the screen and not the left side of the dialog. There are ways around that. You could replace the navigation drawer with your own custom drawer. I did this once and it can be done.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startActivity(intent)
setContent {
Dialog(
onDismissRequest = { },
properties = DialogProperties(dismissOnClickOutside = false)
) {
val scaffoldState = rememberScaffoldState()
val scope = rememberCoroutineScope()
Scaffold(
modifier = Modifier.requiredHeightIn(min = 150.dp, max = 400.dp),
scaffoldState = scaffoldState,
drawerContent = { Text("Drawer content") },
topBar = {
TopAppBar(
title = { Text("Simple Scaffold Screen") },
navigationIcon = {
IconButton(
onClick = {
scope.launch { scaffoldState.drawerState.open() }
}
) {
Icon(Icons.Filled.Menu, contentDescription = "Localized description")
}
}
)
},
floatingActionButtonPosition = FabPosition.End,
floatingActionButton = {
ExtendedFloatingActionButton(
text = { Text("Inc") },
onClick = { /* fab click handler */ }
)
},
content = { innerPadding ->
LazyColumn(contentPadding = innerPadding) {
items(count = 100) {
Text(it.toString())
}
}
}
)
}
}
}
}

Drop down menu clips/shrinks items Jetpack Compose

I am creating a dropdown menu where the items contain a text element and an icon (a spacer in between); but only the first text is shown completely. The icon is only visible when there is another item taking more space.
#Preview(showSystemUi = true)
#Composable
fun MyScreen() {
Box(Modifier.fillMaxSize(), Alignment.Center) {
Box() {
var expanded by remember { mutableStateOf(false) }
IconButton(onClick = { expanded = !expanded }) {
Icon(imageVector = Icons.Default.MoreVert, contentDescription = null)
}
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
MyMenuItem("item 1") // Icon visible
MyMenuItem("item 2") // Icon visible
MyMenuItem("item 3 long text") // Icon width shrunk to 0
MyMenuItem("item 4 long te") // Icon visible but shrunk
}
}
}
}
#Composable
fun MyMenuItem(text: String) {
DropdownMenuItem(onClick = { }) {
Text(text)
Spacer(modifier = Modifier.weight(1f))
Icon(imageVector = Icons.Default.Check, contentDescription = null) // <-- Icon
}
}
Note :
I have also tried using Row() and Surface() in place of DropdownMenuItem() but the result is similar.
Giving width to the MyMenuItem() works; but I want it to size itself automatically based on content.
Generally speaking, to create such a layout you just need to apply Modifier.weight(1f) to your Text.
You also need Modifier.width(IntrinsicSize.Max) for your Column to make the width equal to the widest item, but in your case it's already built into DropdownMenu.
But then this bug pops up, which doesn't allow you to properly size your MyMenuItem with Icon inside. Please put a star to draw more attention to it.
As a temporary solution until this bug is fixed, you can specify the size of the icon manually, like this:
// copied from the source code as it's internal
const val MaterialIconDimension = 24f
#Composable
fun MyMenuItem(text: String) {
DropdownMenuItem(
onClick = { }
) {
Text(text, Modifier.weight(1f))
Icon(
imageVector = Icons.Default.Check,
contentDescription = null,
modifier = Modifier.size(MaterialIconDimension.dp)
)
}
}
Result:

How to use Jetpack compose app bar backbutton

getActionBar().setDisplayHomeAsUpEnabled(true) this i was using for normal android appCp,pact activity to switch between two or more activity.can any one tell me how to do this in jetpack Compose ?
The other answer is correct for showing the back button. This is a slight alternative that uses TopAppBar composable instead.
I also ran into a similar issue. The main thing I wanted to solve was hiding the back button when you are at the root or if there is nothing left in backstack, since setDisplayHomeAsUpEnabled took care of that as long as you specified your parent.
Assuming you are using the nav controller with compose, you can do something like this
val navController = rememberNavController()
Scaffold(
topBar = {
TopAppBar(
title = { Text(text = "app bar title") },
navigationIcon = if (navController.previousBackStackEntry != null) {
{
IconButton(onClick = { navController.navigateUp() }) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = "Back"
)
}
}
} else {
null
}
)
},
content = {
// Body content
}
)
The key here is to set navigationIcon argument of TopAppBar to null when there is nothing in the back stack. This way the back arrow will be hidden when you are at the root and shown otherwise.
You can wrap your main content inside an Scaffold composable and use the topBar to add the back button and handle back action like this:
import androidx.compose.material.Scaffold
.
.
Scaffold(
topBar = {
Row {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = "Back",
modifier = Modifier
.padding(16.dp)
.clickable {
// Implement back action here
}
)
}
}
) {
BodyContent()
}
Both other solutions are fine and helped me refine my own variation that I have to extract because of usage on many screens.
#Composable
fun MyScaffold(#StringRes titleId: Int, upAvailable: Boolean, onUpClicked: () -> Unit, content: #Composable (PaddingValues) -> Unit) {
Scaffold(
topBar = {
TopAppBar(title = { MyText(textId = titleId) }, backgroundColor = Color.Black, navigationIcon = {
if (upAvailable) {
IconButton(onClick = { onUpClicked() }) {
Icon(imageVector = Icons.Filled.ArrowBack, contentDescription = "Back", tint = Color.White)
}
}
})
},
backgroundColor = Color.Transparent,
content = content
)
}
Where MyText is just my variant that accepts string res and has white text color.
Usage:
val isUpAvailable by viewModel.upAvailable.collectAsState()
MyScaffold(titleId = R.string.title, upAvailable = isUpAvailable, onUpClicked = { viewModel.navigateUp() }) {
// Content
}
Then my BaseViewModel provides upAvailable and navigateUp() via navigationManager dependency which handles navigation via navigationController:
if (destination.route == NAVIGATE_UP) { navController.navigateUp() }
...
// set on every navigation
navigationManager.setUpAvailable(navController.previousBackStackEntry != null)
Here a full Example of using compose app bar back button. https://github.com/pgatti86/toolbar-demo
val navController: NavHostController = rememberNavController()
val isBackButtonVisible by remember {
derivedStateOf {
navController.previousBackStackEntry != null
}
}
use derived state to show or hide your back button icon

How to implement BottomAppBar and BottomDrawer pattern using Android Jetpack Compose?

I'm building Android app with Jetpack Compose. Got stuck while trying to implement BottomAppBar with BottomDrawer pattern.
Bottom navigation drawers are modal drawers that are anchored to the bottom of the screen instead of the left or right edge. They are only used with bottom app bars. These drawers open upon tapping the navigation menu icon in the bottom app bar.
Description on material.io, and direct link to video.
I've tried using Scaffold, but it only supports side drawer. BottomDrawer appended to Scaffold content is displayed in content area and BottomDrawer doesn't cover BottomAppBar when open. Moving BottomDrawer after Scaffold function doesn't help either: BottomAppBar is covered by some invisible block and prevents clicking buttons.
I've also tried using BottomSheetScaffold, but it doesn't have BottomAppBar slot.
If Scaffold doesn't support this pattern, what would be correct way to implement it? Is it possible to extend Scaffold component? I fear that incorrect implementation from scratch might create issues later, when I'll try to implement navigation and snackbar.
I think the latest version of scaffold does have a bottom app bar parameter
They (Google Devs) invite you in the Jetpack Compose Layouts pathway to try adding other Material Design Components such as BottomNavigation or BottomDrawer to their respective Scaffold slots, and yet do not give you the solution.
BottomAppBar does have its own slot in Scaffold (i.e. bottomBar), but BottomDrawer does not - and seems to be designed exclusively for use with the BottomAppBar explicitly (see API documentation for the BottomDrawer).
At this point in the Jetpack Compose pathway, we've covered state hoisting, slots, modifiers, and have been thoroughly explained that from time to time we'll have to play around to see how best to stack and organize Composables - that they almost always have a naturally expressable way in which they work best that is practically intended.
Let me get us set up so that we are on the same page:
class MainActivity : ComponentActivity() {
#ExperimentalMaterialApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
LayoutsCodelabTheme {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
LayoutsCodelab()
}
}
}
}
}
That's the main activity calling our primary/core Composable. This is just like in the codelab with the exception of the #ExperimentalMaterialApi annotation.
Next is our primary/core Composable:
#ExperimentalMaterialApi
#Composable
fun LayoutsCodelab() {
val ( gesturesEnabled, toggleGesturesEnabled ) = remember { mutableStateOf( true ) }
val scope = rememberCoroutineScope()
val drawerState = rememberBottomDrawerState( BottomDrawerValue.Closed )
// BottomDrawer has to be the true core of our layout
BottomDrawer(
gesturesEnabled = gesturesEnabled,
drawerState = drawerState,
drawerContent = {
Button(
modifier = Modifier.align( Alignment.CenterHorizontally ).padding( top = 16.dp ),
onClick = { scope.launch { drawerState.close() } },
content = { Text( "Close Drawer" ) }
)
LazyColumn {
items( 25 ) {
ListItem(
text = { Text( "Item $it" ) },
icon = {
Icon(
Icons.Default.Favorite,
contentDescription = "Localized description"
)
}
)
}
}
},
// The API describes this member as "the content of the
// rest of the UI"
content = {
// So let's place the Scaffold here
Scaffold(
topBar = {
AppBarContent()
},
//drawerContent = { BottomBar() } // <-- Will implement a side drawer
bottomBar = {
BottomBarContent(
coroutineScope = scope,
drawerState = drawerState
)
},
) {
innerPadding ->
BodyContent( Modifier.padding( innerPadding ).fillMaxHeight() )
}
}
)
}
Here, we've leveraged the Scaffold exactly as the codelab in the compose pathway suggests we should. Notice my comment that drawerContent is an auto-implementation of the side-drawer. It's a rather nifty way to bypass directly using the [respective] Composable(s) (material design's modal drawer/sheet)! However, it won't work for our BottomDrawer. I think the API is experimental for BottomDrawer, because they'll be making changes to add support for it to Composables like Scaffold in the future.
I base that on how difficult it is to use the BottomDrawer, designed for use solely with BottomAppBar, with the Scaffold - which explicitly contains a slot for BottomAppBar.
To support BottomDrawer, we have to understand that it is an underlying layout controller that wraps the entire app's UI, preventing interaction with anything but its drawerContent when the drawer is open. This requires that it encompasses Scaffold, and that requires that we delegate necessary state control - to the BottomBarContent composable which wraps our BottomAppBar implementation:
#ExperimentalMaterialApi
#Composable
fun BottomBarContent( modifier: Modifier = Modifier, coroutineScope: CoroutineScope, drawerState: BottomDrawerState ) {
BottomAppBar{
// Leading icons should typically have a high content alpha
CompositionLocalProvider( LocalContentAlpha provides ContentAlpha.high ) {
IconButton(
onClick = {
coroutineScope.launch { drawerState.open() }
}
) {
Icon( Icons.Filled.Menu, contentDescription = "Localized description" )
}
}
// The actions should be at the end of the BottomAppBar. They use the default medium
// content alpha provided by BottomAppBar
Spacer( Modifier.weight( 1f, true ) )
IconButton( onClick = { /* doSomething() */ } ) {
Icon( Icons.Filled.Favorite, contentDescription = "Localized description" )
}
IconButton( onClick = { /* doSomething() */ } ) {
Icon( Icons.Filled.Favorite, contentDescription = "Localized description" )
}
}
}
The result shows us:
The TopAppBar at top,
The BottomAppBar at bottom,
Clicking the menu icon in the BottomAppBar opens our BottomDrawer, covering the BottomAppBar and entire content space appropriately while open.
The BottomDrawer is properly hidden, until either the above referenced button click - or gesture - is utilized to open the bottom drawer.
The menu icon in the BottomAppBar opens the drawer partway.
Gesture opens the bottom drawer partway with a quick short swipe, but as far as you guide it to otherwise.
You may have to do something as shown below..
Notice how the Scaffold is called inside the BottomDrawer().
It's confusing though how the documentation says "They (BottomDrawer) are only used with bottom app bars". It made me think I have to look for a BottomDrawer() slot inside Scaffold or that I have to call BottomDrawer() inside BottomAppBar(). In both cases, I experienced weird behaviours. This is how I worked around the issue. I hope it helps someone especially if you are attempting the code lab exercise in Module 5 of Layouts in Jetpack Compose from the Jetpack Compose course.
#ExperimentalMaterialApi
#Composable
fun MyApp() {
var selectedItem by rememberSaveable { mutableStateOf(1)}
BottomDrawer(
modifier = Modifier.background(MaterialTheme.colors.onPrimary),
drawerShape = Shapes.medium,
drawerContent = {
Column(Modifier.fillMaxWidth()) {
for(i in 1..6) {
when (i) {
1 -> Row(modifier = Modifier.clickable { }.padding(16.dp)){
Icon(imageVector = Icons.Rounded.Inbox, contentDescription = null)
Text(text = "Inbox")
}
2 -> Row(modifier = Modifier.clickable { }.padding(16.dp)){
Icon(imageVector = Icons.Rounded.Outbox, contentDescription = null)
Text(text = "Outbox")
}
3 -> Row(modifier = Modifier.clickable { }.padding(16.dp)){
Icon(imageVector = Icons.Rounded.Archive, contentDescription = null)
Text(text = "Archive")
}
}
}
}
},
gesturesEnabled = true
) {
Scaffold(
topBar = {
TopAppBar(
title = {
Text(text = "Learning Compose Layouts" )
},
actions = {
IconButton(onClick = { /*TODO*/ }) {
Icon(Icons.Filled.Favorite, contentDescription = null)
}
}
)
},
bottomBar = { BottomAppBar(cutoutShape = CircleShape, contentPadding = PaddingValues(0.dp)) {
for (item in 1..4) {
BottomNavigationItem(
modifier = Modifier.clipToBounds(),
selected = selectedItem == item ,
onClick = { selectedItem = item },
icon = {
when (item) {
1 -> { Icon(Icons.Rounded.MusicNote, contentDescription = null) }
2 -> { Icon(Icons.Rounded.BookmarkAdd, contentDescription = null) }
3 -> { Icon(Icons.Rounded.SportsBasketball, contentDescription = null) }
4 -> { Icon(Icons.Rounded.ShoppingCart, contentDescription = null) }
}
}
)
}
}
}
) { innerPadding -> BodyContent(
Modifier
.padding(innerPadding)
.padding(8.dp))
}
}
}

Categories

Resources