Offengelegtes Dropdown-Menü für Jetpack-Komposition

Lesezeit: 9 Minuten

Stefans Benutzeravatar
Stefan

Ich habe mich gefragt, ob es eine Lösung für das Dropdown-Menü „Exposed“ für Jetpack Compose gibt? Ich konnte in Jetpack Compose keine geeignete Lösung für diese Komponente finden. Irgendeine Hilfe?

Dropdown-Liste

Benutzer-Avatar von Gabriele Mariotti
Gabriele Mariotti

Der M2 (ab der Version 1.1.0-alpha06) Und M3 Habe die Umsetzung von ExposedDropdownMenu bezogen auf ExposedDropdownMenuBox mit TextField Und DropdownMenu innen.

Etwas wie:

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

Geben Sie hier eine Bildbeschreibung ein

Wenn Sie verwenden M3 (androidx.compose.material3) musst du auch bestehen menuAnchor Modifikator für die TextField:

ExposedDropdownMenuBox(
    expanded = expanded,
    onExpandedChange = { expanded = !expanded },
) {
   TextField(
        //...
        modifier = Modifier.menuAnchor()
    )
    ExposedDropdownMenu(){ /*..  */ }
}

Auch im M3 im DropdownMenuItem Sie müssen den Inhalt in verschieben text Parameter:

DropdownMenuItem(
    text = { Text(text = selectionOption) },
    onClick = {
        selectedOptionText = selectionOption
        expanded = false
    }
)

Mit der M2-Version 1.0.x Es gibt keine eingebaute Komponente.
Sie können a verwenden OutlinedTextField + DropdownMenu. Es ist wichtig, sie einzuwickeln Box. Auf diese Weise wird das TextField als „Anker“ verwendet.

Es handelt sich lediglich um eine grundlegende (sehr einfache) Implementierung:

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

Geben Sie hier eine Bildbeschreibung ein
Geben Sie hier eine Bildbeschreibung ein

  • Im Issuetracker von Google wurde ein Fehler gemeldet: https://issuetracker.google.com/issues/173532272 Hoffentlich wird es implementiert, bevor Stable veröffentlicht wird.

    – Chris

    24. Mai 2021 um 22:07 Uhr

  • Gibt es eine Möglichkeit, die Breite von DropdownMenu an die von OutlinedTextField anzupassen?

    – Ali_Waris

    23. Juli 2021 um 10:07 Uhr

  • Gibt es eine Möglichkeit, das Menü über dem Textfeld statt darunter zu positionieren?

    – Kofi

    13. September 2021 um 14:17 Uhr

  • @Ali_Waris Ich fand diese Antwort hilfreich.

    – Treslumen

    30. März um 21:54

  • @TippuFisalSheriff-Check Developer.android.com/reference/kotlin/androidx/compose/ui/…. Es ist nur Size(0.0f, 0.0f)

    – Gabriele Mariotti

    29. Mai um 12:38 Uhr


Folgendes habe ich getan, um die gleiche Breite wie das Textfeld zu erreichen: Gabrieles Antwort kopieren und ändern.

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

Benutzeravatar von Salomon BRYS
Salomon BRYS

Hier ist meine Version. Ich habe dies erreicht, ohne a zu verwenden TextField (also keine Tastatur). Es gibt eine „normale“ und eine „umrissene“ 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)
                }
            }
        }
    }
}

  • Vielen Dank, dass Sie dies geteilt haben. Dieses sieht genau so aus und funktioniert, wie ich es von einem ExposedDropdown erwarte. Das einzige, was ich ändern musste, war die Verwendung Icons.Filled.ArrowDropDown anstatt Icons.Filled.ExpandMore.

    – ice_chrysler

    11. Okt. 2021 um 10:24

Wenn Sie verwenden material3 und eine neuere Version von Compose (dies funktioniert für v1.3.1), Die DropdownMenuItem hat sich leicht verändert. Text muss jetzt eine Eigenschaft sein (und nicht eine @Composable).

Sie müssen sich weiterhin für die experimentelle API anmelden. @OptIn(ExperimentalMaterial3Api::class).

Dieses Beispiel befindet sich in der androidx.compose.material3 Dokumentation.

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

Als ich dies auf die „alte Art“ machte, traten bei mir die folgenden Fehler auf Text(text = selectionOption) Linie:

  • 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

Benutzeravatar von Arpit Patel
Arpit Patel

Ein paar Änderungen an der Antwort von @Gabriele Mariotti. Ein Benutzer kann ein Gliederungstextfeld auswählen und aus einer Option auswählen. Die Option verschwindet, sobald der Benutzer eine Option auswählt.

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

Zusätzlich zu dem, was hier geschrieben wurde, könnte ich für jemanden nützlich sein und für meine persönliche Notiz für die nächste Verwendung habe ich diese Dropdown-Menü-Funktionskomponente realisiert, die BasicTextField ohne Dekoration und ohne Standardauffüllung und ohne Pfeilsymbol verwendet , wobei der Text des ausgewählten Elements nach rechts ausgerichtet ist (.End), wobei die maximale Textbreite (.fillMaxWidth()) mit einer einzelnen Zeile in der Liste gefüllt wird.

Geben Sie hier eine Bildbeschreibung ein

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

                }
            }
        }
    }

}

Meine Verwendung:

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


        }


    // .....


}

um den Wert nach der Auswahl abzurufen: CategoryDropDownMenuPar.selectedOptionText

1451910cookie-checkOffengelegtes Dropdown-Menü für Jetpack-Komposition

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy