mirror of
https://github.com/BetterAndroid/FlexiUI.git
synced 2025-09-08 19:44:25 +08:00
feat: add keyboard controller for auto complete box in TextField
This commit is contained in:
@@ -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,
|
||||||
|
Reference in New Issue
Block a user