Jetpack Compose中的暴露下拉菜单

54
我想知道是否有一种方案可以在Jetpack Compose中实现下拉式菜单?我在Jetpack Compose中找不到适当的解决方案。有人能够提供帮助吗?\{\{end\}\}

Drop-down

7个回答

112

M2(从版本{{link1:1.1.0-alpha06}}开始)和M3实现了基于ExposedDropdownMenuBoxExposedDropdownMenu,其中包含TextFieldDropdownMenu

类似于:

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

enter image description here

如果你正在使用M3 (androidx.compose.material3),你还需要向TextField传递menuAnchor修饰符:

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

同样在 M3 中,你需要将 DropdownMenuItem 中的内容移动到 text 参数中:

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

在 M2 版本 1.0.x 中,没有内置的组件。
您可以使用 OutlinedTextField + DropdownMenu。重要的是要将它们包装在一个 Box 中。这样,文本框就会被用作“锚点”。

这只是一个基本(非常基本)的实现:

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

enter image description here enter image description here


5
在 Google 的问题跟踪器中提交了一个错误报告:https://issuetracker.google.com/issues/173532272 希望在稳定版发布之前实现。 - Chris
4
有没有办法使下拉菜单的宽度与OutlinedTextField的宽度相同? - Ali_Waris
1
有没有一种方法可以将菜单放置在文本字段上方而不是下方? - Kofi
1
@Ali_Waris 我发现这个答案很有帮助。 - treslumen
1
@TippuFisalSheriff 请查看 https://developer.android.com/reference/kotlin/androidx/compose/ui/geometry/Size#Zero()。这是指 Size(0.0f, 0.0f) - Gabriele Mariotti
显示剩余15条评论

12
这是我为了让宽度与文本字段相同而采取的方法:复制并修改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)
            }
        }
    }
}

8
这是我的版本。 我在不使用TextField(因此没有键盘)的情况下实现了这一点。 有一个“常规”和一个“轮廓”版本。
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.ArrowDropDown
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://dev59.com/hlEG5IYBdhLWcg3wX7pa#6904285

@Composable
fun SimpleExposedDropDownMenu(
    values: List<String>,
    selectedIndex: Int,
    onChange: (Int) -> Unit,
    label: @Composable () -> Unit,
    modifier: 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 = 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.ArrowDropDown,
                    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)
                }
            }
        }
    }
}

2
谢谢你分享这个。这个下拉框看起来和工作起来都和我期望的ExposedDropdown一样。唯一需要改变的是使用Icons.Filled.ArrowDropDown代替Icons.Filled.ExpandMore - ice_chrysler

6
如果您正在使用material3和更新版本的compose(这适用于v1.3.1),DropdownMenuItem稍微有所改变。现在文本必须是一个属性(而不是一个@Composable)。 您仍需要选择实验性API:@OptIn(ExperimentalMaterial3Api::class)。 此示例包含在androidx.compose.material3文档中。
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,
            )
        }
    }
}

以“老方式”执行此操作时,我在Text(text = selectionOption)行上遇到了以下错误:

  • 未为参数“text”传递值
  • 类型不匹配:推断的类型为() -> Unit,但期望是MutableInteractionSource
  • @Composable调用仅能在@Composable函数的上下文中发生

为什么只有readOnly = true呢? - undefined
readOnly标志是我正在寻找的。这样我们就可以禁用键盘输入。当我们有一组预定义的值,而没有可能输入自定义值时,这非常有帮助。这样就不会有键盘输入的麻烦了;-) - undefined

1
一些对 @Gabriele Mariotti 答案的修改 用户可以选择一个大纲文本字段并从选项中进行选择。一旦用户选择了任何选项,该选项将消失。
    @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)
                }
            }
        }
    }
}

0
除了这里已经写的内容之外,我想对一些人来说可能有用,并作为我的个人备忘录记录下来以备将来使用,我已经使用BasicTextField(没有装饰和默认填充,没有箭头图标)实现了这个下拉菜单函数组件,选定的文本向右对齐(.End),在列表中单行填满最大文本宽度(.fillMaxWidth())。

enter image description here

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

                }
            }
        }
    }

}

我的使用方式:

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


        }


    // .....


}

获取选择后的值:categoryDropDownMenuPar.selectedOptionText


