前言:在最近一个项目的登录页上,输入手机号码这个输入框,UI稿要求显示344样式的格式化手机号码,例如“130 1234 5678”,在之前其实也有过类似的需求,但是在实现344格式化手机号码的过程中或多或少碰到不少坑,当时也没有深究,以项目排期紧为由将需求拍回去了,折中的方案是设置输入框的android:inputType=“phone”,android:maxLength=“11”,这是最常见的手机号输入框的实现方式,全部由系统自己处理,无bug,程序员的最爱,但是这一次我决定say no!
之前想做这样的一个格式化手机号输入框时也查看过网上的一些代码,但是在使用他们的代码时,总会有各种各样的bug(空指针、角标越界等等),各种各样的技术博客都是将别人的代码拷过来不假思索的粘贴就变成自己的了,也不实践完善验证代码的正确性,所以目前没看到一款真正能用,不会出现bug的输入框控件。
准备干之前我想各一线大厂这种格式化手机号码的输入框肯定是正确好用的(崇拜的心理),于是准备看看他们实现的效果都是啥样的,结果也是让我感到惊讶,一起来看看!
为保证调研质量,所有app都升级到了最新版本。
1.今日头条 V7.2.9
今日头条的使用情况:
手机号登录为344格式化样式,最多只能输入11位有效字符,但是还可以输入字符,没有做输入过滤,在中间删除字符重新输入过程中无崩溃。
2.新浪微博 V9.6.2
新浪微博的使用情况:
在输入过程中,微博过滤所有的无效字符,只能输入0-9这些数字,其他的都不会输入。但是却没有做手机号位数限制,好吧,如果说与国家的手机号有关,那我无话可说,但是从第11位数字往后就没有了格式化了,也就是上图所示这样。
3.美团 V10.0.401
美团的使用情况:
美团在输入时,有最多11位数字限制,如果将光标移动到非末尾位置输入,会自动将最后一位移除,在光标位置插入新的字符,但是也没有做过滤,如图所示可以输入标点符号,字母只能输入p。
4.网易云音乐 V6.2.2
网易云音乐的使用情况:
网易云音乐并没有做344格式化手机号的处理,默认最多输入11位字符,输入满11位时,光标在非末尾位置时无法增加新的字符,如图所示,网易云可以输入加减乘除、逗号、点、字母w和p。
5.支付宝 V10.1.65.6567
支付宝的使用情况:
也没做位数限制,从第11位开始没有格式化样式了,过滤掉了其他的字符,但是还能输入减号。
6.优酷 V8.0.3
优酷的使用情况:
大优酷竟然不让截屏?可以看到没有做344手机号格式化,也没有限制输入长度,倒是过滤掉了所有的非数字的字符。
7.安居客 V12.16.4
安居客的使用情况
最多输入11位数字,也做了344格式化手机号码,所有字符无法输入(过滤的很干净),但是当我想点中间的某位数字时,竟然整块选中无法取消,这个时候你只有两种选择,要不点击获取验证码,要不重新输入手机号码,这个功能真的非常恶心!
8.贝壳找房 V2.11.0
贝壳找房的使用情况
在贝壳中进行手机号输入,问题和网易云音乐一样,可以输入±*/,.wp这些字符,并且还有一点也比较恶心,当光标移动到非末尾位置时,这时候不论是删除或者输入字符时,光标马上会跳到最后一位,个人感觉这个用户体验非常不好。
通过对上述一些生活中常用的覆盖衣食住行的主流app的输入框比较,现在没有一款能够满足真正意义上的344格式化手机号码输入框,有的是没有过滤特殊字符,有的是没有做长度限制,这里抛开国际化手机号码长度的问题,以中国现有11位手机号码长度为例,来动手写一个自定义的输入框,真正好用的344格式化号码,这里贴上最终的代码:
/*
* 实现自定义手机号输入框,手机号码效果为344效果,例如111 1111 1111
*/
public class PhoneEditText extends EditText implements TextWatcher {
// 特殊下标位置
private static final int PHONE_INDEX_3 = 3;
private static final int PHONE_INDEX_4 = 4;
private static final int PHONE_INDEX_8 = 8;
private static final int PHONE_INDEX_9 = 9;
private String preCharSequence;
private OnPhoneEditTextChangeListener onPhoneEditTextChangeListener;
public PhoneEditText(Context context) {
super(context);
initView();
}
public PhoneEditText(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
public PhoneEditText(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}
public interface OnPhoneEditTextChangeListener {
/**
* 对外提供接口监听
* @param s 字符串
* @param isEleven 现在是否是11位数字
*/
void onTextChange(String s, boolean isEleven);
}
public void setOnPhoneEditTextChangeListener(OnPhoneEditTextChangeListener listener) {
this.onPhoneEditTextChangeListener = listener;
}
private void initView() {
//设置输入过滤器
setFilters(new InputFilter[] {
new InputFilter() {
@Override
public CharSequence filter(CharSequence source, int start, int end,
Spanned spanned, int dstart, int dend) {
//在onTextChanged方法里执行setText(sb.toString());会到这里,内容一样直接返回
if (TextUtils.equals(source, preCharSequence)) {
return null;
}
//过滤掉空格和换行,dstart为13表示光标位置是11位数字+2个空格时,返回空字符
if (" ".equals(source.toString())
|| source.toString().contentEquals("\n")
|| dstart == 13) {
return "";
}
//这里是当光标移动到非末尾位置进行输入操作时,返回空字符
else if (getPhoneText().toString().length() == 11) {
return "";
} else {
return null;
}
}
}, new InputFilter() {
@Override
public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart,
int dend) {
//过滤掉所有的特殊字符,这里的字母只过滤掉了wp,因为在众多机型测试时,只能输入这两个,如果不放心可以添加a-z所有字母
String speChat = "[`~!@#$%^&*()+\\-=|{}':;',\\[\\].<>/?~!@#¥%……&*()——+|{}【】‘;:”“’。,、?wp]";
Pattern pattern = Pattern.compile(speChat);
Matcher matcher = pattern.matcher(source.toString());
if (matcher.find()) {
return "";
} else {
return null;
}
}
}
});
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
super.onTextChanged(s, start, before, count);
if (TextUtils.equals(preCharSequence, s)) {
return;
}
if (null != onPhoneEditTextChangeListener) {
onPhoneEditTextChangeListener.onTextChange(getPhoneText(),
getPhoneText().length() == 11);
}
if (s == null || s.length() == 0) {
return;
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
if (i != PHONE_INDEX_3 && i != PHONE_INDEX_8 && s.charAt(i) == ' ') {
continue;
} else {
sb.append(s.charAt(i));
if ((sb.length() == PHONE_INDEX_4 || sb.length() == PHONE_INDEX_9)
&& sb.charAt(sb.length() - 1) != ' ') {
sb.insert(sb.length() - 1, ' ');
}
}
}
//这里主要处理添加空格后的字符串,before=0为输入字符,before=1为删除字符,将光标移动到正确的位置
if (!sb.toString().equals(s.toString())) {
int index = start + 1;
if (sb.charAt(start) == ' ') {
if (before == 0) {
index++;
} else {
index--;
}
} else {
if (before == 1) {
index--;
}
}
preCharSequence = sb.toString();
setText(sb.toString());
//对setSelection添加异常捕获,防止出现意外的IndexOutOfBoundsException异常
try {
setSelection(index);
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
public void afterTextChanged(Editable s) {
}
// 获得不包含空格的手机号
public String getPhoneText() {
String str = getText().toString();
return replaceBlank(str);
}
private String replaceBlank(String str) {
String dest = "";
if (str != null) {
Pattern p = Pattern.compile("\\s*|\t|\r|\n");
Matcher m = p.matcher(str);
if (m.find()) {
dest = m.replaceAll("");
}
}
return dest;
}
}
使用也非常简单,在xml文件中使用:
在activity中使用:
etPhone.setOnPhoneEditTextChangeListener(new PhoneEditText.OnPhoneEditTextChangeListener() {
@Override
public void onTextChange(String s, boolean isEleven) {
setIvClearVisibility(s);
if (isEleven && tvSmscode.isEnabled()) {
tvSmscode.setTextColor(getResources().getColor(R.color.white));
tvSmscode.setClickable(true);
} else {
tvSmscode.setTextColor(getResources().getColor(R.color.color_30FFFFFF));
tvSmscode.setClickable(false);
}
}
});
具体的逻辑都已经在PhoneEditText中内部处理了,外界只用关心回调即可,用起来还是很方便的。
完成后一起来看看最终效果:
效果符合预期,最多输入11位有效数字,过滤所有的非数字字符,光标在中间删除插入也不会出现光标跳闪现象,用户体验好~
其实这东西并不难,也没有什么技巧,我也只是将现有的各种问题总结处理了一下,作为一个独立的控件能够很方便的被各方使用,以后再碰到类似的需求丝毫不用慌,拿过来用就完了,一次封装,无限使用,给用户提供最完美的用户体验!
如果有问题,欢迎留言,一起探讨~