趁着时间挺充裕,就多写几篇博客。每一篇都是学习中的教训。今天在做东西的时候突然想到之前在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>
有了自定义的控件,接着就是实现自定义控件类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中的快捷功能把它从模拟器中拖出来。
今天就到这吧。该去锻炼了。也祝大家都能有个好身体。