Jetpack Compose是否提供了材料设计的自动完成文本框替代方案?

10
在将我的应用迁移到Jetpack Compose的过程中,我遇到了一个需要自动完成功能的TextField部分。
然而,在版本1.0.0-alpha05中,我无法找到任何使用Compose API来实现此功能的方法。我所能找到的最接近的就是DropdownMenu和DropdownMenuItem组合,但似乎需要大量手动的操作才能创建自动完成菜单。
当然,显而易见的做法是等待Jetpack Compose未来的更新。但我想知道,在他们的迁移过程中遇到这个问题的人是否已经找到了解决方案?

https://dev59.com/hlEG5IYBdhLWcg3wX7pa#67111599 - Gabriele Mariotti
5个回答

9

至少到 v1.0.2 版本为止还没有。

因此,我在 Compose 中实现了一个不错的可用性,可以在这个代码片段中找到。

我也把它放在这里:

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.window.PopupProperties


@Composable
fun TextFieldWithDropdown(
    modifier: Modifier = Modifier,
    value: TextFieldValue,
    setValue: (TextFieldValue) -> Unit,
    onDismissRequest: () -> Unit,
    dropDownExpanded: Boolean,
    list: List<String>,
    label: String = ""
) {
    Box(modifier) {
        TextField(
            modifier = Modifier
                .fillMaxWidth()
                .onFocusChanged { focusState ->
                    if (!focusState.isFocused)
                        onDismissRequest()
                },
            value = value,
            onValueChange = setValue,
            label = { Text(label) },
            colors = TextFieldDefaults.outlinedTextFieldColors()
        )
        DropdownMenu(
            expanded = dropDownExpanded,
            properties = PopupProperties(
                focusable = false,
                dismissOnBackPress = true,
                dismissOnClickOutside = true
            ),
            onDismissRequest = onDismissRequest
        ) {
            list.forEach { text ->
                DropdownMenuItem(onClick = {
                    setValue(
                        TextFieldValue(
                            text,
                            TextRange(text.length)
                        )
                    )
                }) {
                    Text(text = text)
                }
            }
        }
    }
}

如何使用它

val all = listOf("aaa", "baa", "aab", "abb", "bab")

val dropDownOptions = mutableStateOf(listOf<String>())
val textFieldValue = mutableStateOf(TextFieldValue())
val dropDownExpanded = mutableStateOf(false)
fun onDropdownDismissRequest() {
    dropDownExpanded.value = false
}

fun onValueChanged(value: TextFieldValue) {
    dropDownExpanded.value = true
    textFieldValue.value = value
    dropDownOptions.value = all.filter { it.startsWith(value.text) && it != value.text }.take(3)
}

@Composable
fun TextFieldWithDropdownUsage() {
    TextFieldWithDropdown(
        modifier = Modifier.fillMaxWidth(),
        value = textFieldValue.value,
        setValue = ::onValueChanged,
        onDismissRequest = ::onDropdownDismissRequest,
        dropDownExpanded = dropDownExpanded.value,
        list = dropDownOptions.value,
        label = "Label"
    )

5
从Compose 1.1.0-alpha06版本开始,Compose Material现在提供了一个ExposedDropdownMenu组合项,API here,可用于实现一个下拉菜单,以便简化自动完成过程。实际的自动完成逻辑需要自己实现。
API文档给出了以下用法示例,用于可编辑字段:
val options = listOf("Option 1", "Option 2", "Option 3", "Option 4", "Option 5")
var exp by remember { mutableStateOf(false) }
var selectedOption by remember { mutableStateOf("") }
ExposedDropdownMenuBox(expanded = exp, onExpandedChange = { exp = !exp }) {
    TextField(
        value = selectedOption,
        onValueChange = { selectedOption = it },
        label = { Text("Label") },
        trailingIcon = {
            ExposedDropdownMenuDefaults.TrailingIcon(expanded = exp)
        },
        colors = ExposedDropdownMenuDefaults.textFieldColors()
    )
    // filter options based on text field value (i.e. crude autocomplete)
    val filterOpts = options.filter { it.contains(selectedOption, ignoreCase = true) }
    if (filterOpts.isNotEmpty()) {
        ExposedDropdownMenu(expanded = exp, onDismissRequest = { exp = false }) {
            filterOpts.forEach { option ->
                DropdownMenuItem(
                    onClick = {
                        selectedOption = option
                        exp = false
                    }
                ) {
                    Text(text = option)
                }
            }
        }
    }
}

2
谢谢提供样本链接。唯一不起作用的是筛选功能。弹出菜单总是关闭得很奇怪。还有其他人遇到这个问题吗? - c_idle
@c_idle 可能是由于新版本的Compose(使用v1.4.3)或Material3,但我不得不调整DropdownMenuItem以在构造函数中声明Text()组件,以及onclick(),而不是在DropdownMenuItem的body中作为一个组合体(使用最后一个组合体参数的东西,忘记了那个名字)。此外,在调用.menuAchor()函数时,你需要在TextField上指定一个Modifier,否则你将得到关于Focusable的异常。它可以工作,只是如果该字符存在于下拉选项中,我只能在文本字段中输入单个字符。 - clamum
在使用v1.5.0-beta2版本时,即使TextField处于焦点状态并且正在输入,ExposedDropdownMenu#onDismissRequest方法仍会被调用。 - Alireza Farahani

1
使用XML编写的代码,将其作为Compose中的布局使用,使用AndroidView。我们可以使用这个解决方案,直到它默认包含在Compose中。 您可以根据需要自定义并进行样式设置。我已经在我的项目中亲自尝试过,并且它运行良好。
<!-- text_input_field.xml -->
<!-- You can style your textfield here in XML with styles -->
<!-- this file should be in res/layout -->

<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.textfield.TextInputLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <AutoCompleteTextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Label"
        android:inputType="none" />

</com.google.android.material.textfield.TextInputLayout>

// TextFieldWithDropDown.kt
// TextField with dropdown was not included by default in jetpack compose (1.0.2) and less

@Composable
fun TextFieldWithDropDown(
    items: List<String>,
    selectedValue: String?,
    modifier: Modifier = Modifier,
    onSelect: (Int) -> Unit
) {
    AndroidView(
        factory = { context ->
            val textInputLayout = TextInputLayout
                .inflate(context, R.layout.text_input_field, null) as TextInputLayout
            
            // If you need to use different styled layout for light and dark themes
            // you can create two different xml layouts one for light and another one for dark
            // and inflate the one you need here.

            val autoCompleteTextView = textInputLayout.editText as? AutoCompleteTextView
            val adapter = ArrayAdapter(context, android.R.layout.simple_list_item_1, items)
            autoCompleteTextView?.setAdapter(adapter)
            autoCompleteTextView?.setText(selectedValue, false)
            autoCompleteTextView?.setOnItemClickListener { _, _, index, _ -> onSelect(index) }
            textInputLayout
        },
        update = { textInputLayout ->
            // This block will be called when recomposition happens
            val autoCompleteTextView = textInputLayout.editText as? AutoCompleteTextView
            val adapter = ArrayAdapter(textInputLayout.context, android.R.layout.simple_list_item_1, items)
            autoCompleteTextView?.setAdapter(adapter)
            autoCompleteTextView?.setText(selectedValue, false)
        },
        modifier = modifier
    )
}

// MainActivity.kt
// It's important to use AppCompatActivity instead of ComponentActivity to get the material
// look on our XML based textfield

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        setContent {
            Column {
                TextFieldWithDropDown(
                    items = listOf("One", "Two", "Three"),
                    selectedValue = "Two",
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(16.dp)
                ) {
                    // You can also set the value to a state
                    index -> println("$index was selected")
                }
            }
        }
    }
}

