前语
闲话
在我之前的文章 《Compose For Desktop 实践:运用 Compose-jb 做一个时间水印帮手》 中,我埋了一个坑,关于在 Compose 中怎么过滤 TextField 的输入内容。时隔好几个月了,今日这篇文章便是来填这个坑的。
为什么需求增加过滤
在正式开端之前,咱们先来答复一下标题这个问题,为什么需求过滤呢?
众所周知,在安卓的原生 View 体系中,输入框是 EditText 咱们能够通过在 xml 布局文件中增加 inputType
特点来指定预设的几个答应的输入内容格式,例如:number
、numberDecimal
、numberSigned
等特点,别离表明过滤输入成果仅答应输入数字、十进制数字、带符号数字。别的,咱们也能够通过给 EditText 自界说承继自 InputFilter
的过滤办法(setFilters
)来自界说咱们自己的过滤规矩。
可是在 Compose 中咱们应该怎么去做呢?
在 Compose 中,输入框是 TextField
,查遍 TextField
的参数列表,咱们就会发现,并没有给咱们供给任何相似于 EditText 的过滤参数。
最多只能找到一个 keyboardOptions: KeyboardOptions = KeyboardOptions.Default
参数,而这个参数也仅仅请求输入法展示咱们要求的输入类型,例如只展示数字键盘,可是,这儿仅仅请求,并不是要求,所以人家输入法还不一定理呢,哈哈哈。并且这个参数并不能约束输入框的内容,仅仅更改了默许弹出软键盘的类型而已。
其实想想也不难了解,究竟 Compose 是声明式 UI ,怎么处理输入内容的确应该由咱们自己来完成。
可是假如官方能供给几个相似 EditText 的预设过滤参数就好了,惋惜并没有。
所以咱们需求自己完成对输入内容的过滤。
回到标题内容,为什么咱们需求过滤输入内容?
这儿咱们将以上文中中说到的 时间水印帮手 为比方讲解:

在这个界面中,咱们需求输入多个参数。
比方参数 “导出图画质量” ,咱们需求将输入内容约束在 0.0 – 1.0 的浮点数。
当然,咱们完全能够不在输入时约束,能够答应用户随意输入恣意内容,然后在实践提交数据时再做校验,可是,明显,这样是不合理的,用户体验也欠安。
所以咱们最好仍是能直接在用户输入时就做好约束。
完成
先简略试一下
其实想做过滤也不是不行,想想好像仍是挺简略的嘛,这儿以简略的约束输入内容长度(相似 EditTExt 中的 maxLength 特点)为比方举例:
var inputValue by remember { mutableStateOf("") }
TextField(
value = inputValue,
onValueChange = { inputValue = it }
)
或许读者们会说,嗨,不便是约束输入长度嘛,这在声明式 UI 中都不叫事,看我直接这样就行了:
val maxLength = 8
var inputValue by remember { mutableStateOf("") }
TextField(
value = inputValue,
onValueChange = {
if (it.length <= maxLength) inputValue = it
}
)
咱们在输入值改动时加一个判断,只要输入值的长度小于了界说的最大长度咱们才改动 inputValue
的值。
咋一看,好想没有问题是吧?
可是,你再仔细想想。
真的这么简略吗?
你有没有想过以下两种状况:
- 咱们在现已输入了 8 个字符后,把光标移动到中心方位,此刻再输入内容,你猜会产生什么?
- 咱们在输入了缺乏8个的字符(例如 5 个后),一起张贴超过约束字符数的内容(例如 4 个),你猜会产生什么?
不卖关子了,其实关于 状况 1 ,会出现内容的确没有持续增加了,可是光标会往后走的状况:

