前言
终所周知, editText想要监听文本输入,有两种方式:InputFilter与TextWatcher。为什么不建议TextWatcher来修改文本, 我们一起来探讨一下。
1: InputFilter
老规矩,要看作用,先看代码注释. 一言以蔽之: InputFilter可以约束Editable的变化。
**
* InputFilters can be attached to {@link Editable}s to constrain the
* changes that can be made to them.
*/
public interface InputFilter
{
/**
* This method is called when the buffer is going to replace the
* range dstart … dend
of dest
* with the new text from the range start … end
* of source
. Return the CharSequence that you would
* like to have placed there instead, including an empty string
* if appropriate, or null
to accept the original
* replacement. Be careful to not to reject 0-length replacements,
* as this is what happens when you delete text. Also beware that
* you should not attempt to make any changes to dest
* from this method; you may only examine it for context.
*
* Note: If source is an instance of {@link Spanned} or
* {@link Spannable}, the span objects in the source should be
* copied into the filtered result (i.e. the non-null return value).
* {@link TextUtils#copySpansFrom} can be used for convenience if the
* span boundary indices would be remaining identical relative to the source.
*/
public CharSequence filter(CharSequence source, int start, int end,
Spanned dest, int dstart, int dend);
由此可以得知InputFilter是专门用来限制输入的(毕竟从名字就可以看出)。那么它究竟是怎么用的呢? 看看TextView.setText(CharSequence text, BufferType type,boolean notifyBefore, int oldlen)的源码即可:
private void setText(CharSequence text, BufferType type,
boolean notifyBefore, int oldlen) {
/* 省略部分代码 */
int n = mFilters.length;
for (int i = 0; i < n; i++) {
CharSequence out = mFilters[i].filter(text, 0, text.length(), EMPTY_SPANNED, 0, 0);
//关键代码, 根据filter返回值,决定是否要替换text。
if (out != null) {
text = out;
}
}
// 看见了我们的老朋友TextWatcher.BeforeTextChanged
if (notifyBefore) {
if (mText != null) {
oldlen = mText.length();
sendBeforeTextChanged(mText, 0, oldlen, text.length());
} else {
sendBeforeTextChanged("", 0, 0, text.length());
}
}
boolean needEditableForNotification = false;
if (mListeners != null && mListeners.size() != 0) {
needEditableForNotification = true;
}
PrecomputedText precomputed =
(text instanceof PrecomputedText) ? (PrecomputedText) text : null;
if (type == BufferType.EDITABLE || getKeyListener() != null
|| needEditableForNotification) {
createEditorIfNeeded();
mEditor.forgetUndoRedo();
Editable t = mEditableFactory.newEditable(text);
text = t;
setFilters(t, mFilters);
InputMethodManager imm = InputMethodManager.peekInstance();
if (imm != null) imm.restartInput(this);
}
if (Linkify.addLinks(s2, mAutoLinkMask)) {
text = s2;
type = (type == BufferType.EDITABLE) ? BufferType.EDITABLE : BufferType.SPANNABLE;
/*
* We must go ahead and set the text before changing the
* movement method, because setMovementMethod() may call
* setText() again to try to upgrade the buffer type.
*/
setTextInternal(text);
// Do not change the movement method for text that support text selection as it
// would prevent an arbitrary cursor displacement.
if (mLinksClickable && !textCanBeSelected()) {
setMovementMethod(LinkMovementMethod.getInstance());
}
}
}
mBufferType = type;
setTextInternal(text);
// 看见了我们的老朋友TextWatcher.onTextChanged
sendOnTextChanged(text, 0, oldlen, textLength);
onTextChanged(text, 0, oldlen, textLength);
notifyViewAccessibilityStateChangedIfNeeded(AccessibilityEvent.CONTENT_CHANGE_TYPE_TEXT);
// 看见了我们的老朋友TextWatcher.AfterTextChanged
if (needEditableForNotification) {
sendAfterTextChanged((Editable) text);
} else {
notifyAutoFillManagerAfterTextChangedIfNeeded();
}
// SelectionModifierCursorController depends on textCanBeSelected, which depends on text
if (mEditor != null) mEditor.prepareCursorControllers();
}
从setText源码中可以看出其InputerFilter参数与返回值的作用:
- source 代表此次输入的文本
- dest 代表控件已有的文本
- [start,end],[dstart,dend]分别表示source跟dest的文本位置(tips: 当del键被按下时,dend可能小于dstart)
- 返回值 CharSequence。这里的返回值有三种情况:
- 仅当满足条件时,返回null. (即不修改此次text,直接将其添加到textView中)
- 当不满足条件时, 返回空字符串.(用户输入的text被替换为"",添加到textView中)
- 当部分满足条件时, 取满足条件的部分字符串。
既然InputFilter可以约束变化,那么当产品同学提出这个输入框的最大限制是"128个汉字,或者256的英文字符"的要求时,我们应该怎么做呢?
1.1 自定义InputFilter
产品同学的需求是一个汉字占两个字符,其中标点符号(中英文),其他语言字符(日文,韩文),数字(1,2,3)等等需要占多少个字符呢? 作为一个合格的程序员,这里就需要跟产品同学对齐信息之后再进行编写。
既然有长度限制,我们肯定需要外部传入最大长度限制(Max),那么我们就需要考虑三种情况:
- Max - 控件已有字符长度 > 此次输入长度。(OK, 不会突破限制,直接append)
- Max - 控件已有字符长度 == 0。(Can't append)
- 0 < Max - 控件已有字符串 < 此次输入长度. (部分满足, 取部分source append)
1.1.2 MaxCharInputFilter
让我们假设仅英文字符([A-Za-z])占一个字符,其他都占两个字符,那么该如何实现呢?
/**
* @Author: koller
* @Date: 2020/7/1
* 仅[A-Za-z]算一个字符
* 其他全部算两个字符(包括英文标点符号,数字等)。
* 计算最大字符数
*/
class MaxCharLengthInputFilter(private val maxCharLength: Int) : BaseInputFilter() {
override val filterType: FilterType
get() = FilterType.MaxCharFilter
override fun filter(source: CharSequence, start: Int, end: Int, dest: Spanned, dstart: Int, dend: Int): CharSequence? {
var currentCharLength = 0
var dMinusCharLength = 0
dest.forEachIndexed { index, c ->
if (index in dstart until dend) {
dMinusCharLength += c.letterSize
}
currentCharLength += c.letterSize
}
var sourceCharLength = 0
var keep = maxCharLength - currentCharLength - dMinusCharLength
if (keep <= 0) {
// 超出字数限制,返回EMPTY_STRING
return EMPTY_STRING
}
source.forEach { c ->
sourceCharLength += c.letterSize
}
if (keep >= sourceCharLength) {
// 如果完全满足限制,就返回null
return null
}
val buffer = StringBuffer()
source.forEach { c ->
keep -= c.letterSize
if (keep <= 0) {
if (!c.isLetter() && keep < 0) {
// 不允许超过keep
return buffer
}
buffer.append(c)
return buffer
}
buffer.append(c)
}
return buffer
}
private val Char.letterSize: Int
get() {
return if (isLetter()) {
SINGLE_LETTER_SIZE
} else {
DOUBLE_LETTER_SIZE
}
}
private fun Char.isLetter() = this in 'a'..'z' || this in 'A'..'Z'
companion object {
const val SINGLE_LETTER_SIZE = 1
const val DOUBLE_LETTER_SIZE = 2
}
}
1.1.3 MaxBytesInputFilter (UTF-8编码)
已经有最大字符限制了,那我们实现一个最大字节限制,也是so easy
class MaxBytesInputFilter(private val maxBytes: Int) : BaseInputFilter() {
override val filterType: FilterType
get() = FilterType.MaxBytesFilter
override fun filter(source: CharSequence, start: Int, end: Int, dest: Spanned, dstart: Int, dend: Int): CharSequence? {
var currentBytesLength = 0
dest.forEachIndexed { index, c ->
if (index in dstart until dend) {
currentBytesLength -= c.byteSize
} else {
currentBytesLength += c.byteSize
}
}
var sourceByteLength = 0
var keep = maxBytes - currentBytesLength
if (keep <= 0) {
// 超出字数限制,返回EMPTY_STRING
return EMPTY_STRING
}
source.forEach { c ->
sourceByteLength += c.byteSize
}
// 如果完全满足限制,不改source
if (keep >= sourceByteLength) {
return null
}
val buffer = StringBuffer()
source.forEach { c ->
keep -= c.byteSize
if (keep <= 0) {
// 剩下2个字节的空间,最后一位是3字节的汉字
if (c.byteSize > 1 && keep < 0) {
// 不允许超过keep
return buffer
}
return buffer.append(c)
}
buffer.append(c)
}
return buffer
}
private val Char.byteSize
get() = toString().toByteArray().size
}
1.1.4 MaxLengthInputFilter.
最大字符跟字节都有了,那么实现一个最大长度的限制,应该是最简单的了。最大长度顾名思义,直接按照CharSequence.length()来粗暴的进行筛选.
class MaxLengthInputFilter(private val max: Int) : BaseInputFilter() {
override val filterType: FilterType
get() = FilterType.MaxLengthFilter
override fun filter(source: CharSequence, start: Int, end: Int, dest: Spanned, dstart: Int, dend: Int): CharSequence? {
var keep = max - (dest.length - (dend - dstart))
return when {
keep <= 0 -> {
// 超出字数限制,返回EMPTY_STRING
EMPTY_STRING
}
keep >= end - start -> {
// 如果完全满足限制,就返回null(如果返回值为null,TextView中就会使用原始source)
null
}
else -> {
// 部分满足限制, 如还剩2个长度,粘贴5个长度的字符串
keep += start
if (Character.isHighSurrogate(source[keep - 1])) {
// 如果最后一位字符是HighSurrogate(高编码,占2个字符位),就把keep减1,保证不超出字数限制
--keep
if (keep == start) {
return EMPTY_STRING
}
}
source.subSequence(start, keep)
}
}
}
}
留个作业:
如果要实现一个emojiInputFilter应该怎么做?