0
其他答案都是基于选择字符串的。然而,我需要下拉菜单从模型列表中选择,并且只允许选择一个模型。我还希望下拉菜单立即显示所有选项。
以下是一种解决方案,可以在允许自动完成搜索模型的同时选择模型。该解决方案也适用于简单的字符串。
用法:
data class PreviewOption(val text: String, val id: Int)

val options = remember {
    listOf(
        PreviewOption("Option 1", 1),
        PreviewOption("Option 2", 2),
        PreviewOption("Option 3", 3),
        PreviewOption("Option 4", 4),
        PreviewOption("Option 5", 5),
    )
}

var selectedOption by remember { mutableStateOf<PreviewOption?>(null) }

TextFieldMenu(
    label = "Options",
    options = options,
    selectedOption = selectedOption,
    onOptionSelected = { selectedOption = it },
    optionToString = { it.text },
    filteredOptions = { searchInput ->
        options.filter { it.text.contains(searchInput, ignoreCase = true) }
    },
)

enter image description here

完整的M3实现:TextFieldMenu.kt
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.relocation.BringIntoViewRequester
import androidx.compose.foundation.relocation.bringIntoViewRequester
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldColors
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch

/** A text field that allows the user to type in to filter down options. */
@OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class,
    ExperimentalMaterial3Api::class
)
@Composable
fun <T> TextFieldMenu(
    modifier: Modifier = Modifier,
    /** The label for the text field */
    label: String,
    /** All the available options. */
    options: List<T>,
    /** The selected option. */
    selectedOption: T?,
    /** When the option is selected via tapping on the dropdown option or typing in the option. */
    onOptionSelected: (T?) -> Unit,
    /** Converts [T] to a string for populating the initial text field value. */
    optionToString: (T) -> String,
    /** Returns the filtered options based on the input. This where you need to implement your search. */
    filteredOptions: (searchInput: String) -> List<T>,
    /** Creates the row for the filtered down option in the menu. */
    optionToDropdownRow: @Composable (T) -> Unit = { option ->
        Text(optionToString(option))
    },
    /** Creates the view when [filteredOptions] returns a empty list. */
    noResultsRow: @Composable () -> Unit = {
        // By default, wrap in a menu item to get the same style
        DropdownMenuItem(
            onClick = {},
            text = {
                Text(
                    "No Matches Found",
                    style = MaterialTheme.typography.bodySmall,
                    color = MaterialTheme.colorScheme.secondary,
                    fontStyle = FontStyle.Italic,
                )
            },
        )
    },
    focusRequester: FocusRequester = remember { FocusRequester() },
    keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
    trailingIcon: @Composable (expanded: Boolean) -> Unit = { expanded ->
        ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded)
    },
    textFieldColors: TextFieldColors = ExposedDropdownMenuDefaults.textFieldColors(
        containerColor = Color.Transparent,
    ),
    bringIntoViewRequester: BringIntoViewRequester = remember { BringIntoViewRequester() },
    coroutineScope: CoroutineScope = rememberCoroutineScope(),
) {
    // Get our text for the selected option
    val selectedOptionText = remember(selectedOption) {
        selectedOption?.let { optionToString(it) }.orEmpty()
    }

    // Default our text input to the selected option
    var textInput by remember(selectedOptionText) {
        mutableStateOf(selectedOptionText)
    }

    var dropDownExpanded by remember { mutableStateOf(false) }

    // Update our filtered options everytime our text input changes
    val filteredOptions = remember(textInput, dropDownExpanded) {
        when (dropDownExpanded) {
            true -> filteredOptions(textInput)
            // Skip filtering when we don't need to
            false -> emptyList()
        }
    }

    val keyboardController = LocalSoftwareKeyboardController.current
    val focusManager = LocalFocusManager.current

    ExposedDropdownMenuBox(
        expanded = dropDownExpanded,
        onExpandedChange = { dropDownExpanded = !dropDownExpanded },
        modifier = modifier,
    ) {
        // Text Input
        OutlinedTextField(
            value = textInput,
            onValueChange = {
                // Dropdown may auto hide for scrolling but it's important it always shows when a user
                // does a search
                dropDownExpanded = true
                textInput = it
            },
            modifier = Modifier
                // Match the parent width
                .fillMaxWidth()
                .bringIntoViewRequester(bringIntoViewRequester)
                .menuAnchor()
                .focusRequester(focusRequester)
                .onFocusChanged { focusState ->
                    // When only 1 option left when we lose focus, selected it.
                    if (!focusState.isFocused) {
                        // Whenever we lose focus, always hide the dropdown
                        dropDownExpanded = false

                        when (filteredOptions.size) {
                            // Auto select the single option
                            1 -> if (filteredOptions.first() != selectedOption) {
                                onOptionSelected(filteredOptions.first())
                            }
                            // Nothing to we can auto select - reset our text input to the selected value
                            else -> textInput = selectedOptionText
                        }
                    } else {
                        // When focused:
                        // Ensure field is visible by scrolling to it
                        coroutineScope.launch {
                            bringIntoViewRequester.bringIntoView()
                        }
                        // Show the dropdown right away
                        dropDownExpanded = true
                    }
                },
            label = { Text(label) },
            trailingIcon = { trailingIcon(dropDownExpanded) },
            colors = textFieldColors,
            keyboardOptions = keyboardOptions.copy(
                imeAction = when (filteredOptions.size) {
                    // We will either reset input or auto select the single option
                    0, 1 -> ImeAction.Done
                    // Keyboard will hide to make room for search results
                    else -> ImeAction.Search
                }
            ),
            keyboardActions = KeyboardActions(
                onAny = {
                    when (filteredOptions.size) {
                        // Remove focus to execute our onFocusChanged effect
                        0, 1 -> focusManager.clearFocus(force = true)
                        // Can't auto select option since we have a list, so hide keyboard to give more room for dropdown
                        else -> keyboardController?.hide()
                    }
                }
            )
        )

        // Dropdown
        if (dropDownExpanded) {
            val dropdownOptions = remember(textInput) {
                if (textInput.isEmpty()) {
                    // Show all options if nothing to filter yet
                    options
                } else {
                    filteredOptions(textInput)
                }
            }

            ExposedDropdownMenu(
                expanded = dropDownExpanded,
                onDismissRequest = { dropDownExpanded = false },
            ) {
                if (dropdownOptions.isEmpty()) {
                    noResultsRow()
                } else {
                    dropdownOptions.forEach { option ->
                        DropdownMenuItem(
                            onClick = {
                                dropDownExpanded = false
                                onOptionSelected(option)
                                focusManager.clearFocus(force = true)
                            },
                            text = {
                                optionToDropdownRow(option)
                            }
                        )
                    }
                }
            }
        }
    }
}