而关于 状况 2 ,相信不必我说,读者也能猜出来了,那便是张贴后没有任何反响。
没错,明显由于咱们在 onValueChange
中加了判断,假如当时输入的值 (it
) 大于了约束的值(maxLength
)那么咱们将不会做任何响应。可是这个明显是不合理的,由于尽管咱们张贴的所有内容直接刺进输入框的话的确会超出最大字符约束,可是并不是说输入框不能再输入内容了,很明显,输入框还能够接受再输入 3 个字符。所以咱们应该做的处理是将新输入的内容切断,截取契合数量的内容刺进输入框,多余的内容直接舍弃。
原生 View 的 EditText 也是这样的处理逻辑。
那么,现在咱们应该怎么做呢?
实践一下,约束输入字符长度
通过上面的小试牛刀,相信咱们也知道了,关于约束输入内容,不能简略的直接对输入的 String 做处理,而应该考虑到更多的状况,其间最需求重视的状况有两点:一是对输入框光标的操控;二是对挑选多个字符和张贴多个字符状况的处理(由于正常输入能够保证每次只输入(或删去)一个字符,可是张贴或多选后不一定)。
很明显,假如想操控光标的话,咱们不能直接运用 value
为 String 的 TextField
而应该改用运用 TextFieldValue
:
var inputValue by remember { mutableStateOf(TextFieldValue()) }
OutlinedTextField(
value = inputValue,
onValueChange = { }
)
TextFieldValue
是一个封装了输入内容(text: String
)、和挑选以及光标状况(selection: TextRange
)的类。
其间 TextRange
有两个参数 start
、end
别离表明选中文本时的开端和完毕方位,假如两个值相等则表明没有选中任何文本,此刻 TextRange
表明的是光标方位。
现在,咱们现已具备了能够处理上面说的两点问题的前置条件,下面便是应该怎么去处理这个问题了。
其实关于问题 1 ,十分好处理,咱们甚至都不需求过多的去变动代码,只需求把运用的 String 值 改成 TextFieldValue
即可:
val maxLength = 8
var inputValue by remember { mutableStateOf(TextFieldValue()) }
OutlinedTextField(
value = inputValue,
onValueChange = {
if (it.text.length <= maxLength) inputValue = it
}
)
原因也很简略,由于 TextFieldValue
中现已包含了光标信息,这儿咱们在输入内容超过约束长度时不做更改 inputValue
的值,实践上是连同光标信息一起不做更改了,而上面直接运用的是 String ,则仅仅不改动输入内容,可是光标方位仍是会被改动。
而关于问题 2 ,需求咱们做一些特殊的处理。
咱们首要界说一个函数来处理输入内容的改动:
fun filterMaxLength(
inputTextField: TextFieldValue,
lastTextField: TextFieldValue,
maxLength: Int
): TextFieldValue {
// TODO
}
这个函数接纳两个参数:inputTextField
、 lastTextField
别离表明加上新输入的内容后的 TextFieldValue
和没有输入新内容时的 TextFieldValue
。
这儿有个当地需求留意,便是在 TextField
的 onChange
回调中,假如运用的是 TextFieldValue
,那么不仅会在输入内容产生改动时才调用 onChange
回调,而是即便只要光标的移动或状况改动都会调用 onChange
回调。
然后,咱们在这个函数中处理一下关于张贴多个字符时的状况:
val inputCharCount = inputTextField.text.length - lastTextField.text.length
if (inputCharCount > 1) { // 一起张贴了多个字符内容
val allowCount = maxLength - lastTextField.text.length
// 答应再输入字符现已为空,则直接回来原数据
if (allowCount <= 0) return lastTextField
// 还有答应输入的字符,则将其切断后刺进
val newString = StringBuffer()
newString.append(lastTextField.text)
val newChar = inputTextField.text.substring(lastTextField.selection.start..allowCount)
newString.insert(lastTextField.selection.start, newChar)
return lastTextField.copy(text = newString.toString(), selection = TextRange(lastTextField.selection.start + newChar.length))
}
这段代码其实很好了解,首要咱们通过运用未更新前的字符长度减去本次输入的字符长度得到本次实践新增的字符长度,假如这个长度大于 1 则认为是一起张贴了多个内容进输入框。(这儿有个点需求留意,便是这样得到的值或许会等于 0,表明仅仅光标的变动,字符没有变;小于 0 ,表明是删去内容。)
然后再运用最大答应输入的字符长度减去未更新前的输入框字符长度,即可得到当时还答应再刺进多少个字符 allowCount
。
假如尚还余有可输入的字符,则通过截取输入内容字符的契合长度的新增字段来获取。
截取的起点运用的是未更新前的光标开始方位(lastTextField.selection.start
),截取长度便是还答应输入的字符长度。
需求留意的是,这儿之所以运用 lastTextField.selection.start
作为截取起点,而不是 lastTextField.selection.end
是由于张贴刺进时也或许是由于之前现已选中了部分内容,然后再刺进的,此刻就应该以 未更新时的选中状况起点 作为刺进的方位。而假如张贴刺进时并非选中状况,那么运用 start
和 end
都能够,由于此刻它俩的值是相同的。
拿到能够刺进的字符后,接下里便是将其刺进即可:newString.insert(lastTextField.selection.start, newChar)
。
最终回来时别忘了改一下光标的方位,这儿其实也很简略,便是改到新字符刺进的方位+实践刺进的字符数量: TextRange(lastTextField.selection.start + newChar.length)
。
最终,完好的约束输入长度的过滤函数如下:
/**
* 过滤输入内容长度
*
* @param maxLength 答应输入长度,假如 小于 0 则不做过滤,直接回来原数据
* */
fun filterMaxLength(
inputTextField: TextFieldValue,
lastTextField: TextFieldValue,
maxLength: Int
): TextFieldValue {
if (maxLength < 0) return inputTextField // 过错的长度,不处理直接回来
if (inputTextField.text.length <= maxLength) return inputTextField // 总计输入内容没有超出长度约束
// 输入内容超出了长度约束
// 这儿要分两种状况:
// 1. 直接输入的,则回来原数据即可
// 2. 张贴后会导致长度超出,此刻或许还能够输入部分字符,所以需求判断后切断输入
val inputCharCount = inputTextField.text.length - lastTextField.text.length
if (inputCharCount > 1) { // 一起张贴了多个字符内容
val allowCount = maxLength - lastTextField.text.length
// 答应再输入字符现已为空,则直接回来原数据
if (allowCount <= 0) return lastTextField
// 还有答应输入的字符,则将其切断后刺进
val newString = StringBuffer()
newString.append(lastTextField.text)
val newChar = inputTextField.text.substring(lastTextField.selection.start..allowCount)
newString.insert(lastTextField.selection.start, newChar)
return lastTextField.copy(text = newString.toString(), selection = TextRange(lastTextField.selection.start + newChar.length))
}
else { // 正常输入
return if (inputTextField.selection.collapsed) { // 假如当时不是选中状况,则运用前次输入的光标方位,假如运用本次的方位,光标方位会 +1
lastTextField
} else { // 假如当时是选中状况,则运用当时的光标方位
lastTextField.copy(selection = inputTextField.selection)
}
}
}
其实这儿的过滤函数仍是有问题,不知道读者是否发现了?这儿我就不指出也不改了,权当是留给读者们的一个思考题了,哈哈,究竟不希望读者仅仅草草看完直接把代码张贴走就完事了,哈哈哈哈。
咱们在运用的时分只需求改一下 TextField
的 onChange
回调即可:
val maxLength = 8
var inputValue by remember { mutableStateOf(TextFieldValue()) }
OutlinedTextField(
value = inputValue,
onValueChange = {
inputValue = filterMaxLength(it, inputValue, maxLength)
}
)
扩展一下,做一个通用的过滤
尽管上面咱们现已完成了做一个自己的输入内容约束,可是似乎扩展性不怎么好啊。
咱们有没有办法做一个通用的,便利扩展的过滤办法呢?
究竟 View 中的过滤不仅是自己预设了许多好用的过滤,并且它还供给了一个通用的接口 InputFilter
能够让咱们自己界说咱们自己需求的过滤办法。
那么,说干就干,首要咱们来看看 View 中的 InputFilter
是怎么写的:

