feat: add keyboard controller for auto complete box in TextField

This commit is contained in:
2023-11-21 09:31:08 +08:00
parent 97fb1abbcb
commit c2b58a368d

View File

@@ -65,6 +65,12 @@ import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.TextRange
@@ -155,6 +161,7 @@ fun TextField(
) { ) {
val focused by interactionSource.collectIsFocusedAsState() val focused by interactionSource.collectIsFocusedAsState()
val hovered by interactionSource.collectIsHoveredAsState() val hovered by interactionSource.collectIsHoveredAsState()
val keyEventFactory = TextFieldKeyEventFactory()
val animatedDecorTint by animateColorAsState(when { val animatedDecorTint by animateColorAsState(when {
focused || hovered -> colors.decorActiveTint focused || hovered -> colors.decorActiveTint
else -> colors.decorInactiveTint else -> colors.decorInactiveTint
@@ -199,7 +206,9 @@ fun TextField(
BasicTextField( BasicTextField(
value = value, value = value,
onValueChange = onValueChange, onValueChange = onValueChange,
modifier = Modifier.focusRequester(focusRequester).inflatable(), modifier = Modifier.inflatable()
.focusRequester(focusRequester)
.onKeyEvent { keyEventFactory.onKeyEvent?.invoke(it) ?: false },
enabled = enabled, enabled = enabled,
readOnly = readOnly, readOnly = readOnly,
textStyle = textStyle, textStyle = textStyle,
@@ -239,6 +248,7 @@ fun TextField(
completionColors = colors.completionColors, completionColors = colors.completionColors,
completionStyle = style.completionStyle, completionStyle = style.completionStyle,
focusRequester = focusRequester, focusRequester = focusRequester,
keyEventFactory = keyEventFactory,
dropdownMenuWidth = if (needInflatable) maxWidth else Dp.Unspecified, dropdownMenuWidth = if (needInflatable) maxWidth else Dp.Unspecified,
textFieldAvailable = enabled && !readOnly && focused textFieldAvailable = enabled && !readOnly && focused
) )
@@ -554,6 +564,7 @@ private fun AutoCompleteTextFieldBox(
completionColors: AutoCompleteBoxColors, completionColors: AutoCompleteBoxColors,
completionStyle: DropdownMenuStyle, completionStyle: DropdownMenuStyle,
focusRequester: FocusRequester, focusRequester: FocusRequester,
keyEventFactory: TextFieldKeyEventFactory,
dropdownMenuWidth: Dp, dropdownMenuWidth: Dp,
textFieldAvailable: Boolean textFieldAvailable: Boolean
) { ) {
@@ -565,6 +576,7 @@ private fun AutoCompleteTextFieldBox(
var lastMatchedValue by remember { mutableStateOf("") } var lastMatchedValue by remember { mutableStateOf("") }
var lastInputLength by remember { mutableStateOf(0) } var lastInputLength by remember { mutableStateOf(0) }
var lastMatchedValues by remember { mutableStateOf(listOf<String>()) } var lastMatchedValues by remember { mutableStateOf(listOf<String>()) }
var selection by remember { mutableStateOf(-1) }
val inputText = value.text.let { val inputText = value.text.let {
var currentText = it var currentText = it
when { when {
@@ -592,10 +604,40 @@ private fun AutoCompleteTextFieldBox(
if (expanded) { if (expanded) {
lastHandingModified = false lastHandingModified = false
lastMatchedValue = "" lastMatchedValue = ""
} else selection = -1
fun collapse() {
lastMatchedValue = inputText
}
fun selectAndCollapse(position: Int) {
if (position < 0) return
val newValue = TextFieldValue(matchedValues[position], TextRange(matchedValues[position].length))
lastHandingModified = true
lastMatchedValue = matchedValues[position]
onValueChange(newValue)
focusRequester.requestFocus()
}
keyEventFactory.onKeyEvent = {
var release = true
if (it.type == KeyEventType.KeyUp) when (it.key) {
Key.Escape -> collapse()
Key.DirectionUp -> {
if (selection > 0)
selection--
else selection = matchedValues.lastIndex
}
Key.DirectionDown -> {
if (selection < matchedValues.lastIndex)
selection++
else selection = 0
}
Key.DirectionRight, Key.Enter -> selectAndCollapse(selection)
else -> release = false
} else release = false
release
} }
// Clearly, if the text field is not available, // Clearly, if the text field is not available,
// the dropdown menu should not be displayed when reavailable. // the dropdown menu should not be displayed when reavailable.
if (!textFieldAvailable && matchedValues.isNotEmpty()) lastMatchedValue = inputText if (!textFieldAvailable && matchedValues.isNotEmpty()) collapse()
DropdownMenu( DropdownMenu(
expanded = expanded && textFieldAvailable, expanded = expanded && textFieldAvailable,
onDismissRequest = {}, onDismissRequest = {},
@@ -604,15 +646,10 @@ private fun AutoCompleteTextFieldBox(
style = completionStyle, style = completionStyle,
properties = PopupProperties(focusable = false) properties = PopupProperties(focusable = false)
) { ) {
lastMatchedValues.forEach { matchedValue -> lastMatchedValues.forEachIndexed { index, matchedValue ->
DropdownMenuItem( DropdownMenuItem(
onClick = { onClick = { selectAndCollapse(index) },
val newValue = TextFieldValue(matchedValue, TextRange(matchedValue.length)) actived = selection == index
lastHandingModified = true
lastMatchedValue = matchedValue
onValueChange(newValue)
focusRequester.requestFocus()
}
) { ) {
Text(buildAnnotatedString { Text(buildAnnotatedString {
append(matchedValue) append(matchedValue)
@@ -669,6 +706,11 @@ internal expect fun Modifier.pointerHoverState(state: TextFieldPointerState): Mo
@Stable @Stable
internal enum class TextFieldPointerState { NORMAL, TEXT } internal enum class TextFieldPointerState { NORMAL, TEXT }
@Stable
private class TextFieldKeyEventFactory {
var onKeyEvent: ((KeyEvent) -> Boolean)? = null
}
private fun Modifier.textField( private fun Modifier.textField(
colors: TextFieldColors, colors: TextFieldColors,
style: TextFieldStyle, style: TextFieldStyle,