Android UI组件之自定义控件实现IP地址控件

  趁着时间挺充裕,就多写几篇博客。每一篇都是学习中的教训。今天在做东西的时候突然想到之前在MFC的时候都会有一个IP地址控件,可能是PC端用的比较多,但是在移动端好像基本没什么用处,但是偶尔也会有项目要用到,毕竟还是有些项目不需要接入互联网,只需要接入企业的内部网络。这个时候为了程序的通用性,我想到的第一个就是在程序中去配置一个网络环境,并将它保存到本地中,这样以后程序每次加载直接去本地中获取值。既然没有已有的控件,那么久自定义好了。存储在本地首先想到的就是sqlite和SharedPreferences,很明显这么点小东西还是用SharedPreferences又快又方便。那么按照老套路,先来规划一下程序的流程。

  既然要实现IP地址控件,那么首先就要考虑平常使用的习惯,通常我们输入IP地址,可以通过"."进入下一个编辑区域,也可以通过鼠标来直接跳到下一个区域,同时IP地址对数值的大小也有要求。必须是0-255之间。有了这个考虑就大概知道要处理什么事件。接下来就是开始实现。

  自定义IP地址控件ipedittext.xml,这里采用了四个EditText加三个TextView来实现控件。通过android:layout_weight属性让四个控件均匀的排列。

技术分享
 1 <?xml version="1.0" encoding="utf-8"?>
 2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 3     android:layout_width="match_parent"
 4     android:layout_height="wrap_content"
 5     android:layout_marginTop="10dp" 
 6     android:layout_marginLeft="10dp"
 7     android:layout_marginRight="10dp"
 8     android:paddingTop="10dp"
 9     android:paddingBottom="10dp"