这么一看,其实也不是很杂乱,便是一个 InputFilter
类,只要一个 filter
办法,这个办法回来一个 CharSequence
表明通过过滤处理后的新的字符。
它供给了 6 个参数:
-
source
:要刺进的新字符 -
start
:source
中要刺进的字符方位起点 -
end
:source
中要刺进的字符方位结尾 -
dest
: 输入框中的原内容 -
dstart
:dest
中要被source
刺进的方位的起点 -
dend
:dest
中要被source
刺进的方位的结尾
咱们只需求运用这六个参数对字符进行过滤处理后回来新的字符就能够了。
可是 View 中的 InputFilter
明显只担任过滤字符,不担任处理更改光标的方位。
不管怎样,咱们也照猫画虎做一个 Compose 版别的过滤基类 BaseFieldFilter
:
open class BaseFieldFilter {
private var inputValue = mutableStateOf(TextFieldValue())
protected open fun onFilter(inputTextFieldValue: TextFieldValue, lastTextFieldValue: TextFieldValue): TextFieldValue {
return TextFieldValue()
}
protected open fun computePos(): Int {
// TODO
return 0
}
protecte fun getNewTextRange(
lastTextFiled: TextFieldValue,
inputTextFieldValue: TextFieldValue
): TextRange? {
// TODO
retutn null
}
protecte fun getNewText(
lastTextFiled: TextFieldValue,
inputTextFieldValue: TextFieldValue
): TextRange? {
// TODO
return null
}
fun getInputValue(): TextFieldValue {
return inputValue.value
}
fun onValueChange(): (TextFieldValue) -> Unit {
return {
inputValue.value = onFilter(it, inputValue.value)
}
}
}
在这类中咱们需求重点重视 onFilter
办法,咱们的过滤内容首要在这个办法中编写。
然后在 TextField
中首要会运用到 getInputValue
和 onValueChange
办法。
原本我还计划写几个根底的东西办法 getNewText
、 getNewTextRange
、computePos
别离用于计算实践刺进的新字符、实践刺进字符的方位、新的索引方位。
可是后来发现似乎并不好写出一个很好用的通用办法,所以这儿我就留空了。
这个根底类运用起来也很简略,咱们只需求将咱们自己的过滤办法承继这个根底类,然后重载 onFilter
办法即可,仍是以约束输入长度为例,写一个类 FilterMaxLength
:
/**
* 过滤输入内容长度
*
* @param maxLength 答应输入长度,假如 小于 0 则不做过滤,直接回来原数据
* */
class FilterMaxLength(
@androidx.annotation.IntRange(from = 0L)
private val maxLength: Int
) : BaseFieldFilter() {
override fun onFilter(
inputTextFieldValue: TextFieldValue,
lastTextFieldValue: TextFieldValue
): TextFieldValue {
return filterMaxLength(inputTextFieldValue, lastTextFieldValue, maxLength)
}
private fun filterMaxLength(
inputTextField: TextFieldValue,
lastTextField: TextFieldValue,
maxLength: Int
): TextFieldValue {
if (maxLength < 0) return inputTextField // 过错的长度,不处理直接回来
if (inputTextField.text.length <= maxLength) return inputTextField // 总计输入内容没有超出长度约束
// 输入内容超出了长度约束
// 这儿要分两种状况:
// 1. 直接输入的,则回来原数据即可
// 2. 张贴后会导致长度超出,此刻或许还能够输入部分字符,所以需求判断后切断输入
val inputCharCount = inputTextField.text.length - lastTextField.text.length
if (inputCharCount > 1) { // 一起张贴了多个字符内容
val allowCount = maxLength - lastTextField.text.length
// 答应再输入字符现已为空,则直接回来原数据
if (allowCount <= 0) return lastTextField
// 还有答应输入的字符,则将其切断后刺进
val newString = StringBuffer()
newString.append(lastTextField.text)
val newChar = inputTextField.text.substring(lastTextField.selection.start..allowCount)
newString.insert(lastTextField.selection.start, newChar)
return lastTextField.copy(text = newString.toString(), selection = TextRange(lastTextField.selection.start + newChar.length))
}
else { // 正常输入
return if (inputTextField.selection.collapsed) { // 假如当时不是选中状况,则运用前次输入的光标方位,假如运用本次的方位,光标方位会 +1
lastTextField
} else { // 假如当时是选中状况,则运用当时的光标方位
lastTextField.copy(selection = inputTextField.selection)
}
}
}
}
此刻,咱们在 TextField
中只需求这样即可调用:
val filter = remember { FilterMaxLength(8) }
OutlinedTextField(
value = filter.getInputValue(),
onValueChange = filter.onValueChange(),
)
怎么样,是不是十分的便利快捷?
我还想要更多!
当然,上面一向都是拿的约束输入长度举比方,那其他的过滤完成呢?你却是端出来啊,别急,这就为各位奉上我项目中用到的几个过滤办法。
相同的,这些办法或多或少我都留有坑,各位千万不要不检查一下直接就用哦(坏笑)。
哈哈哈,恶作剧了,其实完好没坑的代码各位能够去前语中我说到的那个项目中找。
过滤数字
class FilterNumber(
private val minValue: Double = -Double.MAX_VALUE,
private val maxValue: Double = Double.MAX_VALUE,
private val decimalNumber: Int = -1
) : BaseFieldFilter() {
override fun onFilter(
inputTextFieldValue: TextFieldValue,
lastTextFieldValue: TextFieldValue
): TextFieldValue {
return filterInputNumber(inputTextFieldValue, lastTextFieldValue, minValue, maxValue, decimalNumber)
}
private fun filterInputNumber(
inputTextFieldValue: TextFieldValue,
lastInputTextFieldValue: TextFieldValue,
minValue: Double = -Double.MAX_VALUE,
maxValue: Double = Double.MAX_VALUE,
decimalNumber: Int = -1,
): TextFieldValue {
val inputString = inputTextFieldValue.text
val lastString = lastInputTextFieldValue.text
val newString = StringBuffer()
val supportNegative = minValue < 0
var dotIndex = -1
var isNegative = false
if (supportNegative && inputString.isNotEmpty() && inputString.first() == '-') {
isNegative = true
newString.append('-')
}
for (c in inputString) {
when (c) {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9' -> {
newString.append(c)
val tempValue = newString.toString().toDouble()
if (tempValue > maxValue) newString.deleteCharAt(newString.lastIndex)
if (tempValue < minValue) newString.deleteCharAt(newString.lastIndex) // TODO 需求改进 (例如约束最小值为 100000000,则将无法输入东西)
if (dotIndex != -1) {
if (decimalNumber != -1) {
val decimalCount = (newString.length - dotIndex - 1).coerceAtLeast(0)
if (decimalCount > decimalNumber) newString.deleteCharAt(newString.lastIndex)
}
}
}
'.' -> {
if (decimalNumber != 0) {
if (dotIndex == -1) {
if (newString.isEmpty()) {
if (abs(minValue) < 1) {
newString.append("0.")
dotIndex = newString.lastIndex
}
} else {
newString.append(c)
dotIndex = newString.lastIndex
}
if (newString.isNotEmpty() && newString.toString().toDouble() == maxValue) {
dotIndex = -1
newString.deleteCharAt(newString.lastIndex)
}
}
}
}
}
}
val textRange: TextRange
if (inputTextFieldValue.selection.collapsed) { // 表明的是光标范围
if (inputTextFieldValue.selection.end != inputTextFieldValue.text.length) { // 光标没有指向结尾
var newPosition = inputTextFieldValue.selection.end + (newString.length - inputString.length)
if (newPosition < 0) {
newPosition = inputTextFieldValue.selection.end
}
textRange = TextRange(newPosition)
}
else { // 光标指向了结尾
textRange = TextRange(newString.length)
}
}
else {
textRange = TextRange(newString.length)
}
return lastInputTextFieldValue.copy(
text = newString.toString(),
selection = textRange
)
}
}
仅答应输入指定字符
class FilterOnlyChar() : BaseFieldFilter() {
private var allowSet: Set<Char> = emptySet()
constructor(allowSet: String) : this() {
val tempSet = mutableSetOf<Char>()
for (c in allowSet) {
tempSet.add(c)
}
this.allowSet = tempSet
}
constructor(allowSet: Set<Char>) : this() {
this.allowSet = allowSet
}
override fun onFilter(
inputTextFieldValue: TextFieldValue,
lastTextFieldValue: TextFieldValue
): TextFieldValue {
return filterOnlyChar(
inputTextFieldValue,
lastTextFieldValue,
allowChar = allowSet
)
}
private fun filterOnlyChar(
inputTextFiled: TextFieldValue,
lastTextFiled: TextFieldValue,
allowChar: Set<Char>
): TextFieldValue {
if (allowChar.isEmpty()) return inputTextFiled // 假如答应列表为空则不过滤
val newString = StringBuilder()
var modifierEnd = 0
for (c in inputTextFiled.text) {
if (c in allowChar) {
newString.append(c)
}
else modifierEnd--
}
return inputTextFiled.copy(text = newString.toString())
}
}
过滤电子邮箱地址
class FilterStandardEmail(private val extraChar: String = "") : BaseFieldFilter() {
private val allowChar: MutableSet<Char> = mutableSetOf('@', '.', '_', '-').apply {
addAll('0'..'9')
addAll('a'..'z')
addAll('A'..'Z')
addAll(extraChar.asIterable())
}
override fun onFilter(
inputTextFieldValue: TextFieldValue,
lastTextFieldValue: TextFieldValue
): TextFieldValue {
return inputTextFieldValue.copy(text = filterStandardEmail(inputTextFieldValue.text, lastTextFieldValue.text))
}
private fun filterStandardEmail(
inputString: String,
lastString: String,
): String {
val newString = StringBuffer()
var flag = 0 // 0 -> None 1 -> "@" 2 -> "."
for (c in inputString) {
if (c !in allowChar) continue
when (c) {
'@' -> {
if (flag == 0) {
if (newString.isNotEmpty() && newString.last() != '.') {
if (newString.isNotEmpty()) {
newString.append(c)
flag++
}
}
}
}
'.' -> {
// if (flag >= 1) {
if (newString.isNotEmpty() && newString.last() != '@' && newString.last() != '.') {
newString.append(c)
// flag++
}
// }
}
else -> {
newString.append(c)
}
}
}
return newString.toString()
}
}
过滤十六进制色彩
class FilterColorHex(
private val includeAlpha: Boolean = true
) : BaseFieldFilter() {
override fun onFilter(
inputTextFieldValue: TextFieldValue,
lastTextFieldValue: TextFieldValue
): TextFieldValue {
return inputTextFieldValue.copy(filterInputColorHex(
inputTextFieldValue.text,
lastTextFieldValue.text,
includeAlpha
))
}
private fun filterInputColorHex(
inputValue: String,
lastValue: String,
includeAlpha: Boolean = true
): String {
val maxIndex = if (includeAlpha) 8 else 6
val newString = StringBuffer()
var index = 0
for (c in inputValue) {
if (index > maxIndex) break
if (index == 0) {
if (c == '#') {
newString.append(c)
index++
}
}
else {
if (c in '0'..'9' || c.uppercase() in "A".."F" ) {
newString.append(c.uppercase())
index++
}
}
}
return newString.toString()
}
}
总结
尽管在 Compsoe 中官方没有供给相似于 EditText 中的 inputType 的预设输入内容过滤,可是得益于 Compose 的声明式 UI,在 Compose 中过滤输入内容愈加简略,都不需求太多繁琐的步骤,由于咱们能够直接操作输入的内容。
本文就由浅入深的介绍了怎么在 Compose 中快速完成相似于安卓原生 View 中的 inputType 输入内容过滤的办法,并且供给了几种常用的过滤供咱们运用。
本文正在参加「金石计划」