Reputation: 431
I am trying to rewrite my project UI using Jetpack compose. Any idea to add popup menu using jetpack compose in android?. like this one below.
I tried to implement it using Stack() layout but the results are not up to the mark.
@Composable
fun LiveDataComponentList(productList: List<Product>) {
AdapterList(data = productList) { product ->
Stack() {
Clickable(onClick = { PopupState.toggleOwner(product) }) {
Card(...) {...}
if (PopupState.owner == product) {
Surface(color = Color.Gray,modifier = Modifier.gravity(Alignment.TopEnd) + Modifier.padding(12.dp)) {
Column() {
Text("menu 1")
Text("menu 2")
Text("menu 3")
Text("menu 4")
Text("menu 5")
}
}
}
}
}
}
and PopupState is
@Model
object PopupState
{
var owner:Product?=null
fun toggleOwner(item:Product)
{
if(owner==item)
owner=null
else
owner=item
}
}
result is
Upvotes: 24
Views: 32877
Reputation: 363935
You can use the DropdownMenu
.
Something like:
var expanded by remember { mutableStateOf(false) }
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
DropdownMenuItem(
text = { Text("Refresh") },
onClick = { /* Handle refresh! */ }
)
DropdownMenuItem(
text = { Text("Settings") },
onClick = { /* Handle settings! */ }
)
Divider()
DropdownMenuItem(
text = { Text("Send Feedback") },
onClick = { /* Handle send feedback! */ }
)
}
It works with M3. With M2 you have to use:
androidx.compose.material.DropdownMenuItem(
onClick = { expanded = false }
) {
Text("All Accounts")
}
About the position, as explained in the documentation:
A
DropdownMenu
behaves similarly to aPopup
, and will use the position of the parent layout to position itself on screen. Commonly aDropdownMenu
will be placed in aBox
with a sibling that will be used as the 'anchor'.
Example:
Box(
modifier = Modifier.fillMaxSize().wrapContentSize(Alignment.TopStart)
){
IconButton(onClick = { expanded = true }) {
Icon(
Icons.Default.MoreVert,
contentDescription = "Localized description"
)
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
//...
}
Upvotes: 44
Reputation: 11
For My use case I created a Icon button which has pop up menu and that can be used where pop menu is needed.
@Composable
fun PopUpMenuButton(
options: List<PopUpMenuItem>,
action: (String) -> Unit,
iconTint: Color = Color.Black,
modifier: Modifier
) {
var expanded by remember { mutableStateOf(false) }
Column {
Box(modifier = Modifier.size(24.dp)) {
IconButton(onClick = {
expanded = !expanded
}) {
Icon(
painter = painterResource(id = R.drawable.ic_dots),
contentDescription = null,
modifier = Modifier.wrapContentSize(),
tint = iconTint
)
}
}
Box(modifier = modifier) {
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
modifier = Modifier
.widthIn(min = 120.dp, max = 240.dp)
.background(MaterialTheme.colors.background)
) {
options.forEachIndexed { _, item ->
DropdownMenuItem(onClick = {
expanded = false
action(item.id)
}) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
painterResource(id = item.icon),
contentDescription = null,
tint = iconTint,
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = item.label,
style = MaterialTheme.typography.body1,
overflow = TextOverflow.Ellipsis
)
}
}
if (item.hasBottomDivider) {
Divider()
}
}
}
}
}
}
Then I created a simple data class for defining menu item
data class PopUpMenuItem(
val id: String,
val label: String,
val icon: Int,
val hasBottomDivider: Boolean = false,
)
Then at the calling side I simply use this button like this
PopUpMenuButton(
modifier = Modifier.wrapContentSize(),
options = PopMenuOptionsProvider.sectionCardMenu,
iconTint = MaterialTheme.extendedColor.regularGray,
action = { menuId -> onSectionMenuAction(menuId) }
)
It can be further refactored to make it more extensible, but this worked for me.
Upvotes: 1
Reputation: 891
Since DropDownPopup was removed, I implemented one using DropDownMenu instead like this:
PopupMenu:
@Composable
fun PopupMenu(
menuItems: List<String>,
onClickCallbacks: List<() -> Unit>,
showMenu: Boolean,
onDismiss: () -> Unit,
toggle: @Composable () -> Unit,
) {
DropdownMenu(
toggle = toggle,
expanded = showMenu,
onDismissRequest = { onDismiss() },
) {
menuItems.forEachIndexed { index, item ->
DropdownMenuItem(onClick = {
onDismiss()
onClickCallbacks[index]
}) {
Text(text = item)
}
}
}
}
Toggle (thing to long click on to trigger PopupMenu):
@Preview
@Composable
fun Toggle() {
var showMenu by remember { mutableStateOf(false) }
PopupMenu(
menuItems = listOf("Delete"),
onClickCallbacks = listOf { println("Deleted") },
showMenu = showMenu,
onDismiss = { showMenu = false }) {
Text(
modifier = Modifier.clickable(onClick = {}, onLongClick = {
showMenu = true
}),
text = "Long click here",
)
}
}
Upvotes: 12
Reputation: 431
After some research I found a solution to this, the key component is DropdownPopup
@Composable
fun LiveDataComponentList(productList: List<Product>) {
AdapterList(data = productList) { product ->
Clickable(onClick = { PopupState.toggleOwner(product) }) {
Card(...) {...}
}
if (PopupState.owner == product) {
DropdownPopup(dropDownAlignment = DropDownAlignment.End)
{
Surface(
shape = RoundedCornerShape(4.dp),
elevation = 16.dp,
color = Color.White,
modifier = Modifier.gravity(Alignment.End)+ Modifier.padding(end = 10.dp)
)
{
Column(modifier = Modifier.padding(10.dp)) {
MenuItem(text ="Edit", onClick = {})
MenuItem(text = "Delete", onClick = {})
MenuItem(text = "Details", onClick = {})
}
}
}
}
}
}
@Composable
fun MenuItem(text: String, onClick: () -> Unit) {
Clickable(onClick = onClick, modifier = Modifier.padding(6.dp)) {
Text(text = text, style = MaterialTheme.typography.subtitle1)
}
}
This solution works fine with compose version dev10
Upvotes: 3