10     android:orientation="horizontal"
11     android:background="@drawable/record_list_file_bg" >
12     <EditText
13         android:id="@+id/firstIPfield"
14         android:layout_width="0dp"
15         android:layout_height="wrap_content"
16         android:layout_weight="1"
17         android:gravity="center"
18         android:inputType="numberDecimal"
19         />
20     <TextView
21         android:layout_width="wrap_content"
22         android:layout_height="wrap_content"
23         android:text="."/>
24     <EditText
25         android:id="@+id/secondIPfield"
26         android:layout_width="0dp"
27         android:layout_height="wrap_content"
28         android:layout_weight="1"
29         android:gravity="center"
30         android:inputType="numberDecimal"
31         />
32     <TextView
33         android:layout_width="wrap_content"
34         android:layout_height="wrap_content"
35         android:text="."/>
36     <EditText
37         android:id="@+id/thirdIPfield"
38         android:layout_width="0dp"
39         android:layout_height="wrap_content"
40         android:layout_weight="1"
41         android:gravity="center"
42         android:inputType="numberDecimal"
43         />
44     <TextView
45         android:layout_width="wrap_content"
46         android:layout_height="wrap_content"
47         android:text="."/>
48     <EditText
49         android:id="@+id/fourthIPfield"
50         android:layout_width="0dp"
51         android:layout_height="wrap_content"
52         android:layout_weight="1"
53         android:gravity="center"
54         android:inputType="numberDecimal"
55         />
56 
57 </LinearLayout>
View Code

  有了自定义的控件,接着就是实现自定义控件类IPEditText.java。

  1 import android.content.Context;
  2 import android.text.Editable;
  3 import android.text.TextUtils;
  4 import android.text.TextWatcher;
  5 import android.util.AttributeSet;
  6 import android.util.Log;
  7 import android.view.LayoutInflater;
  8 import android.view.View;
  9 import android.widget.EditText;
 10 import android.widget.LinearLayout;
 11 import android.widget.Toast;
 12 
 13 public class IPEditText extends LinearLayout {
 14     private EditText firstIPEdit;
 15     private EditText secondIPEdit;
 16     private EditText thirdIPEdit;
 17     private EditText fourthIPEdit;
 18     
 19     private String firstIP;
 20     private String secondIP;
 21     private String thirdIP;
 22     private String fourthIP;
 23     
 24     public IPEditText(Context context, AttributeSet attrs) {
 25         super(context, attrs);
 26         // TODO Auto-generated constructor stub
 27         View view = LayoutInflater.from(context).inflate(R.layout.ipedittext, this);
 28         
 29         firstIPEdit = (EditText) view.findViewById(R.id.firstIPfield);
 30         secondIPEdit = (EditText) view.findViewById(R.id.secondIPfield);
 31         thirdIPEdit = (EditText) view.findViewById(R.id.thirdIPfield);
 32         fourthIPEdit = (EditText) view.findViewById(R.id.fourthIPfield);
 33         
 34         setIPEditTextListener(context);
 35     }
 36     
 37     public void setIPEditTextListener(final Context context){
 38         //设置第一个字段的事件监听
 39         firstIPEdit.addTextChangedListener(new TextWatcher() {
 40             @Override
 41             public void onTextChanged(CharSequence s, int start, int before, int count) {
 42                 // TODO Auto-generated method stub
 43                 Log.i("test",s.toString());
 44                 if(null!=s && s.length()>0){
 45                     if(s.length() > 2 || s.toString().trim().contains(".")){
 46                         if(s.toString().trim().contains(".")){
 47                             firstIP = s.toString().trim().substring(0,s.length()-1);
 48                         }else{
 49                             firstIP = s.toString().trim();
 50                         }
 51                         if (Integer.parseInt(firstIP) > 255) {
 52                             Toast.makeText(context, "IP大小在0-255之间",
 53                                     Toast.LENGTH_LONG).show();
 54                             return;
 55                         }
 56                         secondIPEdit.setFocusable(true);
 57                         secondIPEdit.requestFocus();
 58                     }
 59                     else
 60                     {
 61                         firstIP = s.toString().trim();
 62                     }
 63                 }
 64             }
 65             @Override
 66             public void beforeTextChanged(CharSequence s, int start, int count,
 67                     int after) {
 68                 // TODO Auto-generated method stub
 69             }
 70             @Override
 71             public void afterTextChanged(Editable s) {
 72                 // TODO Auto-generated method stub
 73                 firstIPEdit.removeTextChangedListener(this);
 74                 firstIPEdit.setText(firstIP);
 75                 firstIPEdit.setSelection(firstIP.length());
 76                 firstIPEdit.addTextChangedListener(this);
 77             }
 78         });
 79         //设置第二个IP字段的事件监听
 80         secondIPEdit.addTextChangedListener(new TextWatcher() {
 81             @Override
 82             public void onTextChanged(CharSequence s, int start, int before, int count) {
 83                 // TODO Auto-generated method stub
 84                 if(null!=s && s.length()>0){
 85                     if(s.length() > 2 || s.toString().trim().contains(".")){
 86                         if(s.toString().trim().contains(".")){
 87                             secondIP = s.toString().trim().substring(0,s.length()-1);
 88                             
 89                         }else{
 90                             secondIP = s.toString().trim();
 91                         }
 92                         if (Integer.parseInt(secondIP) > 255) {
 93                             Toast.makeText(context, "IP大小在0-255之间",
 94                                     Toast.LENGTH_LONG).show();
 95                             return;
 96                         }
 97                         thirdIPEdit.setFocusable(true);
 98                         thirdIPEdit.requestFocus();
 99                     }
100                     else
101                     {
102                         secondIP = s.toString().trim();
103                     }
104                     
105                 }
106             }    
107             @Override
108             public void beforeTextChanged(CharSequence s, int start, int count,
109                     int after) {
110                 // TODO Auto-generated method stub
111                 
112             }
113 
114             @Override
115             public void afterTextChanged(Editable s) {
116                 // TODO Auto-generated method stub
117                 secondIPEdit.removeTextChangedListener(this);
118                 secondIPEdit.setText(secondIP);
119                 secondIPEdit.setSelection(secondIP.length());
120                 secondIPEdit.addTextChangedListener(this);
121             }
122         });
123         //设置第三个IP字段的事件监听
124         thirdIPEdit.addTextChangedListener(new TextWatcher() {
125             
126             @Override
127             public void onTextChanged(CharSequence s, int start, int before, int count) {
128                 // TODO Auto-generated method stub
129             
130                 if(null!=s && s.length()>0){
131                     if(s.length() > 2 || s.toString().trim().contains(".")){
132                         if(s.toString().trim().contains(".")){
133                             thirdIP = s.toString().trim().substring(0,s.length()-1);
134                             
135                         }else{
136                             thirdIP = s.toString().trim();
137                         }
138                         if (Integer.parseInt(thirdIP) > 255) {
139                             Toast.makeText(context, "IP大小在0-255之间",
140                                     Toast.LENGTH_LONG).show();
141                             return;
142                         }
143                         fourthIPEdit.setFocusable(true);
144                         fourthIPEdit.requestFocus();
145                     }else{
146                         thirdIP = s.toString().trim();
147                     }
148                     
149                 }
150             }
151             @Override
152             public void beforeTextChanged(CharSequence s, int start, int count,
153                     int after) {
154                 // TODO Auto-generated method stub
155                 
156             }
157             
158             @Override
159             public void afterTextChanged(Editable s) {
160                 // TODO Auto-generated method stub
161                 thirdIPEdit.removeTextChangedListener(this);
162                 thirdIPEdit.setText(thirdIP);
163                 thirdIPEdit.setSelection(thirdIP.length());
164                 thirdIPEdit.addTextChangedListener(this);
165                 
166             }
167         });
168         //设置第四个IP字段的事件监听
169         fourthIPEdit.addTextChangedListener(new TextWatcher() {
170             
171             @Override
172             public void onTextChanged(CharSequence s, int start, int before, int count) {
173                 // TODO Auto-generated method stub
174                 if(null!=s && s.length()>0){
175                     fourthIP = s.toString().trim();
176                     if (Integer.parseInt(fourthIP) > 255) {
177                         Toast.makeText(context, "请输入合法的ip地址", Toast.LENGTH_LONG)
178                                 .show();
179                         return;
180                     }
181                 }
182             }
183             
184             @Override
185             public void beforeTextChanged(CharSequence s, int start, int count,
186                     int after) {
187                 // TODO Auto-generated method stub
188                 
189             }
190             
191             @Override
192             public void afterTextChanged(Editable s) {
193                 // TODO Auto-generated method stub
194                 
195             }
196         });
197     }
198     
199     public String getText(Context context) {
200         if (TextUtils.isEmpty(firstIP) || TextUtils.isEmpty(secondIP)
201                 || TextUtils.isEmpty(thirdIP) || TextUtils.isEmpty(fourthIP)) {
202             Toast.makeText(context, "请输入完整的IP", Toast.LENGTH_LONG).show();
203         }
204         return firstIP + "." + secondIP + "." + thirdIP + "." + fourthIP;
205     }
206 }

  代码有点多,那是因为前面三个控件的逻辑比较多,搞定一个另外两个修改一下就好了,最后一个EditText比较简单。简单的说明一下事件处理。通过在EditText上添加addTextChangedListener函数来这是一个监听器,观察控件内容的变化。参数是一个TextWatcher接口,实现他的三个方法就能够对控件内值得变化进行一个处理。

  对于控件中内容发生变化时,首先我们要判断新增的字符是不是".",和长度有没有超过3位,若果是其中的任意一种,就可以处理值并跳转到下一个EditText控件。如果都不是,那就可以直接获取控件的内容并设置显示。逻辑还是比较简单的。那么就这么简单的结束了吗?肯定不是。

  贴出来的代码肯定都是功能完善的代码,但是最初的代码肯定不是这样。对于头脑比较简单的我来说,在最初认为,既然EditText中的内容改变了,那我直接获取改变后的值然后立即通过EditText.setText来设置不就行了吗?所以最初afterTextChanged()中的firstIPEdit.setText(firstIP);其实是放在onTextChanged()中的。然后就出现了红色的error和不幸的程序崩溃了。查看了一下logcat日志,报的错误是stackoverflow(栈溢出),发现原来是函数堆栈溢出,进入了一个函数调用的死循环,这里用循环不恰当,准确的说是连锁调用,导致最终栈溢出,为什么?当一次改动发生后,判断值是合法有效的,然后调用EditText.setText(),因为setText()会去改变控件的内容,而控件此时正在被TextWatch监视着,于是又进入onTextChanged,然后再一次判断,再一次合法,再一次设置内容,再一次判断,于是程序一直向前走,函数堆栈越来越大最终溢出。

  现在知道是因为什么溢出的就来看看这个TextWatcher,既然能够通过addTextChangedListener添加一个TextWatcher进行监听,那么也应该可以remove掉,但是在TextWatcher这个实例的函数中remove它自己能成功吗?于是再一次去SDK中看看remove函数是怎么工作的。发现removeTextChangedListener和我们想的不太一样,他并不是在remove的时候就释放掉那个对象,而只是暂时把他从监视Textview类型(EditText是继承TextView)的对象中的一个队列中移除。函数介绍如下: 

