How can menu icons of Toolbar can be turned into overflow in Compose?
Scaffold(
topBar = {
TopAppBar(
title = {
Text(text = "LayoutsCodelab")
},
actions = {
IconButton(onClick = { /* doSomething() */ }) {
Icon(Icons.Filled.Favorite)
}
IconButton(onClick = { /* doSomething() */ }) {
Icon(Icons.Filled.Refresh)
}
IconButton(
onClick = { /* doSomething() */ }) {
Icon(Icons.Filled.Call)
}
}
)
},
bottomBar = {
BottomNavigationLayout()
}
) { innerPadding ->
PhotoCard(Modifier.padding(innerPadding))
}
I want only one of the icons in Toolbar menu to be visible while others to be added to overflow menu like done with xml using app:showAsAction="never"
<item
android:id="#+id/action_sign_out"
android:title="#string/toolbar_sign_out"
app:showAsAction="never"/>
You have to provide the OverFlowMenu yourself, e.g.:
#Preview
#Composable
fun PreviewOverflowMenu() {
OverflowMenuTest()
}
#Composable
fun OverflowMenuTest() {
var showMenu by remember { mutableStateOf(false) }
TopAppBar(
title = { Text("Title") },
actions = {
IconButton(onClick = { /*TODO*/ }) {
Icon(Icons.Default.Favorite)
}
IconButton(onClick = { showMenu = !showMenu }) {
Icon(Icons.Default.MoreVert)
}
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
DropdownMenuItem(onClick = { /*TODO*/ }) {
Icon(Icons.Filled.Refresh)
}
DropdownMenuItem(onClick = { /*TODO*/ }) {
Icon(Icons.Filled.Call)
}
}
}
)
}
Edit: Updated for Compose 1.0.0-beta08
I modified a bit #jns's answer to make it more modular and reusable.
This is the reusable OverflowMenu:
#Composable
fun OverflowMenu(content: #Composable () -> Unit) {
var showMenu by remember { mutableStateOf(false) }
IconButton(onClick = {
showMenu = !showMenu
}) {
Icon(
imageVector = Icons.Outlined.MoreVert,
contentDescription = stringResource(R.string.more),
)
}
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
content()
}
}
And this is how it is used inside the TopAppBar:
TopAppBar(
title = {
Text(text = stringResource(R.string.my_title))
},
actions = {
OverflowMenu {
DropdownMenuItem(onClick = { /*TODO*/ }) {
Text("Settings")
}
DropdownMenuItem(onClick = { /*TODO*/ }) {
Text("Bookmarks")
}
}
}
)
We can possibly add icons to DropDownMenuItems if desired. And these items can be extracted as reusable composables as well. If there are other action buttons that you want to show as iconified buttons on the menu(i.e. show as action), you should put them before the OverflowMenu.
TopAppBar(
title = {
Text(text = stringResource(R.string.bookmark))
},
actions = {
//This icon will be shown on the top bar, on the left of the overflow menu
IconButton(onClick = { /*TODO*/ }) {
Icon(Icons.Filled.FavoriteBorder, stringResource(R.string.cd_favorite_item))
}
OverflowMenu {
SettingsDropDownItem(onClick = { /*TODO*/ })
BookmarksDropDownItem(onClick = { /*TODO*/ })
}
}
)
.
#Composable
fun SettingsDropDownItem(onClick : () -> Unit) {
//Drop down menu item with an icon on its left
DropdownMenuItem(onClick = onClick) {
Icon(Icons.Filled.Settings,
contentDescription = stringResource(R.string.settings),
modifier = Modifier.size(24.dp))
Spacer(modifier = Modifier.width(8.dp))
Text(stringResource(R.string.settings))
}
}
#Composable
fun BookmarksDropDownItem(onClick : () -> Unit) {
//Drop down menu item with an icon on its left
DropdownMenuItem(onClick = onClick) {
Icon(painter = painterResource(R.drawable.ic_bookmark_filled),
contentDescription = stringResource(R.string.bookmark),
modifier = Modifier.size(24.dp))
Spacer(modifier = Modifier.width(8.dp))
Text(stringResource(R.string.bookmark))
}
}
Inspired by #jns's answer, I made an ActionMenu composable which takes a list of ActionItemSpec objects. and displays them with an overflow menu if necessary. I modelled the ActionItemSpec a bit like the old XML menu item entries, but added an onClick lambda.
It's used like this
#Preview
#Composable
fun PreviewActionMenu() {
val items = listOf(
ActionItemSpec("Call", Icons.Default.Call, ActionItemMode.ALWAYS_SHOW) {},
ActionItemSpec("Send", Icons.Default.Send, ActionItemMode.IF_ROOM) {},
ActionItemSpec("Email", Icons.Default.Email, ActionItemMode.IF_ROOM) {},
ActionItemSpec("Delete", Icons.Default.Delete, ActionItemMode.IF_ROOM) {},
)
TopAppBar(
title = { Text("App bar") },
navigationIcon = {
IconButton(onClick = {}) {
Icon(Icons.Default.Menu, "Menu")
}
},
actions = {
// show 3 icons including overflow
ActionMenu(items, defaultIconSpace = 3)
}
)
}
and the preview looks like this
Full pastebin is here: https://gist.github.com/MachFour/369ebb56a66e2f583ebfb988dda2decf
Related
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
)
}
}
How can I arrange the two inner BottomNav Items so that they are not so close to the "+" FAB?
I tried surrounding the forEach which displays the Items with a Row and use the Arrangement modifier like so:
Row(horizontalArrangement = Arrangement.SpaceBetween) { //Not working :(
items.forEach { item ->
BottomNavigationItem(
icon = { Icon(painterResource(id = item.icon), contentDescription = item.title) },
label = { Text(text = item.title) },
selectedContentColor = Color.White,
unselectedContentColor = Color.White.copy(0.4f),
alwaysShowLabel = true,
selected = currentRoute == item.route,
onClick = {
navController.navigate(item.route) {
navController.graph.startDestinationRoute?.let { route ->
popUpTo(route) {
saveState = true
}
}
launchSingleTop = true
restoreState = true
}
}
)
}
}
Unfortunately thats not working
Arrangement.SpaceBetween works as expected - it adds a spacer between items:
Place children such that they are spaced evenly across the main axis, without free space before the first child or after the last child. Visually: 1##2##3
You need to let your Row know about FAB location. You can add a spacer with Modifier.weight in the middle of your row, for example like this:
items.forEachIndexed { i, item ->
if (i == items.count() / 2) {
Spacer(Modifier.weight(1f))
}
BottomNavigationItem(
// ...
You can use BottomAppBar & give it cutoutShape with a dummy item in the middle. It would give you your desired results.
Output:
Code Sample:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AppTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
BottomBarWithFabDem()
}
}
}
}
}
val items = listOf(
Screen.PickUp,
Screen.Explore,
Screen.Camera,
Screen.Favorites,
Screen.Profile
)
sealed class Screen(val route: String?, val title: String?, val icon: ImageVector?) {
object PickUp : Screen("pickup", "PickUp", Icons.Default.ShoppingCart)
object Explore : Screen("explore", "Explore", Icons.Default.Info)
object Camera : Screen("camera", null, null)
object Favorites : Screen("favorites", "Fav", Icons.Default.Favorite)
object Profile : Screen("profile", "Profile", Icons.Default.Person)
}
#Composable
fun BottomBarWithFabDem() {
val navController = rememberNavController()
Scaffold(
bottomBar = {
BottomNav(navController)
},
floatingActionButtonPosition = FabPosition.Center,
isFloatingActionButtonDocked = true,
floatingActionButton = {
FloatingActionButton(
shape = CircleShape,
onClick = {
Screen.Camera.route?.let {
navController.navigate(it) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}
Screen.Camera.route?.let { navController.navigate(it) }
},
contentColor = Color.White
) {
Icon(imageVector = Icons.Filled.Add, contentDescription = "Add icon")
}
}
) {
MainScreenNavigation(navController)
}
}
#Composable
fun MainScreenNavigation(navController: NavHostController) {
NavHost(navController, startDestination = Screen.Profile.route!!) {
composable(Screen.Profile.route) {}
composable(Screen.Explore.route!!) {}
composable(Screen.Favorites.route!!) {}
composable(Screen.PickUp.route!!) {}
composable(Screen.Camera.route!!) {}
}
}
#Composable
fun BottomNav(navController: NavController) {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination
BottomAppBar(cutoutShape = CircleShape, modifier = Modifier.height(64.dp)) {
Row {
items.forEachIndexed { index, it ->
if (index != 2) {
// Main item
BottomNavigationItem(
icon = {
it.icon?.let {
Icon(
imageVector = it,
contentDescription = "",
modifier = Modifier.size(35.dp),
tint = Color.White
)
}
},
label = {
it.title?.let {
Text(
text = it,
color = Color.White
)
}
},
selected = currentRoute?.hierarchy?.any { it.route == it.route } == true,
selectedContentColor = Color(R.color.purple_700),
unselectedContentColor = Color.White.copy(alpha = 0.4f),
onClick = {}
)
} else {
// placeholder for center fab
BottomNavigationItem(
icon = {},
label = { },
selected = false,
onClick = { },
enabled = false
)
}
}
}
}
}
What i am trying to do is to close the TopBar dropdown menu after clicking the dropdown item. It can be easily done, if i am putting the dropdown items directly inside the dropdown menu. But here i am trying to separate it as a composable for readability.
Here is my TopAppBar
#Composable
fun TopBar(
scope: CoroutineScope,
scaffoldState: ScaffoldState,
event: (AdminLaunchEvents) -> Unit,
navController: NavHostController
) {
val openDialog = remember { mutableStateOf(false) }
TopAppBar(
title = {
Text(text = "Main App Admin Area", fontSize = 18.sp)
},
actions = {
OverflowMenu() {
SettingsDropDownItem(onClick = {})
ModeDropDownItem(onClick = {})
LogoutDropDownItem(onClick = {
openDialog.value = true
})
}
},
backgroundColor = MaterialTheme.colors.primary,
contentColor = Color.White
)
if (openDialog.value) {
LogOutComponent(openDialog = openDialog, event = event,navController = navController)
}
}
And this is the OverFlowMenu composable which contains the DropDown Menu
#Composable
fun OverflowMenu(content: #Composable () -> Unit) {
var showMenu by remember { mutableStateOf(false) }
IconButton(onClick = {
showMenu = !showMenu
}) {
Icon(
imageVector = Icons.Outlined.MoreVert,
contentDescription = "More",
)
}
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
content()
}
}
Now given below is the DropDownItem.
#Composable
fun SettingsDropDownItem(onClick: () -> Unit) {
DropdownMenuItem(onClick = onClick) {
Icon(
Icons.Filled.Settings,
contentDescription = "Settings",
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text("Settings")
}
}
What i am trying to do is, when i click the SettingsDroDownItem, i need to capture the click event in the OverFlowMenu composable to make the showMenu false, so as the hide the DropdownMenu. I can get the click event in the TopAppBar, but how to get it on DropDownMenu.
How to do that?
The first option is moving showMenu state out of OverflowMenu, as this is not the only composable which depends on the value. Something like this:
OverFlowMenu:
#Composable
fun OverflowMenu(showMenu: Bool, setShowMenu: (Bool) -> Unit, content: #Composable () -> Unit) {
// ...
}
TopBar:
actions = {
var (showMenu, setShowMenu) = remember { mutableStateOf(false) }
OverflowMenu(showMenu, setShowMenu) {
SettingsDropDownItem(onClick = {
openDialog.value = true
setShowMenu(false)
})
}
},
An other options is creating something like OverflowMenuScope, and running SettingsDropDownItem on this scope so it can close the menu itself:
OverflowMenu:
interface OverflowMenuScope {
fun closeMenu()
}
#Composable
fun OverflowMenu(content: #Composable OverflowMenuScope.() -> Unit) {
var showMenu by remember { mutableStateOf(false) }
val scope = remember {
object: OverflowMenuScope {
override fun closeMenu() {
showMenu = false
}
}
}
//...
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
scope.content()
}
}
SettingsDropDownItem:
#Composable
fun OverflowMenuScope.SettingsDropDownItem(onClick: () -> Unit) {
DropdownMenuItem(onClick = {
closeMenu()
onClick()
}) {
Icon(
Icons.Filled.Settings,
contentDescription = "Settings",
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text("Settings")
}
}
So in XML you were able to structure menu items and nest them like this.
But in jetpack compose, I am unable to figure out how this would work.
I already read and built a simple drop down menu from here. But trying to do the same as XML in jetpack compose doesn't make much sense. The menus are created separately and independent. I am looking for something simpler and better than that.
How about something like this?
Create a composable for your main menu, and one for your nested menu.
Main Menu Composable
#Composable
fun MainMenu(
menuSelection: MutableState<MenuSelection>,
expandedMain: MutableState<Boolean>,
expandedNested: MutableState<Boolean>
) {
DropdownMenu(
expanded = expandedMain.value,
onDismissRequest = { expandedMain.value = false },
) {
DropdownMenuItem(
onClick = {
expandedMain.value = false // hide main menu
expandedNested.value = true // show nested menu
menuSelection.value = MenuSelection.NESTED
}
) {
Text("Nested Options \u25B6")
}
Divider()
DropdownMenuItem(
onClick = {
// close main menu
expandedMain.value = false
menuSelection.value = MenuSelection.SETTINGS
}
) {
Text("Settings")
}
Divider()
DropdownMenuItem(
onClick = {
// close main menu
expandedMain.value = false
menuSelection.value = MenuSelection.ABOUT
}
) {
Text("About")
}
}
}
Nested Menu Composable
#Composable
fun NestedMenu(
expandedNested: MutableState<Boolean>,
nestedMenuSelection: MutableState<NestedMenuSelection>
) {
DropdownMenu(
expanded = expandedNested.value,
onDismissRequest = { expandedNested.value = false }
) {
DropdownMenuItem(
onClick = {
// close nested menu
expandedNested.value = false
nestedMenuSelection.value = NestedMenuSelection.FIRST
}
) {
Text("First")
}
DropdownMenuItem(
onClick = {
// close nested menu
expandedNested.value = false
nestedMenuSelection.value = NestedMenuSelection.SECOND
}
) {
Text("Second")
}
}
}
Then place them in your main drop down menu composable.
Top Bar Composable
#Composable
fun TopAppBarDropdownMenu(
menuSelection: MutableState<MenuSelection>,
nestedMenuSelection: MutableState<NestedMenuSelection>
) {
val expandedMain = remember { mutableStateOf(false) }
val expandedNested = remember { mutableStateOf(false) }
// Three Dot icon
Box(
Modifier
.wrapContentSize(Alignment.TopEnd)
) {
IconButton(
onClick = {
// Expand the main menu on three dots icon click
// and hide the nested menu.
expandedMain.value = true
expandedNested.value = false
}
) {
Icon(
Icons.Filled.MoreVert,
contentDescription = "More Menu"
)
}
}
MainMenu(
menuSelection = menuSelection,
expandedMain = expandedMain,
expandedNested = expandedNested
)
NestedMenu(
expandedNested = expandedNested,
nestedMenuSelection = nestedMenuSelection
)
}
The menuSelection and nestedMenuSelection parameters of TopAppBarDropdownMenu would allow implementing actions in the TopAppBarDropdownMenu host depending on which state is activated from the menu. Something like this:
#Composable
fun MainScreen(
/* some params */
) {
val menuSelection = remember { mutableStateOf(MenuSelection.NONE) }
val nestedMenuSelection = remember { mutableStateOf(NestedMenuSelection.DEFAULT) }
Scaffold(
topBar = {
TopAppBar(
title = { Text(text = stringResource(R.string.app_name)) },
actions = {
TopAppBarDropdownMenu(
menuSelection = menuSelection,
nestedMenuSelection= nestedMenuSelection
)
}
)
}
)
}
Not sure it's the "right" way to do it but it works for me.
You could potentially make it easier to instantiate nested menus by creating a NestedMenu composable with more parameters, which would make it easier to reuse. Just keep in mind that Material Design guidelines recommend using Cascading Menus only on desktop.
I am wondering this question as well.
At present, I compose this nested Menu like this:
#Composable
fun ExpandableDropdownItem(
text: String,
dropDownItems: #Composable () -> Unit
) {
var expanded by remember {
mutableStateOf(false)
}
DropdownMenuItem(onClick = {
expanded = true
}) {
Text(text = text)
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
dropDownItems()
}
}
This can be used with other DropdownItems.
And that's not the best solution : the place of the nested Menu is strange.
I made a nested menu variation for sorting that seems to work well using AnimatedVisibility.
When expanding, it switches the top level menus visibility to the submenus, and can even add a back button if needed, but more levels might get confusing with the switches.
#Composable
fun NestedSortMenu(
onSortClick: (SortOrder) -> Unit
) {
var expanded by remember { mutableStateOf(false) }
var orderType: OrderType by remember { mutableStateOf(OrderType.Ascending) }
AnimatedVisibility(visible = !expanded) {
Column {
DropdownMenuItem(onClick = {
orderType = OrderType.Ascending
expanded = !expanded
}) {
Text(text = stringResource(R.string.ascending))
Icon(
Icons.Filled.NavigateNext,
contentDescription = stringResource(R.string.ascending),
modifier = Modifier.size(20.dp)
)
}
DropdownMenuItem(onClick = {
orderType = OrderType.Descending
expanded = !expanded
}) {
Text(text = stringResource(R.string.descending))
Icon(
Icons.Filled.NavigateNext,
contentDescription = stringResource(R.string.descending),
modifier = Modifier.size(20.dp)
)
}
}
}
AnimatedVisibility(visible = expanded) {
Column {
DropdownMenuItem(onClick = {
expanded = !expanded
onSortClick(SortOrder.Title(orderType))
}) {
Text(text = stringResource(R.string.title_menu))
}
DropdownMenuItem(onClick = {
expanded = !expanded
onSortClick(SortOrder.Format(orderType))
}) {
Text(text = stringResource(R.string.format_menu))
}
DropdownMenuItem(onClick = {
expanded = !expanded
onSortClick(SortOrder.DateAndTime(orderType))
}) {
Text(text = stringResource(R.string.date_and_time))
}
}
}
}
I have the following composable function that creates a TopAppBar. The preview shows the bar just fine, but can I get the menu to expand in the preview as well?
#Composable
#Preview
fun AppBarTop(refreshOnClickHandler: (() -> Unit)? = null) {
var showMenu by remember { mutableStateOf(false) }
TopAppBar(
elevation = 10.dp,
title = {
Text(stringResource(R.string.app_name))
},
actions = {
IconButton(onClick = { /* //TODO Make this */ }) {
Icon(
Icons.Filled.Search,
contentDescription = stringResource(R.string.content_desc_search_icon)
)
}
IconButton(onClick = { showMenu = !showMenu }) {
Icon(
Icons.Filled.MoreVert,
contentDescription = stringResource(R.string.content_desc_menu_icon)
)
}
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
DropdownMenuItem(onClick = { /*TODO*/ }) {
Text(stringResource(R.string.action_refresh))
}
DropdownMenuItem(onClick = { /*TODO*/ }) {
Text(stringResource(R.string.action_settings))
}
}
}
)
}
Figured it out. Interactive preview is an experimental feature, so you have to enable it in the Android Studio settings first.
File -> Settings -> Experimental then check the box next to Enable interactive and animation preview tools.
After doing that, the interactive preview button will appear just above the preview of your component.