更新:使用该监听后不能长按删除以及输入时会有卡顿的问题。
更新:当EditText设置InputType不为TEXT时,会发生Crash,具体看文末
-----------------------------------------割------------------------------------------
也许称其为算法略微夸张,但是其中包含的各种情况略多,需要考虑全面,也是件稍微费神的事。最近项目需要用到该逻辑,网上一些版本是有bug的,强迫症患者的我便花些功夫研究了一下其中的门道。。
核心思路
使用TextWatcher监听字符串变化,判断是否需要对其进行格式化操作,若需要则按照构造函数传入配置进行格式化,并将变化的值设置到EditText中,最后设置光标位置
难点:光标位置的计算
前言
先了解一下关于TextWatcher中需要重写的方法
beforeTextChanged
: s将在start位置起的count个字符被after个字符替代(s为改变之前)
onTextChanged
: s为start位置起的before个字符被count个字符替代后的结果(s为改变之后)
afterTextChanged
: s为改变完成的结果
PS!PS!PPS!:在TextWatcher监听中,如果粘贴空格或粘贴的字符串中有空格,在复写的方法中,beforeTextChanged
中 after
的值是不包括空格的, onTextChanged
中 count
同理
正文
构造函数
public NumSpaceTextWatcher(@NonNull EditText target) {
this(target, DEFAULT_OFFSET);
}
public NumSpaceTextWatcher(@NonNull EditText target, int offset) {
mDesTxt = target;
mOffset = offset;
}
第一个构造函数传入2个参数,target
为目标EditTxt,不可为空,offset
为设置几位数之后添加1个空格,该值不传默认为4(该处可扩展,比如若想4位加横杠而不是空格,再添加一个参数,将代码中空格字符串用该参数代替即可)
1、beforeTextChanged
@Override
public void beforeTextChanged(CharSequence s, int start, int count,
int after) {
mBeforeTextLength = s.length();
mBeforeNumTxtLength = s.toString().replaceAll(" ", "").length();
mBeforeLocation = mDesTxt.getSelectionEnd();
// 重置mBuffer
if (mBuffer.length() > 0) {
mBuffer.delete(0, mBuffer.length());
}
// 计算改变前空格的个数
mBeforeSpaceNumber = 0;
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) == ' ') {
mBeforeSpaceNumber++;
}
}
}
该方法中先获取改变前的字符串长度,改变前字符串中包含的数字长度,改变前光标所处的位置,为下面判断是否需要对操作后的字符串进行格式化及光标位置计算做准备
清空记录字符串信息的mBuffer,格式化操作中每一步字符串的改变都有它来记录
计算改变前空格的个数,也是为计算改变后的光标位置计算做准备
2、onTextChanged
@Override
public void onTextChanged(CharSequence s, int start, int before,
int count) {
mOnTextLength = s.length();
mNumTxtLength = s.toString().replaceAll(" ", "").length();
// 判断是否是粘贴,其中粘贴小于offset位的不做判断,并且offset>2判断才有意义
if (mOffset >= 2 && count >= mOffset) {
isPaste = true;
mPasteNum = count;
} else {
isPaste = false;
mPasteNum = 0;
}
// 若是经过afterTextChanged方法,则直接return
if (isChanged) {
isChanged = false;
return;
}
// 若改变后长度小于等于mOffset - 1,则直接return
if (mOnTextLength <= mOffset - 1) {
isChanged = false;
return;
}
// 若改变前后长度一致,并且数字位数相同,则isChanged为false
// (数字位数相同是防止用户单选空格后输入数字)
if (mBeforeTextLength == mOnTextLength && mBeforeNumTxtLength == mNumTxtLength) {
isChanged = false;
return;
} else {
isChanged = true;
}
// 若要进行格式化,则判断该情况
// 判断是否选中空格覆盖(排除删除空格的情况)
if (before == 1 && count == 0) {
isOverrideSpace = false;
} else {
isOverrideSpace = mBeforeTextLength - mBeforeSpaceNumber - before + count != mNumTxtLength;
}
// 若是该情况,计算覆盖空格的个数
if (isOverrideSpace) {
mOverrideSpaceNum = mNumTxtLength - (mBeforeTextLength - mBeforeSpaceNumber - before + count);
} else {
mOverrideSpaceNum = 0;
}
}
该方法中做的操作有三个
- 获取改变后的字符串长度,改变后字符串中包含数字的长度,联合前一个方法中获取到的数据对下面判断做准备
- 判断是否对监听到的字符串改变做格式化操作
- 判断特殊情况是否是选中编辑框中包含空格的字符串进行粘贴等操作
现在解释一下后两个操作,以及前面获取到参数的使用
首先理一下在编辑框中键入字符串的情况
最简单的
- 键入一个字符
- 删除一个字符
但是在最简单的情况下有一个问题就是当光标处于格式化后字符串空格前后时,在操作完成之后,光标位置的计算与设置
其次
- 直接粘贴字符串
- 选中原有字符(是否包含空格),粘贴字符串
再次
- 选中字符串删除
其实选中删除这种情况才是最简单的,因为光标位置不需要计算,删除到哪里就设置到哪里,对其余字符串进行格式化处理
下面是代码判断
// 判断是否是粘贴,其中粘贴小于offset位的不做判断,并且offset>2判断才有意义
if (mOffset >= 2 && count >= mOffset) {
isPaste = true;
mPasteNum = count;
} else {
isPaste = false;
mPasteNum = 0;
}
首先判断是否是粘贴字符串,这里的粘贴
并不是真正意义的粘贴,而是,粘贴的时候,粘贴字符串的长度要大于等于offset的值(offset>=2才有意义),这种情况下,粘贴并且格式化字符串之后必定会添加空格,判断并记录结果
该判断的意义在于后面帮助计算光标位置
// 若是经过afterTextChanged方法,则直接return
if (isChanged) {
isChanged = false;
return;
}
// 若改变后长度小于等于mOffset - 1,则直接return
if (mOnTextLength <= mOffset - 1) {
isChanged = false;
return;
}
// 若改变前后长度一致,并且数字位数相同,则isChanged为false
// (数字位数相同是防止用户单选空格后输入数字)
if (mBeforeTextLength == mOnTextLength && mBeforeNumTxtLength == mNumTxtLength) {
isChanged = false;
return;
} else {
isChanged = true;
}
这三个判断为控制是否对监听到变化的字符串进行格式化操作,因为我们在进行格式化之后势必要进行setText
操作,而该操作必然会被该监听监听到,所以我们要将他们过滤掉
isChanged
为控制是否进行格式化操作的标识位,若为true,必然进行过格式化,则直接return
若改变后长度小于offset,则该字符串必然不需要格式化,如银行卡4位以空格,当记过字符串长度为3时,直接显示就好了,不需要进行格式化
第三种情况稍微复杂一些,主要考虑到一些特殊和奇葩状况
先说正常情况
原字符串 1234 5678
目标字符串 1256 5678
替换其中的几个字符串,此时,格式不需要变,光标位置不需要重新计算,遵循系统逻辑即可,mBeforeTextLength == mOnTextLength
这个条件可以帮我们过滤这个情况
特殊状况
原字符串 1234 5678
目标字符串 5678 5678 9
该情况是全选原字符串,粘贴567856789
字符串,此时,长度一致,但需要进行格式化。
如果用户选中空格,然后键入一个字符的情况,该情况下需要对变化后的字符串进行格式化。mBeforeNumTxtLength == mNumTxtLength
对这种情况进行了判断
特殊状况2
原字符串 1234 5678
目标字符串 5678 5678
该情况是全选原字符串,粘贴56 785678
字符串,此时,长度一致,no,长度不一致,注意看前言中的PS!这种情况会对字符串进行格式化
如果用户选中空格,然后键入一个字符的情况,该情况下需要对变化后的字符串进行格式化。mBeforeNumTxtLength == mNumTxtLength
对这种情况进行了判断
奇葩状况
原字符串 1234 5678
目标字符串 5678 9567 8
该情况是选中原字符串的空格,键入9
,这种情况在该判断条件下,也会进行格式化操作
// 若要进行格式化,则判断该情况
// 判断是否选中空格覆盖(排除删除空格的情况)
if (before == 1 && count == 0) {
isOverrideSpace = false;
} else {
isOverrideSpace = mBeforeTextLength - mBeforeSpaceNumber - before + count != mNumTxtLength;
}
// 若是该情况,计算覆盖空格的个数
if (isOverrideSpace) {
mOverrideSpaceNum = mNumTxtLength - (mBeforeTextLength - mBeforeSpaceNumber - before + count);
} else {
mOverrideSpaceNum = 0;
}
最后一个判断是当有选中后那个空格的情况进行覆盖时,用来辅助计算光标的位置。
mNumTxtLength - (mBeforeTextLength - mBeforeSpaceNumber - before + count)
变化后的字符串长度 - (变化前的字符串的长度 - 变化前的空格数 - 被替换的字符个数 + 替换的字符个数) = 被覆盖的空格数
得到这个结果后在这种情况计算光标位置时,加上这个结果就是最后的光标位置
3、afterTextChanged
@Override
public void afterTextChanged(Editable s) {
if (isChanged) {
mLocation = mDesTxt.getSelectionEnd();
// 去除空格
mBuffer.append(s.toString().replace(" ", ""));
// 格式化字符串,mOffset位加一个空格
int index = 0;
int mAfterSpaceNumber = 0;
while (index < mBuffer.length()) {
if (index == mOffset * (1 + mAfterSpaceNumber) + mAfterSpaceNumber) {
mBuffer.insert(index, ' ');
mAfterSpaceNumber++;
}
index++;
}
// 判断是否是粘贴键入
if (isPaste) {
mLocation += mPasteNum / mOffset;
isPaste = false;
// 判断是否是选中空格输入
} else if (isOverrideSpace) {
mLocation += mOverrideSpaceNum;
// 判断此时光标是否在特殊位置上
} else if (mLocation % (mOffset + 1) == 0) {
// 是键入OR删除
if (mBeforeLocation <= mLocation) {
mLocation++;
} else {
mLocation--;
}
}
// 若是删除数据刚好删除一位,前一位是空格,mLocation会超出格式化后字符串的长度(因为格
// 式化后的长度没有不包括最后的空格),将光标移到正确的位置
String str = mBuffer.toString();
if (mLocation > str.length()) {
mLocation = str.length();
} else if (mLocation < 0) {
mLocation = 0;
}
mDesTxt.setText(str);
Editable etable = mDesTxt.getText();
Selection.setSelection(etable, mLocation);
isChanged = false;
}
}
最后重头戏来了,这个方法做了两件事
- 格式化字符串
- 计算光标位置并设置
首先根据isChanged
,看是否需要进行格式化,在onTextChanged
中我们会得到该结果
然后得到修改后字符串光标位置(系统的逻辑,键入一个字符,光标后移一位),后续计算正确的光标位置都在这个值的基础上变化
int index = 0;
int mAfterSpaceNumber = 0;
while (index < mBuffer.length()) {
if (index == mOffset * (1 + mAfterSpaceNumber) + mAfterSpaceNumber) {
mBuffer.insert(index, ' ');
mAfterSpaceNumber++;
}
index++;
}
然后进行格式化操作,先对当前的字符串去除空格的操作,然后判断达到offset位就加一个空格,这个index的计算
index = mOffset * (1 + mAfterSpaceNumber) + mAfterSpaceNumber
mAfterSpaceNumber
是格式化后的空格数,用来辅助计算光标位置
// 判断是否是粘贴键入
if (isPaste) {
mLocation += mPasteNum / mOffset;
isPaste = false;
// 判断是否是选中空格输入
} else if (isOverrideSpace) {
mLocation += mOverrideSpaceNum;
// 判断此时光标是否在特殊位置上
} else if (mLocation % (mOffset + 1) == 0) {
// 是键入OR删除
if (mBeforeLocation <= mLocation) {
mLocation++;
} else {
mLocation--;
}
}
接下来就是最后计算光标位置的工作,这边计算主要是各种特殊情况的判断,因为正常情况只需要遵循系统原有逻辑即可(输入+1,删除-1等)
这边主要对下面一些情况做了判断
-
粘贴键入
此粘贴非彼粘贴,具体可以看
onTextChanged
中的判断,以及下面对于粘贴这种情况的说明这种情况主要是因为格式化后会多出空格,若只遵循系统逻辑,会因为多出空格的原因,使得光标没有到粘贴字符串的最后一个字符后面(光标前移),因此我们判断粘贴的字符串会多出几个空格,光标位置增加相应个数即可,并将标志位
isPaste
重置为false如
1234 5678
选中56
复制567890
,此时期望结果为1234 5678 9078
,期望光标位置应在0
后面,而如果不进行光标位置判断,遵循系统逻辑此时光标会在7后面,因为粘贴6个字符,光标只向后移动6位,而格式化加了一个空格,所以光标应该向后移动7位才对 -
isOverrideSpace
该情况是判断是否因为选中字段包含空格,覆盖之后,格式化又重新生成,导致光标位置没有正确移动
如
1234 5678
选中5
(包括5前面的空格)复制789
,期望结果为1234 7896 78
,光标应在9后面,而遵循系统逻辑,粘贴3位,光标相应移动3位,由于4后面的空格被覆盖后由于格式化重新生成,所以,光标移动三位是从4后面移动到空格后面然后依次7
、8
,所以光标前移 -
mLocation % (mOffset + 1) == 0
该条件是判断光标是否在特殊的位置上,那什么位置时特殊位置呢?
mLocation = mOffset * (1 + x) + x
(注意mLocation是键入字符之后光标的位置)所有符合上述公式的,都为特殊位置,因为当键入一个字符时,会因为格式化产生一个空格,或者删除字符时,会因为格式化减少一个空格
如
offset = 4
时,特殊位置有5
,即1234
字符串当键入5时,此时光标位置即mLocation = 5
,但是期望字符串为1234 5
,光标位置应在5后面,而不主动设置,光标将会显示在5的前面,因此该情况也需要判断mBeforeLocation <= mLocation
这个判断是判断用户操作是键入还是删除的,以此来判断光标位置应该+1还是-1
若有一种情况符合多个条件也会根据优先级,即if else if 的顺序进行处理,不会有处理多次的状况
最后将格式化后的字符串设置到EditText中,设置光标位置,并将isChanged
重置为false(在setText之后就会再次监听到,并且判断不需要格式化,然后回来,继续执行setSelection
,然后重置isChanged
,如果不重置,就再也不会进行格式化了)
至此,竣工。
更新:当EditText设置InputType不为Text时,会发生Crash
如设置EditText InputType为Number时,当输入第5位数时会发生Crash,报错setSpan (6 ... 6) ends beyond length 5
具体原因是因为当设置InputType为Number时,EditText不接受空格字符,导致editable的replace方法失效,设置selection时位置越界
跟入replace方法源码
SpannableStringBuilder.class
public SpannableStringBuilder replace(final int start, final int end,
CharSequence tb, int tbstart, int tbend) {
checkRange("replace", start, end);
int filtercount = mFilters.length;
// 这边有进行遍历Filter
for (int i = 0; i < filtercount; i++) {
CharSequence repl = mFilters[i].filter(tb, tbstart, tbend, this, start, end);
if (repl != null) {
tb = repl;
tbstart = 0;
tbend = repl.length();
}
}
...
}
当设置InputType为Number时,Filter数组里面多了一个名为DigitsKeyListener的Filter(继承自InputFilter)
DigitsKeyListener.class
private static final char[][] CHARACTERS = {
{ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' },
{ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '+' },
{ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.' },
{ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '+', '.' },
};
在该类中有一个二维数组,我们看到他已经规定了可以接受字符串的数组,然后通过创建时的参数控制使用二维数组中的某一个,默认为第0个
因此,我们可以通过重写该Listener的方法,改变他可以接受的字符(添加空格字符),在设置InputType为Number时,来手动添加我们自定义的Listener来实现Number类型的4位一空格
(汗颜...本来是为银行卡号写的类,居然不支持Number类型InputType而不自知...回头考虑重写精简一下该代码)
附完整源码:
import android.support.annotation.NonNull;
import android.text.Editable;
import android.text.InputType;
import android.text.Selection;
import android.text.TextWatcher;
import android.text.method.DigitsKeyListener;
import android.util.Log;
import android.widget.EditText;
/**
* 类银行卡4位插入一空格监听
* Created by penggao on 2016/7/7.
*/
public class NumSpaceTextWatcher implements TextWatcher {
private static final int DEFAULT_OFFSET = 4;
// 目标输入框
private final EditText mDesTxt;
// 偏移量(几位插入一空格)
private int mOffset;
// 记录目标字符串
private StringBuffer mBuffer = new StringBuffer();
// 改变之前的文本长度
private int mBeforeTextLength;
// 改变之后的文本长度
private int mOnTextLength;
// 改变之前去除空格的文本长度
private int mBeforeNumTxtLength;
// 改变之后去除空格的文本长度
private int mNumTxtLength;
// 目标 光标的位置
private int mLocation = 0;
// 之前 光标的位置(可判断用户是否做删除操作)
private int mBeforeLocation = 0;
// 改变前有多少空格
private int mBeforeSpaceNumber = 0;
// 是否选中空格覆盖
private boolean isOverrideSpace;
// 被覆盖的空格数
private int mOverrideSpaceNum;
// 是否是粘贴(此粘贴非彼粘贴)
private boolean isPaste;
// 复制的字符数(不包括空格)
private int mPasteNum;
// 是否需要进行格式化字符串操作
private boolean isChanged = false;
public NumSpaceTextWatcher(@NonNull EditText target) {
this(target, DEFAULT_OFFSET);
}
public NumSpaceTextWatcher(@NonNull EditText target, int offset) {
if (target.getInputType() == InputType.TYPE_CLASS_NUMBER) {
target.setInputType(InputType.TYPE_CLASS_TEXT);
// 当InputType为Number时,手动设置我们的Listener
target.setKeyListener(new MyDigitsKeyListener());
} else if (target.getInputType() != InputType.TYPE_CLASS_TEXT) {
// 仅支持Text及Number类型的EditText
throw new IllegalArgumentException("EditText only support TEXT and NUMBER InputTyp!");
}
mDesTxt = target;
mOffset = offset;
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
mBeforeTextLength = s.length();
mBeforeNumTxtLength = s.toString().replaceAll(" ", "").length();
mBeforeLocation = mDesTxt.getSelectionEnd();
// 重置mBuffer
if (mBuffer.length() > 0) {
mBuffer.delete(0, mBuffer.length());
}
// 计算改变前空格的个数
mBeforeSpaceNumber = 0;
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) == ' ') {
mBeforeSpaceNumber++;
}
}
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
mOnTextLength = s.length();
mNumTxtLength = s.toString().replaceAll(" ", "").length();
// 判断是否是粘贴,其中粘贴小于offset位的不做判断,并且offset>2判断才有意义
if (mOffset >= 2 && count >= mOffset) {
isPaste = true;
mPasteNum = count;
} else {
isPaste = false;
mPasteNum = 0;
}
// 若是经过afterTextChanged方法,则直接return
if (isChanged) {
isChanged = false;
return;
}
// 若改变后长度小于等于mOffset - 1,则直接return
if (mOnTextLength <= mOffset - 1) {
isChanged = false;
return;
}
// 若改变前后长度一致,并且数字位数相同,则isChanged为false
// (数字位数相同是防止用户单选空格后输入数字)
if (mBeforeTextLength == mOnTextLength && mBeforeNumTxtLength == mNumTxtLength) {
isChanged = false;
return;
} else {
isChanged = true;
}
// 若要进行格式化,则判断该情况
// 判断是否选中空格覆盖(排除删除空格的情况)
if (before == 1 && count == 0) {
isOverrideSpace = false;
} else {
isOverrideSpace = mBeforeTextLength - mBeforeSpaceNumber - before + count != mNumTxtLength;
}
// 若是该情况,计算覆盖空格的个数
if (isOverrideSpace) {
mOverrideSpaceNum = mNumTxtLength - (mBeforeTextLength - mBeforeSpaceNumber - before + count);
} else {
mOverrideSpaceNum = 0;
}
}
@Override
public void afterTextChanged(Editable s) {
if (isChanged) {
mLocation = mDesTxt.getSelectionEnd();
// 去除空格
mBuffer.append(s.toString().replace(" ", ""));
// 格式化字符串,mOffset位加一个空格
int index = 0;
int mAfterSpaceNumber = 0;
while (index < mBuffer.length()) {
if (index == mOffset * (1 + mAfterSpaceNumber) + mAfterSpaceNumber) {
mBuffer.insert(index, ' ');
mAfterSpaceNumber++;
}
index++;
}
// 判断是否是粘贴键入
if (isPaste) {
mLocation += mPasteNum / mOffset;
isPaste = false;
// 判断是否是选中空格输入
} else if (isOverrideSpace) {
mLocation += mOverrideSpaceNum;
// 判断此时光标是否在特殊位置上
} else if (mLocation % (mOffset + 1) == 0) {
// 是键入OR删除
if (mBeforeLocation <= mLocation) {
mLocation++;
} else {
mLocation--;
}
}
// 若是删除数据刚好删除一位,前一位是空格,mLocation会超出格式化后字符串的长度(因为格
// 式化后的长度没有不包括最后的空格),将光标移到正确的位置
String str = mBuffer.toString();
if (mLocation > str.length()) {
mLocation = str.length();
} else if (mLocation < 0) {
mLocation = 0;
}
s.replace(0, s.length(), str);
Editable editable = mDesTxt.getText();
Selection.setSelection(editable, mLocation);
}
}
// 继承DigitsKeyListener,实现我们自己的Listener
private class MyDigitsKeyListener extends DigitsKeyListener {
private char[] mAccepted = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ' ' };
@Override
protected char[] getAcceptedChars() {
return mAccepted;
}
}
}
网友评论