public void removeTextChangedListener (TextWatcher watcher)

Added in API level 1
Removes the specified TextWatcher from the list of those whose methods are called whenever this TextView‘s text changes.

  于是来尝试将setText函数移动到afterTextChanged()函数中并加上removeTextChangedListener和addTextChangedListener。程序果然没有崩掉,但是奇怪的事情发生了。光标居然没有在后面反倒跑到内容前面,并且值得大小也不是123,而是321,在logcat中也能看出来:

    02-04 12:54:56.861: I/test(573): 1
    02-04 12:54:59.392: I/test(573): 21
    02-04 12:55:01.591: I/test(573): 321

  那么为什么会这样呢,原来在有内容的情况下EditText中的光标会在最前面,每一次输入都是往光标后输入,这样就会每输入一次在光标后添加上字符之后,光标又跑到最前面。在网上搜索一下,发现可以通过EditText.setSelection()函数解决,于是尝试之,果然奏效。程序正常。

  接下来就是写入SharedPreferences中,对于SharedPreferences的操作比较简单,首先是通过上下文环境去得到一个SharedPreferences文件的对象,然后通过获取这个对象的editor来对它进行操作。这里为了方便就先把另外两个参数写死。因为我的事件监听的类是单独写在一个包里的,所以上下文环境需要获取,如果是在同一个类里的话就不用我这么麻烦。记住一定要commit。

1 case R.id.config:
2             mAvtivity = MainActivity.get();
3             Context context = mAvtivity;
4             SharedPreferences sp = context.getSharedPreferences("config", Context.MODE_PRIVATE);
5             Editor editor = sp.edit();
6             editor.putString("server_ip", mAvtivity.getmIPEditText().getText(mAvtivity).toString());
7             editor.putInt("server_port", 8080);
8             editor.putBoolean("isserver", true);
9             editor.commit();

  结果如下图,成功保存,这样程序以后只需要在网络处理的类中去读取IP就可以了,端口和网站地址同上。

技术分享

技术分享

  最后一句话,保存的SharedPreferences文件本质上是一个XML文件,在/data/data/PACKAGE_NAME/shared_prefs/下,可以通过DDMS中的快捷功能把它从模拟器中拖出来。技术分享

  今天就到这吧。该去锻炼了。也祝大家都能有个好身体。

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。