Can You Expand a DropdownMenu in the Jetpack Compose Preview? - android

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.

Related

Android Jetpack compose: BackdropScaffold background with GoogleMap corner issue

I'm using the new BackdropScaffold composable to make a similar looking screen like Google Map with a Map on the back and a list on the front. (See the image)
As you can see there is a problem with the corner around the front layer. Currently is displayed the surface under (pale blue). What I would like to achieve is having the Google Map shown in those corners. I tried to play with the size and padding of GoogleMap composable or the front panel but no luck.
UPDATE
The following example code shows the issue I'm facing. As you can see the BackdropScaffold background is correctly applied (RED). The corners of the front layer are transparent. The issue comes out when you have a different color in your background layer (BLUE). If the background layer contains a map you have the same issue.
BackdropScaffold is dividing the space but not overlaying any layer. The front layer should overlay a bit the back layer to fix this problem.
#OptIn(ExperimentalMaterialApi::class)
#Composable
internal fun test() {
val scope = rememberCoroutineScope()
val selection = remember { mutableStateOf(1) }
val scaffoldState = rememberBackdropScaffoldState(BackdropValue.Concealed)
val frontLayerHeightDp = LocalConfiguration.current.screenHeightDp / 3
LaunchedEffect(scaffoldState) {
scaffoldState.conceal()
}
BackdropScaffold(
scaffoldState = scaffoldState,
appBar = {
TopAppBar(
title = { Text("Backdrop scaffold") },
navigationIcon = {
if (scaffoldState.isConcealed) {
IconButton(onClick = { scope.launch { scaffoldState.reveal() } }) {
Icon(Icons.Default.Menu, contentDescription = "Localized description")
}
} else {
IconButton(onClick = { scope.launch { scaffoldState.conceal() } }) {
Icon(Icons.Default.Close, contentDescription = "Localized description")
}
}
},
actions = {
var clickCount by remember { mutableStateOf(0) }
IconButton(
onClick = {
// show snackbar as a suspend function
scope.launch {
scaffoldState.snackbarHostState
.showSnackbar("Snackbar #${++clickCount}")
}
}
) {
Icon(Icons.Default.Favorite, contentDescription = "Localized description")
}
},
elevation = 0.dp,
backgroundColor = Color.Transparent
)
},
backLayerContent = {
LazyColumn(modifier = Modifier.background(Color.Blue)) {
items(if (selection.value >= 3) 3 else 5) {
ListItem(
Modifier.clickable {
selection.value = it
scope.launch { scaffoldState.conceal() }
},
text = { Text("Select $it", color = Color.White) }
)
}
}
},
backLayerBackgroundColor = Color.Red,
frontLayerShape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp),
headerHeight = frontLayerHeightDp.dp,
frontLayerBackgroundColor = Color.White,
frontLayerContent = {
LazyColumn {
items(50) {
ListItem(
text = { Text("Item $it") },
icon = {
Icon(
Icons.Default.Favorite,
contentDescription = "Localized description"
)
}
)
}
}
}
)
}
BackdropScaffold is creating a Surface for from layer under the hood, when you create your own inside frontLayerContent it's displayed on top of built-in one.
Instead use frontLayerShape and frontLayerBackgroundColor parameters:
frontLayerShape = BottomSheetShape,
frontLayerBackgroundColor = Color.White,
frontLayerContent = {
LazyColumn(
modifier = Modifier.padding(16.dp)
) {
items(
items = moorings,
itemContent = { mooring ->
...
}
)
}
}
p.s. some comments about your code:
When you have modifier parameter, you should only apply it for the topmost container in your view - here you've applied it for content of frontLayerContent, which may cause unexpected behaviour.
You don't need to wrap LazyColumn in a Column - it has no effect when Column has only one child, and if you need to apply a modifier you can do it directly for LazyColumn

Jetpack Compose : Hoisitng click state from DropDownItems to DropDownMenu

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")
}
}

What is the better or easier way to create "nested" menus in Jetpack Compose?

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))
}
}
}
}

Jetpack compose DropdownMenu With rounded Corners

Hello I can't figure out how to make a cut corners menu in jetpack compose 1.0.0-beta02. I tried wrapping the while menu with a surface but It didn't work.
TopAppBar(
modifier = Modifier
.statusBarsPadding(),
title = {
Text(text = "Title")
},
actions = {
var menuExpanded by remember { mutableStateOf(false) }
IconButton(onClick = { menuExpanded = true }) {
Icon(Icons.Default.MoreVert, contentDescription = null)
}
DropdownMenu(
expanded = menuExpanded,
onDismissRequest = {
menuExpanded = false
},
) {
DropdownMenuItem(onClick = {}) {
Text("Item 2")
}
}
},
)
Which gives me
But I need something like this, which is rounded.
Using a M2 MaterialTheme theme, the default shape used by the DropdownMenu is defined by the
medium attribute in the shapes used in the MaterialTheme (check your theme).
val Shapes = Shapes(
small = RoundedCornerShape(4.dp),
medium = RoundedCornerShape(4.dp), //<- used by `DropdownMenu`
large = RoundedCornerShape(0.dp)
)
You can change this value in your theme or you can override the medium shape only in your DropdownMenu.
Something like:
MaterialTheme(shapes = MaterialTheme.shapes.copy(medium = RoundedCornerShape(16.dp))) {
DropdownMenu(
expanded = menuExpanded,
onDismissRequest = {
menuExpanded = false
}
) {
DropdownMenuItem(onClick = {}) {
Text("Item 2")
}
DropdownMenuItem(onClick = {}) {
Text("Item 3")
}
}
}
Using a M3 MaterialTheme the default shape used by the DropdownMenu is defined by the extraSmall attribute in the shapes:
MaterialTheme(
shapes = MaterialTheme.shapes.copy(extraSmall = RoundedCornerShape(16.dp))){
//... DropdownMenu()
}

How can Toolbar with overflow menu be created with Jetpack Compose?

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

Categories

Resources