stefano
Me preguntaba si hay una solución para el menú desplegable Expuesto para la composición del jetpack. No pude encontrar una solución adecuada para este componente dentro de la composición del jetpack. ¿Alguna ayuda?
gabriele mariotti
El M2 (a partir de la versión 1.1.0-alpha06
) y M3 contar con la implementación de ExposedDropdownMenu
Residencia en ExposedDropdownMenuBox
con TextField
y DropdownMenu
adentro.
Algo como:
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)
}
}
}
}
Si estás usando M3 (androidx.compose.material3
) también tienes que pasar el menuAnchor
modificador de la TextField
:
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = { expanded = !expanded },
) {
TextField(
//...
modifier = Modifier.menuAnchor()
)
ExposedDropdownMenu(){ /*.. */ }
}
También en M3 en el DropdownMenuItem
tienes que mover el contenido en el text
parámetro:
DropdownMenuItem(
text = { Text(text = selectionOption) },
onClick = {
selectedOptionText = selectionOption
expanded = false
}
)
Con la versión M2 1.0.x
no hay un componente incorporado.
Puedes usar un OutlinedTextField
+ DropdownMenu
. Es importante envolverlos en un Box
. De esta forma, TextField se utilizará como ‘ancla’.
Es solo una implementación básica (muy básica):
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
Box() {
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)
}
}
}
}
-
Se ha presentado un error en el rastreador de problemas de Google: https://issuetracker.google.com/issues/173532272 Esperemos que se implemente antes de que se publique la versión estable.
– Chris
24 mayo 2021 a las 22:07
-
¿Hay alguna manera de hacer que el ancho de DropdownMenu sea el mismo que el de OutlinedTextField?
– Ali_Waris
23 de julio de 2021 a las 10:07
-
¿Hay alguna forma de colocar el menú encima del campo de texto en lugar de debajo?
– Kofi
13 de septiembre de 2021 a las 14:17
-
@Ali_Waris Encontré esta respuesta útil.
– treslumen
30 de marzo a las 21:54
-
Cheque @TippuFisalSheriff desarrollador.android.com/referencia/kotlin/androidx/compose/ui/…. Es sólo
Size(0.0f, 0.0f)
– Gabriele Mariotti
29 de mayo a las 12:38
Esto es lo que hice para obtener el mismo ancho que el campo de texto: copiar y modificar la respuesta de Gabriele.
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)
}
}
}
}
Salomón BRYS
Aquí está mi versión. Logré esto sin usar un TextField
(así que no hay teclado). Hay una versión “normal” y otra “delineada”.
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)
}
}
}
}
}
-
Gracias por compartir esto. Este se ve y funciona exactamente como esperaba un ExposedDropdown. Lo único que tuve que cambiar fue usar
Icons.Filled.ArrowDropDown
en lugar deIcons.Filled.ExpandMore
.– ice_chrysler
11 oct 2021 a las 10:24
Si estás usando material3
y una versión más nueva de componer (esto funciona para v1.3.1
), el DropdownMenuItem
ha cambiado ligeramente. El texto ahora debe ser una propiedad (en lugar de un @Composable
).
Aún deberá optar por la API experimental, @OptIn(ExperimentalMaterial3Api::class)
.
Este ejemplo está en el androidx.compose.material3 documentación.
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
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]) }
// We want to react on tap/press on TextField to show menu
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = { expanded = !expanded },
) {
TextField(
// The `menuAnchor` modifier must be passed to the text field for correctness.
modifier = Modifier.menuAnchor(),
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(
text = { Text(selectionOption) },
onClick = {
selectedOptionText = selectionOption
expanded = false
},
contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
)
}
}
}
Al hacer esto de la “manera antigua”, tuve los siguientes errores en el Text(text = selectionOption)
línea:
No value passed for parameter 'text'
Type mismatch: inferred type is () -> Unit but MutableInteractionSource was expected
@Composable invocations can only happen from the context of a @Composable function
Arpit Patel
Algunas modificaciones a la respuesta de @Gabriele Mariotti Un usuario puede seleccionar un campo de texto de esquema y seleccionar una opción. La opción desaparecerá una vez que el usuario seleccione cualquier opción.
@Composable
fun DropDownMenu(optionList: List<String>,label:String,) {
var expanded by remember { mutableStateOf(false) }
var selectedText by remember { mutableStateOf("") }
var textfieldSize by remember { mutableStateOf(Size.Zero) }
val icon = if (expanded)
Icons.Filled.KeyboardArrowUp
else
Icons.Filled.KeyboardArrowDown
Column() {
OutlinedTextField(
value = selectedText,
onValueChange = { selectedText = it },
enabled = false,
modifier = Modifier
.fillMaxWidth()
.onGloballyPositioned { coordinates ->
//This value is used to assign to the DropDown the same width
textfieldSize = coordinates.size.toSize()
}
.clickable { expanded = !expanded },
label = { Text(label) },
trailingIcon = {
Icon(icon, "Drop Down Icon",
Modifier.clickable { expanded = !expanded })
}
)
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
modifier = Modifier
.width(with(LocalDensity.current) { textfieldSize.width.toDp() })
) {
optionList.forEach { label ->
DropdownMenuItem(onClick = {
selectedText = label
expanded = !expanded
}) {
Text(text = label)
}
}
}
}
}
Además de lo que se ha escrito aquí, el caso podría ser útil para alguien y para mi nota de nota personal para los próximos usos, me di cuenta de este componente de función de menú desplegable usando BasicTextField sin decoración y sin relleno predeterminado, sin icono de flecha , con el texto del elemento seleccionado alineado a la derecha (.End), llenando el ancho máximo del texto (.fillMaxWidth()) con una sola línea en la lista.
data class DropDownMenuParameter(
var options: List<String>,
var expanded: Boolean,
var selectedOptionText: String,
var backgroundColor: Color
)
@ExperimentalMaterialApi
@Composable
fun DropDownMenuComponent(params: DropDownMenuParameter) {
var expanded by remember { mutableStateOf(params.expanded) }
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = {
expanded = !expanded
}
) {
BasicTextField(
modifier = Modifier
.background(params.backgroundColor)
.fillMaxWidth(),
readOnly = true,
value = params.selectedOptionText,
onValueChange = { },
textStyle = TextStyle(
color = Color.White,
textAlign = TextAlign.End,
fontSize = 16.sp,
),
singleLine = true
)
ExposedDropdownMenu(
modifier = Modifier
.background(params.backgroundColor),
expanded = expanded,
onDismissRequest = {
expanded = false
}
) {
params.options.forEach { selectionOption ->
DropdownMenuItem(
modifier = Modifier
.background(params.backgroundColor),
onClick = {
params.selectedOptionText = selectionOption
expanded = false
},
) {
Text(
text = selectionOption,
color = Color.White,
)
}
}
}
}
}
Mi uso:
@OptIn(ExperimentalAnimationApi::class, ExperimentalMaterialApi::class)
@Composable
fun SubscribeSubscriptionDetails(selectedSubscription : Subscription){
val categoryOptions = listOf("Entertainment", "Gaming", "Business", "Utility", "Music", "Food & Drink", "Health & Fitness", "Bank", "Transport", "Education", "Insurance", "News")
val categoryExpanded by rememberSaveable { mutableStateOf(false) }
val categorySelectedOptionText
by rememberSaveable { mutableStateOf(selectedSubscription.category) }
val categoryDropDownMenuPar by remember {
mutableStateOf(
DropDownMenuParameter(
options = categoryOptions,
expanded = categoryExpanded,
selectedOptionText = categorySelectedOptionText,
backgroundColor = serviceColorDecoded
)
)
}
// ....
Row { // categoria
Text(
modifier = Modifier
.padding(textMargin_24, 0.dp, 0.dp, 0.dp)
.weight(0.5f),
text = "Categoria",
fontWeight = FontWeight.Bold,
color = Color.White,
textAlign = TextAlign.Left,
fontSize = 16.sp,
)
Row(
modifier = Modifier
.padding(0.dp, 0.dp, 24.dp, 0.dp)
.weight(0.5f),
horizontalArrangement = Arrangement.End
){
DropDownMenuComponent(categoryDropDownMenuPar)
}
}
// .....
}
para recuperar el valor después de la selección: categoryDropDownMenuPar.selectedOptionText