虽然这个链接可能回答了问题,但最好在此处包含答案的基本部分并提供参考链接。如果链接页面更改,仅有链接的答案可能会失效。-【来自审查】 - SimpleCoder
嘿,谢谢你让我知道了。因为我是新来的,所以没有想到这一点,现在我已经把代码包含在答案中了。 - Jack

0

正如你所说,目前还没有这样的组件。你有两个选择:使用DropDownMenuBaseTextField创建自己的自定义组件,或者通过androidx.compose.ui.platform.ComposeView使用混合xml-autocomplete并组合屏幕。


1
你是否已经制作了像你描述的自定义可组合件?看到一个完成解决方案的示例会很好。 - machfour
1
很抱歉我没有实现那个功能,但是我知道一个库可以作为AutoCompleteText使用(你需要实现一些东西,但是这个库会节省很多时间)。请查看:https://github.com/skydoves/Orchestra#spinner - Agna JirKon Rx

0

经过这么多的研究,我已经创建了完美的自动完成功能,就像AutoCompleteView一样工作。

只需复制粘贴并在您的项目中使用。


import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DropDown(
    items: List<String>,
    onItemSelected: (String) -> Unit,
    selectedItem: String,
    label: String,
    modifier: Modifier = Modifier,
) {
    var expanded by remember { mutableStateOf(false) }
    var listItems by remember(items, selectedItem) {
        mutableStateOf(
            if (selectedItem.isNotEmpty()) {
                items.filter { x -> x.startsWith(selectedItem.lowercase(), ignoreCase = true) }
            } else {
                items.toList()
            }
        )
    }
    var selectedText by remember(selectedItem) { mutableStateOf(selectedItem) }
    
    LaunchedEffect(selectedItem){
        selectedText = selectedItem
    }

    Box(
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = 16.dp, vertical = 2.dp)
    ) {
        ExposedDropdownMenuBox(
            expanded = expanded,
            onExpandedChange = {
                expanded = !expanded
            },
            modifier = Modifier.fillMaxWidth()
        ) {
            TextField(
                value = selectedText,
                label = { Text(label) },
                onValueChange = {
                    if (!expanded) {
                        expanded = true
                    }
                    selectedText = it
                    listItems = if (it.isNotEmpty()) {
                        items.filter { x -> x.startsWith(it.lowercase(), ignoreCase = true) }
                    } else {
                        items.toList()
                    }
                },
                trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
                modifier = Modifier
                    .fillMaxWidth()
                    .menuAnchor()
            )

            ExposedDropdownMenu(
                expanded = expanded,
                onDismissRequest = { expanded = false }
            ) {
                if (listItems.isEmpty()) {
                    DropdownMenuItem(
                        text = { Text(text = "No items found") },
                        onClick = {
                            expanded = false
                        }
                    )
                } else {
                    listItems.forEach { item ->
                        DropdownMenuItem(
                            text = { Text(text = item) },
                            onClick = {
                                selectedText = item
                                expanded = false
                                onItemSelected(item)
                            }
                        )
                    }
                }
            }
        }
    }
}



1
你的回答可以通过提供更多支持性信息来改进。请编辑以添加进一步的细节,例如引用或文档,以便他人能够确认你的回答是否正确。你可以在帮助中心找到关于如何撰写好回答的更多信息。 - Community

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