Jetpack Compose 是否提供 Material AutoComplete TextView 替代品?

Does Jetpack Compose offer a Material AutoComplete TextView replacement?

在将我的应用程序迁移到 Jetpack compose 的过程中,我遇到了我的应用程序的一部分,其中 TextField 需要自动完成功能。

但是,从版本 1.0.0-alpha05 开始,我找不到任何使用 Compose API 来实现此目的的功能。我发现最接近的是 DropdownMenuDropdownMenuItem 可组合项,但似乎需要大量手动管道才能从中创建自动完成菜单。

当然,显而易见的事情就是等待 Jetpack Compose 的未来更新。但是我想知道,有没有人在他们的迁移中遇到这个问题找到了解决方案?

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

撰写屏幕

检查我使用 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")
                }
            }
        }
    }
}

至少在 v1.0.2 之前没有

所以我在 this gist

中实现了一个很好用的 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"
    )

从 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 = selectedOptionText,
        onValueChange = { selectedOptionText = 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)
                }
            }
        }
    }
}