I was wondering if there is a solution for Exposed drop-down menu for jetpack compose? I couldn't find a proper solution for this component inside jetpack compose. Any help?
mutableStateOf creates an observable MutableState<T> , which is an observable type integrated with the compose runtime. interface MutableState<T> : State<T> { override var value: T } Any changes to value will schedule recomposition of any composable functions that read value .
A LazyColumn is a vertically scrolling list that only composes and lays out the currently visible items. It's similar to a Recyclerview in the classic Android View system.
A Scaffold is a layout which implements the basic material design layout structure. You can add things like a TopBar, BottomBar, FAB or a Drawer.
The version 1.1.0-alpha06
introduced the implementation of ExposedDropdownMenu
based on ExposedDropdownMenuBox
with TextField
and DropdownMenu
inside.
Something like:
val options = listOf("Option 1", "Option 2", "Option 3", "Option 4", "Option 5")
var expanded by remember { mutableStateOf(false) }
var selectedOptionText by remember { mutableStateOf(options[0]) }
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = {
expanded = !expanded
}
) {
TextField(
readOnly = true,
value = selectedOptionText,
onValueChange = { },
label = { Text("Label") },
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(
expanded = expanded
)
},
colors = ExposedDropdownMenuDefaults.textFieldColors()
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = {
expanded = false
}
) {
options.forEach { selectionOption ->
DropdownMenuItem(
onClick = {
selectedOptionText = selectionOption
expanded = false
}
) {
Text(text = selectionOption)
}
}
}
}
If you are using M3 (androidx.compose.material3
) you have also to pass the menuAnchor
modifier to the TextField
:
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = { expanded = !expanded },
) {
TextField(
//...
modifier = Modifier.menuAnchor()
)
ExposedDropdownMenu(){ /*.. */ }
}
With the version 1.0.x
there isn't a built-in component.
You can use a OutlinedTextField
+ DropdownMenu
.
It is just a basic (very basic) implementation:
var expanded by remember { mutableStateOf(false) }
val suggestions = listOf("Item1","Item2","Item3")
var selectedText by remember { mutableStateOf("") }
var textfieldSize by remember { mutableStateOf(Size.Zero)}
val icon = if (expanded)
Icons.Filled.ArrowDropUp //it requires androidx.compose.material:material-icons-extended
else
Icons.Filled.ArrowDropDown
Column() {
OutlinedTextField(
value = selectedText,
onValueChange = { selectedText = it },
modifier = Modifier
.fillMaxWidth()
.onGloballyPositioned { coordinates ->
//This value is used to assign to the DropDown the same width
textfieldSize = coordinates.size.toSize()
},
label = {Text("Label")},
trailingIcon = {
Icon(icon,"contentDescription",
Modifier.clickable { expanded = !expanded })
}
)
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
modifier = Modifier
.width(with(LocalDensity.current){textfieldSize.width.toDp()})
) {
suggestions.forEach { label ->
DropdownMenuItem(onClick = {
selectedText = label
}) {
Text(text = label)
}
}
}
}
This is what I did to get the width the same as the text field: Copying and modifying Gabriele's answer.
var expanded by remember { mutableStateOf(false) }
val suggestions = listOf("Item1","Item2","Item3")
var selectedText by remember { mutableStateOf("") }
var dropDownWidth by remember { mutableStateOf(0) }
val icon = if (expanded)
Icons.Filled.....
else
Icons.Filled.ArrowDropDown
Column() {
OutlinedTextField(
value = selectedText,
onValueChange = { selectedText = it },
modifier = Modifier.fillMaxWidth()
.onSizeChanged {
dropDownWidth = it.width
},
label = {Text("Label")},
trailingIcon = {
Icon(icon,"contentDescription", Modifier.clickable { expanded = !expanded })
}
)
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
modifier = Modifier
.width(with(LocalDensity.current){dropDownWidth.toDp()})
) {
suggestions.forEach { label ->
DropdownMenuItem(onClick = {
selectedText = label
}) {
Text(text = label)
}
}
}
}
Here's my version.
I achieved this without using a TextField
(so no keyboard).
There's a "regular" and an "outlined" version.
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.ZeroCornerSize
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.toSize
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
// ExposedDropDownMenu will be added in Jetpack Compose 1.1.0.
// This is a reimplementation while waiting.
// See https://stackoverflow.com/questions/67111020/exposed-drop-down-menu-for-jetpack-compose/6904285
@Composable
fun SimpleExposedDropDownMenu(
values: List<String>,
selectedIndex: Int,
onChange: (Int) -> Unit,
label: @Composable () -> Unit,
modifier: Modifier,
backgroundColor: Color = MaterialTheme.colors.onSurface.copy(alpha = TextFieldDefaults.BackgroundOpacity),
shape: Shape = MaterialTheme.shapes.small.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize)
) {
SimpleExposedDropDownMenuImpl(
values = values,
selectedIndex = selectedIndex,
onChange = onChange,
label = label,
modifier = modifier,
backgroundColor = backgroundColor,
shape = shape,
decorator = { color, width, content ->
Box(
Modifier
.drawBehind {
val strokeWidth = width.value * density
val y = size.height - strokeWidth / 2
drawLine(
color,
Offset(0f, y),
Offset(size.width, y),
strokeWidth
)
}
) {
content()
}
}
)
}
@Composable
fun SimpleOutlinedExposedDropDownMenu(
values: List<String>,
selectedIndex: Int,
onChange: (Int) -> Unit,
label: @Composable () -> Unit,
modifier: Modifier,
backgroundColor: Color = MaterialTheme.colors.onSurface.copy(alpha = TextFieldDefaults.BackgroundOpacity),
shape: Shape = MaterialTheme.shapes.small
) {
SimpleExposedDropDownMenuImpl(
values = values,
selectedIndex = selectedIndex,
onChange = onChange,
label = label,
modifier = modifier,
backgroundColor = backgroundColor,
shape = shape,
decorator = { color, width, content ->
Box(
Modifier
.border(width, color, shape)
) {
content()
}
}
)
}
@Composable
private fun SimpleExposedDropDownMenuImpl(
values: List<String>,
selectedIndex: Int,
onChange: (Int) -> Unit,
label: @Composable () -> Unit,
modifier: Modifier,
backgroundColor: Color,
shape: Shape,
decorator: @Composable (Color, Dp, @Composable () -> Unit) -> Unit
) {
var expanded by remember { mutableStateOf(false) }
var textfieldSize by remember { mutableStateOf(Size.Zero) }
val indicatorColor =
if (expanded) MaterialTheme.colors.primary.copy(alpha = ContentAlpha.high)
else MaterialTheme.colors.onSurface.copy(alpha = TextFieldDefaults.UnfocusedIndicatorLineOpacity)
val indicatorWidth = (if (expanded) 2 else 1).dp
val labelColor =
if (expanded) MaterialTheme.colors.primary.copy(alpha = ContentAlpha.high)
else MaterialTheme.colors.onSurface.copy(ContentAlpha.medium)
val trailingIconColor = MaterialTheme.colors.onSurface.copy(alpha = TextFieldDefaults.IconOpacity)
val rotation: Float by animateFloatAsState(if (expanded) 180f else 0f)
val focusManager = LocalFocusManager.current
Column(modifier = modifier.width(IntrinsicSize.Min)) {
decorator(indicatorColor, indicatorWidth) {
Box(
Modifier
.fillMaxWidth()
.background(color = backgroundColor, shape = shape)
.onGloballyPositioned { textfieldSize = it.size.toSize() }
.clip(shape)
.clickable {
expanded = !expanded
focusManager.clearFocus()
}
.padding(start = 16.dp, end = 12.dp, top = 7.dp, bottom = 10.dp)
) {
Column(Modifier.padding(end = 32.dp)) {
ProvideTextStyle(value = MaterialTheme.typography.caption.copy(color = labelColor)) {
label()
}
Text(
text = values[selectedIndex],
modifier = Modifier.padding(top = 1.dp)
)
}
Icon(
imageVector = Icons.Filled.ExpandMore,
contentDescription = "Change",
tint = trailingIconColor,
modifier = Modifier
.align(Alignment.CenterEnd)
.padding(top = 4.dp)
.rotate(rotation)
)
}
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
modifier = Modifier
.width(with(LocalDensity.current) { textfieldSize.width.toDp() })
) {
values.forEachIndexed { i, v ->
val scope = rememberCoroutineScope()
DropdownMenuItem(
onClick = {
onChange(i)
scope.launch {
delay(150)
expanded = false
}
}
) {
Text(v)
}
}
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With