I\'m trying to implement an EditText that limits input to alpha chars only [A-Za-z].
I started with the InputFilter method from this post. When I type \"a%\" the text d
We had a similar problem and I believe a solution[0] that would work for you as well. Our requirements were to implement an EditText that stripped rich text input. For example, if the user copied bold text to their clipboard and pasted it into the EditText, the EditText should remove the bold emphasis styling and preserve only the plain text.
The solution class looks something like this:
public class PlainEditText extends EditText {
public PlainEditText(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
addFilter(this, new PlainTextInputFilter());
}
private void addFilter(TextView textView, InputFilter filter) {
InputFilter[] filters = textView.getFilters();
InputFilter[] newFilters = Arrays.copyOf(filters, filters.length + 1);
newFilters[filters.length] = filter;
textView.setFilters(newFilters);
}
private static class PlainTextInputFilter implements InputFilter {
@Override
public CharSequence filter(CharSequence source, int start, int end, Spanned dest,
int dstart, int dend) {
return stripRichText(source, start, end);
}
private CharSequence stripRichText(CharSequence str, int start, int end) {
// ...
}
}
}
Our original implementation for stripRichText() was simple:
// -- BROKEN. DO NOT USE --
String plainText = str.subSequence(start, end).toString();
return plainText;
The Java base String class doesn't retain any styling information so converting the CharSequence interface to a concrete String copies only plain text.
What we didn't realize was that some Android soft keyboards add and depend on temporary compositional hints for typos and other things. The problem manifests by removing the hints as well as repeating characters in an unexpected way (usually doubling the entire EditText field's input). The documentation[1] for InputFilter.filter() communicates the requirement this way:
* Note: If <var>source</var> is an instance of {@link Spanned} or
* {@link Spannable}, the span objects in the <var>source</var> should be
* copied into the filtered result (i.e. the non-null return value).
I believe the proper solution is to preserve temporary spans:
/** Strips all rich text except spans used to provide compositional hints. */
private CharSequence stripRichText(CharSequence str, int start, int end) {
String plainText = str.subSequence(start, end).toString();
SpannableString ret = new SpannableString(plainText);
if (str instanceof Spanned) {
List<Object> keyboardHintSpans = getComposingSpans((Spanned) str, start, end);
copySpans((Spanned) str, ret, keyboardHintSpans);
}
return ret;
}
/**
* @return Temporary spans, often applied by the keyboard to provide hints such as typos.
*
* @see {@link android.view.inputmethod.BaseInputConnection#removeComposingSpans}
* @see {@link android.inputmethod.latin.inputlogic.InputLogic#setComposingTextInternalWithBackgroundColor}
*/
@NonNull private List<Object> getComposingSpans(@NonNull Spanned spanned,
int start,
int end) {
// TODO: replace with Apache CollectionUtils.filter().
List<Object> ret = new ArrayList<>();
for (Object span : getSpans(spanned, start, end)) {
if (isComposingSpan(spanned, span)) {
ret.add(span);
}
}
return ret;
}
private Object[] getSpans(@NonNull Spanned spanned, int start, int end) {
Class<Object> anyType = Object.class;
return spanned.getSpans(start, end, anyType);
}
private boolean isComposingSpan(@NonNull Spanned spanned, Object span) {
return isFlaggedSpan(spanned, span, Spanned.SPAN_COMPOSING);
}
private boolean isFlaggedSpan(@NonNull Spanned spanned, Object span, int flags) {
return (spanned.getSpanFlags(span) & flags) == flags;
}
/**
* Apply only the spans from src to dst specific by spans.
*
* @see {@link android.text.TextUtils#copySpansFrom}
*/
public static void copySpans(@NonNull Spanned src,
@NonNull Spannable dst,
@NonNull Collection<Object> spans) {
for (Object span : spans) {
int start = src.getSpanStart(span);
int end = src.getSpanEnd(span);
int flags = src.getSpanFlags(span);
dst.setSpan(span, start, end, flags);
}
}
[0] Actual implementation available here: https://github.com/wikimedia/apps-android-wikipedia/blob/e9ffffd8854ff15cde791a2e6fb7754a5450d6f7cf/app/src/main/java/org/wikipedia/richtext/RichTextUtil.java
[1] https://android.googlesource.com/platform/frameworks/base/+/029942f77d05ed3d20256403652b220c83dad6e1/core/java/android/text/InputFilter.java#37
I would just like to add my solution to the problem(as late as it is). I found that if you add
yourEditText.setInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
Then the backspace problems stop
Fix for repeating text, work on all Android Versions:
public static InputFilter getOnlyCharactersFilter() {
return getCustomInputFilter(true, false, false);
}
public static InputFilter getCharactersAndDigitsFilter() {
return getCustomInputFilter(true, true, false);
}
public static InputFilter getCustomInputFilter(final boolean allowCharacters, final boolean allowDigits, final boolean allowSpaceChar) {
return new InputFilter() {
@Override
public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
boolean keepOriginal = true;
StringBuilder sb = new StringBuilder(end - start);
for (int i = start; i < end; i++) {
char c = source.charAt(i);
if (isCharAllowed(c)) {
sb.append(c);
} else {
keepOriginal = false;
}
}
if (keepOriginal) {
return null;
} else {
if (source instanceof Spanned) {
SpannableString sp = new SpannableString(sb);
TextUtils.copySpansFrom((Spanned) source, start, sb.length(), null, sp, 0);
return sp;
} else {
return sb;
}
}
}
private boolean isCharAllowed(char c) {
if (Character.isLetter(c) && allowCharacters) {
return true;
}
if (Character.isDigit(c) && allowDigits) {
return true;
}
if (Character.isSpaceChar(c) && allowSpaceChar) {
return true;
}
return false;
}
};
}
Now you can use this filer like:
//Accept Characters Only
edit_text.setFilters(new InputFilter[]{getOnlyCharactersFilter()});
//Accept Digits and Characters
edit_text.setFilters(new InputFilter[]{getCharactersAndDigitsFilter()});
//Accept Digits and Characters and SpaceBar
edit_text.setFilters(new InputFilter[]{getCustomInputFilter(true,true,true)});
EditText input = (EditText) findViewById(R.id.inputText);
input.addTextChangedListener(new TextWatcher() {
public void onTextChanged(CharSequence s, int start, int before, int count) {
// TODO Auto-generated method stub
for( int i = start;i<s.toString().length(); i++ ) {
if( !Character.isLetter(s.charAt( i ) ) ) {
input.setText("");
}
}
}
public void beforeTextChanged(CharSequence s, int start, int count,
int after) {
// TODO Auto-generated method stub
}
public void afterTextChanged(Editable s) {
// TODO Auto-generated method stub
}
});
input.addTextChangedListener(new TextWatcher() {
public void onTextChanged(CharSequence s, int start, int before, int count) {
// TODO Auto-generated method stub
}
public void beforeTextChanged(CharSequence s, int start, int count,
int after) {
// TODO Auto-generated method stub
}
public void afterTextChanged(Editable s) {
// TODO Auto-generated method stub
for( int i = 0;i<s.toString().length(); i++ ) {
if( !Character.isLetter(s.charAt( i ) ) ) {
s.replace(i, i+1,"");
}
}
}
});
Bingo, I found the problem!
When I use android:cursorVisible="false" on the EditText the start and dstart parameters don't match up correctly.
The start parameter is still always 0 for me, but the dstart parameter is also always 0 so it works out as long as I use .replaceAll(). This is contrary to what this post says so I don't quite understand why but at least I can build something that works now!