The problem with most the answers here is that they all mess up the cursor position.
- If you simply replace text, your cursor ends up in the wrong place for the next typed character
- If you think you handled that by putting their cursor back at the end, well then they can't add prefix text or middle text, they are always jumped back to the end on each typed character, it's a bad experience.
You have an easy way to handle this, and a more universal way to handle it.
The easy way
<EditText
android:id="@+id/itemNameEditText"
android:text="@={viewModel.selectedCartItemModel.customName}"
android:digits="abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
android:inputType="textVisiblePassword"/>
DONE!
Visible password will fix the issue of double callbacks and problems like that. Problem with this solution is it removes your suggestions and autocompletes, and things like that. So if you can get away with this direction, PLEASE DO!!! It will eliminate so many headaches of trying to handle every possible issue of the hard way lol.
The Hard Way
The issue is related to the inputfilter callback structure being triggered by autocomplete. It is easy to reproduce. Just set your inputType = text, and then type abc@ you'll see it get called two times and if you can end up with abcabc instead of just abc if you were trying to ignore @ for example.
- First thing you have to handle is deleting to do this, you must
return null to accept "" as that is triggered by delete.
- Second thing you have to handle is holding delete as that updates every so often, but can come in as a long string of characters, so you need to see if your text length shrunk before doing replacement text or you can end up duplicating your text while holding delete.
- Third thing you need to handle is the duplicate callback, by keeping track of the previous text change call to avoid getting it twice. Don't worry you can still type the same letters back to back, it won't prevent that.
Here is my example. It's not perfect, and still has some kinks to work out, but it's a good place to start.
The following example is using databinding, but you are welcome to just use the intentFilter without databinding if that's your style. Abbreviated UI for showing only the parts that matter.
In this example, I restrict to alpha, numeric, and spaces only. I was able to cause a semi-colon to show up once while pounding on the android keyboard like crazy. So there is still some tweaking I believe that may need done.
DISCLAIMER
--I have not tested with auto complete
--I have not tested with suggestions
--I have not tested with copy/paste
--This solution is a 90% there solution to help you, not a battle tested solution
XML FILE
<layout
xmlns:bind="http://schemas.android.com/apk/res-auto"
>
<EditText
bind:allowAlphaNumericOnly="@{true}
OBJECT FILE
@JvmStatic
@BindingAdapter("allowAlphaNumericOnly")
fun restrictTextToAlphaNumericOnly(editText: EditText, value: Boolean) {
val tagMap = HashMap<String, String>()
val lastChange = "repeatCheck"
val lastKnownSize = "handleHoldingDelete"
if (value) {
val filter = InputFilter { source, start, end, dest, dstart, dend ->
val lastKnownChange = tagMap[lastChange]
val lastKnownLength = tagMap[lastKnownSize]?.toInt()?: 0
//handle delete
if (source.isEmpty() || editText.text.length < lastKnownLength) {
return@InputFilter null
}
//duplicate callback handling, Android OS issue
if (source.toString() == lastKnownChange) {
return@InputFilter ""
}
//handle characters that are not number, letter, or space
val sb = StringBuilder()
for (i in start until end) {
if (Character.isLetter(source[i]) || Character.isSpaceChar(source[i]) || Character.isDigit(source[i])) {
sb.append(source[i])
}
}
tagMap[lastChange] = source.toString()
tagMap[lastKnownSize] = editText.text.length.toString()
return@InputFilter sb.toString()
}
editText.filters = arrayOf(filter)
}
}