2019-08-13
关键字:EditText、自定义View、实时检测IP地址
在做 APK 开发的时候,有时会遇到要输入IP地址的情况,但IP地址是有一定规则的,而 Android 官方又没有提供专用于输入IP地址的输入框。所以通常最简单节省时间的做法就是直接提供一个 EditText 供用户输入,然后等用户提交以后再去检查规则。还有些同学不愿意这么随便,他们可能自己开发或者到网上去寻找相关的开源代码。笔者昨天出于无聊,也做了一个IP地址输入框控件,现在将它开源出来,希望能给后来者带来些许帮助。
笔者的这个IP地址输入框控件非常简单,它是直接继承自官方的 EditText 控件并加入相关处理逻辑实现的。它具有如下几个特点:
1、与 EditText 控件在外观与使用方式上均一致;
2、伴随着输入依据 IPv4 规则实时检测输入字符;
3、根据 IPv4 规则,不同状态的数据以不同形态区分显示;
4、源码极其轻量。
使用展示
笔者的这个输入框并没有对输入字符做很严格的限定,事实上它允许输入任何字符,但是对于那些非IP地址字符,它就会以灰色显示。之所以这么做,是考虑到可能真的有些场景会有这种显示非IP地址字符的需求,不想限制的太死了。
输入的字符中,符合IP地址规范的,会以绿色显示;可能符合的文本,则以橙色显示;不符合规范的,则以红色显示。
在使用上也很简单,直接在需要用到的 layout 上按以下格式引入即可:
<com.my.pkg.IPEditText android:id="@+id/iv01" android:layout_centerInParent="true" android:inputType="text|number" android:layout_width="300dp" android:layout_height="50dp" />
可以说和官方的 EditText 完全没有区别了。
源码浅析
笔者的这个IP地址输入框控件命名为 IPEditText。它仅一份三百多行的 Java 代码,没有额外的文件。它大致的流程如下图所示
关于构造器
构造器虽然有两个,但实际上笔者只实现了在 XML 中使用的构造方法
public IPEditText(Context context) { super(context); log("IPEditText(context)"); } public IPEditText(Context context, AttributeSet attrs) { super(context, attrs); log("IPEditText(context,attrs)"); super.addTextChangedListener(this); textInitialization(); }
在构造中注册了文本改变的监听器,并且拦截了外部监听注册请求
@Override public void addTextChangedListener(TextWatcher watcher) { log("we ignore your call"); }
关于IP规则检查
IPv4 规则的检查功能全部封装在内部类 IPChecker 中。检查会将IP地址分成 4 个字段逐一检查。每个IP字段又被抽象成 IPs 类,IPs 类就包括了当前字段的状态,是正确还是未知还是错误。同时类里面还有两个 StringBuilder 对象,它们在默认情况下是 null 的。只有在检查到当前IP字段的值有错时,才会去实例化对应的 StringBuilder 类对象,这些错误不同类型的信息会被记录到这些 StringBuilder 对象中。
完整的 IPEditText 源码如下所示
package com.my.pkg; import android.content.Context; import android.text.Editable; import android.text.Html; import android.text.TextWatcher; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.widget.EditText; public class IPEditText extends EditText implements TextWatcher { private static final String TAG = "IPEditText"; // private static final String COLOR_CORRECT = ""; // private static final String COLOR_WARN = ""; // private static final String COLOR_ERROR = ""; private boolean isNeed2Update; /** * 编辑前的光标。 * */ private int oldPos; /** * 编辑前的字符长度。 * */ private int oldLength; private View bg; private IPChecker ipChecker; public IPEditText(Context context) { super(context); log("IPEditText(context)"); } public IPEditText(Context context, AttributeSet attrs) { super(context, attrs); log("IPEditText(context,attrs)"); super.addTextChangedListener(this); textInitialization(); } private void textInitialization(){ ipChecker = new IPChecker(); //get text. String txt = getText().toString(); log("txt null?" + (txt==null) + ",txt:" + txt); //check whether ip string. if (txt != null && !"".equals(txt)) { ipChecker.check(txt); setText(Html.fromHtml(ipChecker.toString())); setSelection(getText().length()); } log("getNormalText:" + ipChecker.getNormalTxt()); } private void notifyUpdate(String txt){ isNeed2Update = false; if (txt != null && !"".equals(txt)) { ipChecker.check(txt); setText(Html.fromHtml(ipChecker.toString())); if(getText().length() > oldLength){ setSelection(oldPos + 1); }else{ setSelection(oldPos); } } } @Override public void addTextChangedListener(TextWatcher watcher) { log("we ignore your call"); } private void log(String msg){ Log.d(TAG, msg); } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { if(isNeed2Update){ oldPos = start; oldLength = s.length(); } // log("isNeed:" + isNeed2Update + ",start:" + start + ",length:" + s.length() + ",after:" + after); } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } @Override public void afterTextChanged(Editable s) { if(isNeed2Update){ notifyUpdate(s.toString()); }else{ isNeed2Update = true; log("after " + s.toString()); } } private class IPChecker { /**ipv4*/ IPs[] ips = new IPs[4]; /**记录当前检查到哪一IP段落了*/ int checkingIdx; /**记录小数点的数量。*/ int pointCounter; void check(String txt){ IPEditText.this.log("checking:" + txt); // reset environment. checkingIdx = 0; pointCounter = 0; ips[0] = null; ips[1] = null; ips[2] = null; ips[3] = null; if (txt == null || "".equals(txt)) { return; } char a[] = txt.toCharArray(); int idx = 0; for(int i = 0; i < a.length; i++){ if(isNumber(a[i])){ if (ips[checkingIdx] == null) { ips[checkingIdx] = new IPs(); } //over length. if(ips[checkingIdx].insertIdx >= 3){ ips[checkingIdx].initVariableArray(); ips[checkingIdx].sb.append(a[i]); ips[checkingIdx].status = ips[checkingIdx].STATUS_ERROR; continue; } ips[checkingIdx].number[ips[checkingIdx].insertIdx++] = a[i]; int c = Integer.valueOf(new String(ips[checkingIdx].number, 0, ips[checkingIdx].insertIdx)); if(c < 0 || c > 255){ ips[checkingIdx].status = ips[checkingIdx].STATUS_ERROR; }else{ if(checkingIdx == 3){ ips[checkingIdx].status = ips[checkingIdx].STATUS_CORRECT; }else if(checkingIdx > 3){ }else{ ips[checkingIdx].status = ips[checkingIdx].STATUS_WARN; } } }else{ if('.' != a[i]){ // Error string... IPEditText.this.log("character is not number."); if(ips[checkingIdx] == null){ ips[checkingIdx] = new IPs(); } ips[checkingIdx].initInvalidCharacterArray(); ips[checkingIdx].sbInvalid.append(new String(a, i, a.length - i)); break; } if(ips[checkingIdx] == null){ // '.' as header. ips[checkingIdx] = new IPs(); ips[checkingIdx].initInvalidCharacterArray(); ips[checkingIdx].sbInvalid.append(new String(a, i, a.length - i)); break; } // check current ip part. if(ips[checkingIdx].status == 1){ ips[checkingIdx].status = ips[checkingIdx].STATUS_CORRECT; } // record it. pointCounter++; // upgrade~ checkingIdx++; } }// for -- end. //traversal ips for(IPs i:ips){ if (i != null) { String jkl = new String(i.number, 0, i.insertIdx); if("---".equals(jkl)){ jkl = i.sb.toString(); } IPEditText.this.log(jkl + ",status:" + i.status); } } } boolean isNumber(char c){ IPEditText.this.log("checking character:" + c); if('0' <= c && c <= '9') { return true; } return false; } CharSequence getRichTxt(){ StringBuilder sb = new StringBuilder(); int point = pointCounter; for(IPs i:ips){ if (i == null) { break; } if(i.isCharacterInvalid){ sb.append("<font color='#A8A8A8'>"); sb.append(i.sbInvalid.toString()); sb.append("</font>"); continue; } String tmp = new String(i.number, 0, i.insertIdx); if("---".equals(tmp)){ tmp = i.sb.toString(); } if(i.status == i.STATUS_CORRECT){ sb.append("<font color='#008b00'>"); }else if(i.status == i.STATUS_WARN){ sb.append("<font color='#FFA500'>"); }else if(i.status == i.STATUS_ERROR){ sb.append("<font color='#ff0000'>"); }else{ //... } sb.append(tmp); sb.append("</font>"); if(point > 0){ sb.append('.'); point--; } } return sb.toString(); } CharSequence getNormalTxt(){ StringBuilder sb = new StringBuilder(); int point = pointCounter; for(IPs i:ips){ if (i == null) { break; } if(i.isCharacterInvalid){ sb.append(i.sbInvalid.toString()); continue; } String tmp = new String(i.number, 0, i.insertIdx); if("---".equals(tmp)){ tmp = i.sb.toString(); } sb.append(tmp); if(point > 0){ sb.append('.'); point--; } } return sb.toString(); } @Override public String toString() { return getRichTxt().toString(); } private class IPs { final int STATUS_CORRECT = 0; final int STATUS_WARN = 1; final int STATUS_ERROR = 2; char[] number = new char[3]; int insertIdx; /** * 0 -- correct * 1 -- warn * 2 -- error * */ int status; /** * 当这个值为 true,所有内容都记录到 sbInvalid 中, * number[], sb 都没有内容。 * */ boolean isCharacterInvalid; StringBuilder sb; StringBuilder sbInvalid; IPs(){ number[0] = 0; number[1] = 0; number[2] = 0; } void initVariableArray(){ if (sb != null) { return; } sb = new StringBuilder(); // append old data. for(int i = 0; i < insertIdx; i++){ sb.append(number[i]); } // indicator number[0] = '-'; number[1] = '-'; number[2] = '-'; insertIdx = 3; } void initInvalidCharacterArray(){ if (sbInvalid != null) { return; } sbInvalid = new StringBuilder(); String tmp = new String(number, 0, insertIdx); if("---".equals(tmp)){ tmp = sb.toString(); } number[0] = 0; number[1] = 0; number[2] = 0; insertIdx = 0; sb = null; status = STATUS_ERROR; sbInvalid.append(tmp); isCharacterInvalid = true; } } } }