@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
@Preview(showSystemUi = true)
@Composable
private fun PreviewTextFieldMenu() {
    data class PreviewOption(val text: String, val id: Int)

    var selectedOption by remember { mutableStateOf<PreviewOption?>(null) }
    val options = remember {
        listOf(
            PreviewOption("Option 1", 1),
            PreviewOption("Option 2", 2),
            PreviewOption("Option 3", 3),
            PreviewOption("Option 4", 4),
            PreviewOption("Option 5", 5),
        )
    }

    Column(
        modifier = Modifier
            // Reduce column height when keyboard is shown
            // Note: This needs to be set _before_ verticalScroll so that BringIntoViewRequester APIs work
            .imePadding()
            .verticalScroll(rememberScrollState())
            .padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(12.dp)
    ) {

        val nameFocusRequester = remember { FocusRequester() }
        val optionsFocusRequester = remember { FocusRequester() }

        var nameInput by remember { mutableStateOf("") }

        // Free Style Input
        OutlinedTextField(
            modifier = Modifier
                .focusRequester(nameFocusRequester)
                .fillMaxWidth(),
            label = {
                Text(
                    text = "Name",
                    overflow = TextOverflow.Ellipsis,
                    maxLines = 1,
                )
            },
            value = nameInput,
            onValueChange = { nameInput = it },
            singleLine = true,
            keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
            keyboardActions = KeyboardActions(
                onNext = { optionsFocusRequester.requestFocus() },
            ),
        )

        TextFieldMenu(
            modifier = Modifier.fillMaxWidth(),
            label = "Options",
            options = options,
            selectedOption = selectedOption,
            onOptionSelected = { selectedOption = it },
            optionToString = { it.text },
            filteredOptions = { searchInput ->
                options.filter { it.text.contains(searchInput, ignoreCase = true) }
            },
            focusRequester = optionsFocusRequester,
        )
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接