You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android Compose:将ExposedDropdownMenu锚定至TextField光标

解决Jetpack Compose下拉建议菜单位置问题

问题描述

使用Jetpack Compose开发自动完成输入框,需求是用户输入时显示下拉建议且TextField不丢失焦点,期望下拉菜单显示在TextField下方。但当前用ExposedDropdownMenuBox实现时,菜单显示在了TextField上方,当前代码及效果如下:

当前代码:

@Composable
fun AutoCompleteTextView(
    modifier: Modifier = Modifier,
    textFieldValue: TextFieldValue = TextFieldValue(),
    onTextChanged: ((TextFieldValue) -> Unit) = ::onTextChnaged,
    textStyle: TextStyle = TextStyle.Default,
    singleLine: Boolean = false
) {
    val options = listOf("Option 1", "Option 2", "Option 3", "Option 4", "Option 5")

    var isDropdownVisible by remember { mutableStateOf(false) }
    var expanded by remember { mutableStateOf(false) }
    var selectedOptionText by remember { mutableStateOf("") }

    ExposedDropdownMenuBox(
        modifier = Modifier,
        expanded = expanded,
        onExpandedChange = { expanded = it }) {
        TextField(
            value = textFieldValue,
            onValueChange = { value ->
                onTextChanged(value)
                value.selection.start
                isDropdownVisible = value.text.isNotEmpty() && value.text.first() == '@'
            },
            label = { Text("Enter your text here.") },
            trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
            textStyle = textStyle,
            singleLine = singleLine,
            modifier = modifier.menuAnchor(),
        )

        // filter options based on text field value
        val filteringOptions = options.filter { it.contains(selectedOptionText, ignoreCase = true) }
        if (filteringOptions.isNotEmpty()) {
            ExposedDropdownMenu(
                expanded = expanded,
                onDismissRequest = { expanded = false },
            ) {
                filteringOptions.forEach { selectionOption ->
                    DropdownMenuItem(
                        text = { Text(selectionOption) },
                        onClick = {
                            selectedOptionText = selectionOption
                            expanded = false
                        },
                        contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
                    )
                }
            }
        }
    }

    BackHandler(enabled = isDropdownVisible) {
        isDropdownVisible = false
    }
}

fun onTextChnaged(textFieldValue: TextFieldValue): Unit { }

当前效果截图:
当前下拉菜单显示在TextField上方

问题分析及解决方案

关键问题点

  1. 菜单位置自动调整:ExposedDropdownMenu默认会根据屏幕剩余空间自动选择显示位置,若TextField下方空间不足,会自动跳到上方。
  2. 状态联动错误:isDropdownVisible和expanded两个状态未联动,导致菜单展开逻辑混乱。
  3. 过滤条件错误:使用selectedOptionText过滤选项,而非用户当前输入的文本,无法实现实时匹配。

修改后的代码

@Composable
fun AutoCompleteTextView(
    modifier: Modifier = Modifier,
    textFieldValue: TextFieldValue = TextFieldValue(),
    onTextChanged: ((TextFieldValue) -> Unit) = {},
    textStyle: TextStyle = TextStyle.Default,
    singleLine: Boolean = false
) {
    val options = listOf("Option 1", "Option 2", "Option 3", "Option 4", "Option 5")

    var expanded by remember { mutableStateOf(false) }
    // 基于输入框中@后的文本实时过滤选项
    val filteringOptions = options.filter { 
        textFieldValue.text.isNotEmpty() && 
        it.contains(textFieldValue.text.substringAfterLast('@'), ignoreCase = true) 
    }

    ExposedDropdownMenuBox(
        expanded = expanded,
        onExpandedChange = { expanded = it }
    ) {
        TextField(
            value = textFieldValue,
            onValueChange = { value ->
                onTextChanged(value)
                // 输入文本包含@且非空时自动展开菜单
                expanded = value.text.isNotEmpty() && value.text.contains('@')
            },
            label = { Text("Enter your text here.") },
            trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
            textStyle = textStyle,
            singleLine = singleLine,
            modifier = modifier.menuAnchor(),
            // 避免输入法收起导致焦点丢失
            keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.None)
        )

        if (filteringOptions.isNotEmpty() && expanded) {
            ExposedDropdownMenu(
                expanded = expanded,
                onDismissRequest = { expanded = false },
                // 强制菜单显示在TextField下方
                modifier = Modifier.exposedDropdownSize(),
                placement = DropdownMenuPlacement.Below
            ) {
                filteringOptions.forEach { selectionOption ->
                    DropdownMenuItem(
                        text = { Text(selectionOption) },
                        onClick = {
                            // 替换输入框中@后的内容为选中选项
                            val newText = textFieldValue.text.substringBeforeLast('@') + "@$selectionOption"
                            onTextChanged(textFieldValue.copy(text = newText))
                            expanded = false
                        },
                        contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
                    )
                }
            }
        }
    }

    BackHandler(enabled = expanded) {
        expanded = false
    }
}

修改说明

  1. 合并状态逻辑:移除冗余的isDropdownVisible,直接用expanded控制菜单显示,简化状态管理。
  2. 实时过滤匹配:使用输入框中@后的文本作为过滤条件,确保下拉选项与用户输入实时匹配。
  3. 强制菜单位置:通过placement = DropdownMenuPlacement.Below强制菜单显示在TextField下方,Modifier.exposedDropdownSize()保证菜单宽度与输入框一致。
  4. 保持输入焦点:设置imeAction = ImeAction.None,避免点击菜单时输入法收起导致TextField丢失焦点。
  5. 优化选中交互:选中选项后直接更新输入框文本,符合自动完成的预期交互逻辑。

内容的提问来源于stack exchange,提问作者Kartheek

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 00:35:05