mirror of
https://github.com/BetterAndroid/FlexiUI.git
synced 2025-09-08 19:44:25 +08:00
feat: add auto complete box in TextField
This commit is contained in:
@@ -36,6 +36,7 @@ import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
|||||||
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
@@ -64,14 +65,18 @@ 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.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
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.text.input.VisualTransformation
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.PopupProperties
|
||||||
import com.highcapable.flexiui.LocalColors
|
import com.highcapable.flexiui.LocalColors
|
||||||
import com.highcapable.flexiui.LocalShapes
|
import com.highcapable.flexiui.LocalShapes
|
||||||
import com.highcapable.flexiui.LocalSizes
|
import com.highcapable.flexiui.LocalSizes
|
||||||
@@ -84,12 +89,11 @@ import com.highcapable.flexiui.utils.orElse
|
|||||||
import com.highcapable.flexiui.utils.solidColor
|
import com.highcapable.flexiui.utils.solidColor
|
||||||
import com.highcapable.flexiui.utils.status
|
import com.highcapable.flexiui.utils.status
|
||||||
|
|
||||||
// TODO: auto complete text box (possible a few long time later)
|
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class TextFieldColors(
|
data class TextFieldColors(
|
||||||
val cursorColor: Color,
|
val cursorColor: Color,
|
||||||
val selectionColors: TextSelectionColors,
|
val selectionColors: TextSelectionColors,
|
||||||
|
val completionColors: AutoCompleteBoxColors,
|
||||||
val decorInactiveTint: Color,
|
val decorInactiveTint: Color,
|
||||||
val decorActiveTint: Color,
|
val decorActiveTint: Color,
|
||||||
val borderInactiveColor: Color,
|
val borderInactiveColor: Color,
|
||||||
@@ -97,6 +101,12 @@ data class TextFieldColors(
|
|||||||
val backgroundColor: Color
|
val backgroundColor: Color
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class AutoCompleteBoxColors(
|
||||||
|
val highlightContentColor: Color,
|
||||||
|
val menuColors: DropdownMenuColors
|
||||||
|
)
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class TextFieldStyle(
|
data class TextFieldStyle(
|
||||||
val padding: Dp,
|
val padding: Dp,
|
||||||
@@ -106,18 +116,29 @@ data class TextFieldStyle(
|
|||||||
val endPadding: Dp,
|
val endPadding: Dp,
|
||||||
val shape: Shape,
|
val shape: Shape,
|
||||||
val borderInactive: BorderStroke,
|
val borderInactive: BorderStroke,
|
||||||
val borderActive: BorderStroke
|
val borderActive: BorderStroke,
|
||||||
|
val completionStyle: DropdownMenuStyle
|
||||||
|
)
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class AutoCompleteOptions(
|
||||||
|
val checkCase: Boolean = true,
|
||||||
|
val checkStartSpace: Boolean = true,
|
||||||
|
val checkEndSpace: Boolean = true,
|
||||||
|
val threshold: Int = 2
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TextField(
|
fun TextField(
|
||||||
value: TextFieldValue,
|
value: TextFieldValue,
|
||||||
onValueChange: (TextFieldValue) -> Unit,
|
onValueChange: (TextFieldValue) -> Unit,
|
||||||
|
completionValues: List<String> = emptyList(),
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
colors: TextFieldColors = TextField.colors,
|
colors: TextFieldColors = TextField.colors,
|
||||||
style: TextFieldStyle = TextField.style,
|
style: TextFieldStyle = TextField.style,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
readOnly: Boolean = false,
|
readOnly: Boolean = false,
|
||||||
|
completionOptions: AutoCompleteOptions = AutoCompleteOptions(),
|
||||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||||
singleLine: Boolean = false,
|
singleLine: Boolean = false,
|
||||||
@@ -210,6 +231,17 @@ fun TextField(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
AutoCompleteTextFieldBox(
|
||||||
|
value = value,
|
||||||
|
onValueChange = onValueChange,
|
||||||
|
completionValues = completionValues,
|
||||||
|
completionOptions = completionOptions,
|
||||||
|
completionColors = colors.completionColors,
|
||||||
|
completionStyle = style.completionStyle,
|
||||||
|
focusRequester = focusRequester,
|
||||||
|
dropdownMenuWidth = if (needInflatable) maxWidth else Dp.Unspecified,
|
||||||
|
textFieldAvailable = enabled && !readOnly && focused
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,11 +249,13 @@ fun TextField(
|
|||||||
fun TextField(
|
fun TextField(
|
||||||
value: String,
|
value: String,
|
||||||
onValueChange: (String) -> Unit,
|
onValueChange: (String) -> Unit,
|
||||||
|
completionValues: List<String> = emptyList(),
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
colors: TextFieldColors = TextField.colors,
|
colors: TextFieldColors = TextField.colors,
|
||||||
style: TextFieldStyle = TextField.style,
|
style: TextFieldStyle = TextField.style,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
readOnly: Boolean = false,
|
readOnly: Boolean = false,
|
||||||
|
completionOptions: AutoCompleteOptions = AutoCompleteOptions(),
|
||||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||||
singleLine: Boolean = false,
|
singleLine: Boolean = false,
|
||||||
@@ -243,11 +277,13 @@ fun TextField(
|
|||||||
textFieldValue = it
|
textFieldValue = it
|
||||||
onValueChange(it.text)
|
onValueChange(it.text)
|
||||||
},
|
},
|
||||||
|
completionValues = completionValues,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
colors = colors,
|
colors = colors,
|
||||||
style = style,
|
style = style,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
readOnly = readOnly,
|
readOnly = readOnly,
|
||||||
|
completionOptions = completionOptions,
|
||||||
keyboardOptions = keyboardOptions,
|
keyboardOptions = keyboardOptions,
|
||||||
keyboardActions = keyboardActions,
|
keyboardActions = keyboardActions,
|
||||||
singleLine = singleLine,
|
singleLine = singleLine,
|
||||||
@@ -385,11 +421,13 @@ fun PasswordTextField(
|
|||||||
fun BackspaceTextField(
|
fun BackspaceTextField(
|
||||||
value: TextFieldValue,
|
value: TextFieldValue,
|
||||||
onValueChange: (TextFieldValue) -> Unit,
|
onValueChange: (TextFieldValue) -> Unit,
|
||||||
|
completionValues: List<String> = emptyList(),
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
colors: TextFieldColors = TextField.colors,
|
colors: TextFieldColors = TextField.colors,
|
||||||
style: TextFieldStyle = TextField.style,
|
style: TextFieldStyle = TextField.style,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
readOnly: Boolean = false,
|
readOnly: Boolean = false,
|
||||||
|
completionOptions: AutoCompleteOptions = AutoCompleteOptions(),
|
||||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||||
singleLine: Boolean = false,
|
singleLine: Boolean = false,
|
||||||
@@ -406,11 +444,13 @@ fun BackspaceTextField(
|
|||||||
TextField(
|
TextField(
|
||||||
value = value,
|
value = value,
|
||||||
onValueChange = onValueChange,
|
onValueChange = onValueChange,
|
||||||
|
completionValues = completionValues,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
colors = colors,
|
colors = colors,
|
||||||
style = style,
|
style = style,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
readOnly = readOnly,
|
readOnly = readOnly,
|
||||||
|
completionOptions = completionOptions,
|
||||||
keyboardOptions = keyboardOptions,
|
keyboardOptions = keyboardOptions,
|
||||||
keyboardActions = keyboardActions,
|
keyboardActions = keyboardActions,
|
||||||
singleLine = singleLine,
|
singleLine = singleLine,
|
||||||
@@ -456,11 +496,13 @@ fun BackspaceTextField(
|
|||||||
fun BackspaceTextField(
|
fun BackspaceTextField(
|
||||||
value: String,
|
value: String,
|
||||||
onValueChange: (String) -> Unit,
|
onValueChange: (String) -> Unit,
|
||||||
|
completionValues: List<String> = emptyList(),
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
colors: TextFieldColors = TextField.colors,
|
colors: TextFieldColors = TextField.colors,
|
||||||
style: TextFieldStyle = TextField.style,
|
style: TextFieldStyle = TextField.style,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
readOnly: Boolean = false,
|
readOnly: Boolean = false,
|
||||||
|
completionOptions: AutoCompleteOptions = AutoCompleteOptions(),
|
||||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||||
singleLine: Boolean = false,
|
singleLine: Boolean = false,
|
||||||
@@ -481,11 +523,13 @@ fun BackspaceTextField(
|
|||||||
textFieldValue = it
|
textFieldValue = it
|
||||||
onValueChange(it.text)
|
onValueChange(it.text)
|
||||||
},
|
},
|
||||||
|
completionValues = completionValues,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
colors = colors,
|
colors = colors,
|
||||||
style = style,
|
style = style,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
readOnly = readOnly,
|
readOnly = readOnly,
|
||||||
|
completionOptions = completionOptions,
|
||||||
keyboardOptions = keyboardOptions,
|
keyboardOptions = keyboardOptions,
|
||||||
keyboardActions = keyboardActions,
|
keyboardActions = keyboardActions,
|
||||||
singleLine = singleLine,
|
singleLine = singleLine,
|
||||||
@@ -501,6 +545,88 @@ fun BackspaceTextField(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AutoCompleteTextFieldBox(
|
||||||
|
value: TextFieldValue,
|
||||||
|
onValueChange: (TextFieldValue) -> Unit,
|
||||||
|
completionValues: List<String>,
|
||||||
|
completionOptions: AutoCompleteOptions,
|
||||||
|
completionColors: AutoCompleteBoxColors,
|
||||||
|
completionStyle: DropdownMenuStyle,
|
||||||
|
focusRequester: FocusRequester,
|
||||||
|
dropdownMenuWidth: Dp,
|
||||||
|
textFieldAvailable: Boolean
|
||||||
|
) {
|
||||||
|
if (completionValues.isEmpty()) return
|
||||||
|
// We need to use some "last" to remember the last using data,
|
||||||
|
// because we need to mantain the animation state of the dropdown menu.
|
||||||
|
// This allows the animation to finish playing due to the next composable event.
|
||||||
|
var lastHandingModified by remember { mutableStateOf(false) }
|
||||||
|
var lastMatchedValue by remember { mutableStateOf("") }
|
||||||
|
var lastInputLength by remember { mutableStateOf(0) }
|
||||||
|
var lastMatchedValues by remember { mutableStateOf(listOf<String>()) }
|
||||||
|
val inputText = value.text.let {
|
||||||
|
var currentText = it
|
||||||
|
when {
|
||||||
|
!completionOptions.checkStartSpace -> currentText = currentText.trimStart()
|
||||||
|
!completionOptions.checkEndSpace -> currentText = currentText.trimEnd()
|
||||||
|
}; currentText
|
||||||
|
}
|
||||||
|
val hasInput = inputText.isNotEmpty()
|
||||||
|
val matchedValues = completionValues.filter {
|
||||||
|
if (inputText.length >= completionOptions.threshold)
|
||||||
|
if (completionOptions.checkCase)
|
||||||
|
it.startsWith(inputText)
|
||||||
|
else it.lowercase().startsWith(inputText.lowercase())
|
||||||
|
else false
|
||||||
|
}.sortedBy { it.length }
|
||||||
|
if (matchedValues.isNotEmpty() && !lastHandingModified) {
|
||||||
|
lastMatchedValues = matchedValues
|
||||||
|
lastInputLength = inputText.length
|
||||||
|
}
|
||||||
|
val matchText = if (completionOptions.checkCase)
|
||||||
|
lastMatchedValue != inputText
|
||||||
|
else lastMatchedValue.lowercase() != inputText.lowercase()
|
||||||
|
val expanded = hasInput && matchedValues.isNotEmpty() && matchText
|
||||||
|
// As long as it is expanded, reset the lastHandingModified, lastMatchedValue.
|
||||||
|
if (expanded) {
|
||||||
|
lastHandingModified = false
|
||||||
|
lastMatchedValue = ""
|
||||||
|
}
|
||||||
|
// Clearly, if the text field is not available,
|
||||||
|
// the dropdown menu should not be displayed when reavailable.
|
||||||
|
if (!textFieldAvailable && matchedValues.isNotEmpty()) lastMatchedValue = inputText
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = expanded && textFieldAvailable,
|
||||||
|
onDismissRequest = {},
|
||||||
|
modifier = dropdownMenuWidth.orElse()?.let { Modifier.width(it) } ?: Modifier.width(IntrinsicSize.Max),
|
||||||
|
colors = completionColors.menuColors,
|
||||||
|
style = completionStyle,
|
||||||
|
properties = PopupProperties(focusable = false)
|
||||||
|
) {
|
||||||
|
lastMatchedValues.forEach { matchedValue ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
onClick = {
|
||||||
|
val newValue = TextFieldValue(matchedValue, TextRange(matchedValue.length))
|
||||||
|
lastHandingModified = true
|
||||||
|
lastMatchedValue = matchedValue
|
||||||
|
onValueChange(newValue)
|
||||||
|
focusRequester.requestFocus()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(buildAnnotatedString {
|
||||||
|
append(matchedValue)
|
||||||
|
addStyle(
|
||||||
|
style = SpanStyle(color = completionColors.highlightContentColor, fontWeight = FontWeight.Bold),
|
||||||
|
start = 0,
|
||||||
|
end = lastInputLength
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun InnerDecorationBox(
|
private fun InnerDecorationBox(
|
||||||
decorTint: Color,
|
decorTint: Color,
|
||||||
@@ -597,6 +723,10 @@ private fun defaultTextFieldColors() = TextFieldColors(
|
|||||||
handleColor = LocalColors.current.themePrimary,
|
handleColor = LocalColors.current.themePrimary,
|
||||||
backgroundColor = LocalColors.current.themeSecondary
|
backgroundColor = LocalColors.current.themeSecondary
|
||||||
),
|
),
|
||||||
|
completionColors = AutoCompleteBoxColors(
|
||||||
|
highlightContentColor = LocalColors.current.themePrimary,
|
||||||
|
menuColors = DropdownMenu.colors
|
||||||
|
),
|
||||||
decorInactiveTint = LocalColors.current.themeSecondary,
|
decorInactiveTint = LocalColors.current.themeSecondary,
|
||||||
decorActiveTint = LocalColors.current.themePrimary,
|
decorActiveTint = LocalColors.current.themePrimary,
|
||||||
borderInactiveColor = LocalColors.current.themeSecondary,
|
borderInactiveColor = LocalColors.current.themeSecondary,
|
||||||
@@ -617,7 +747,8 @@ private fun defaultTextFieldStyle() = TextFieldStyle(
|
|||||||
else -> LocalShapes.current.secondary
|
else -> LocalShapes.current.secondary
|
||||||
},
|
},
|
||||||
borderInactive = defaultTextFieldInactiveBorder(),
|
borderInactive = defaultTextFieldInactiveBorder(),
|
||||||
borderActive = defaultTextFieldActiveBorder()
|
borderActive = defaultTextFieldActiveBorder(),
|
||||||
|
completionStyle = DropdownMenu.style
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
Reference in New Issue
Block a user