趁着時間挺充裕,就多寫幾篇博客。每一篇都是學習中的教訓。今天在做東西的時候突然想到之前在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中的快捷功能把它從模擬器中拖出來。
今天就到這吧。該去鍛煉了。也祝大家都能有個好身體。