Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android自定義控件系列案例【五】

Android自定義控件系列案例【五】

編輯:關於Android編程

案例效果:

\\

實例分析:

在開發銀行相關客戶端的時候或者開發在線支付相關客戶端的時候經常要求用戶綁定銀行卡,其中銀行卡號一般需要空格分隔顯示,最常見的就是每4位數以空格進行分隔,以方便用戶實時比對自己輸入的卡號是否正確。當產品經理或UI設計師把這樣的需求拿給我們的時候,我們的大腦會馬上告訴我們Android中有個EditText控件可以用來輸入卡號,但好像沒見過可以分隔顯示的屬性或方法啊。當我們睜大眼睛對著效果圖正發呆的時候,突然發現當用戶輸入內容的時候還出現了清除圖標,點擊清空圖標還可以清空用戶輸入的內容。
本案例將帶大家解決銀行卡號分隔顯示與添加清空圖標這兩個問題,采用的解決方式依然是自定義控件,並且擴展於EditText控件。

(1)銀行卡號分隔邏輯分析:

因為要在用戶輸入內容過程中實時的讓用戶看到每4位(可配置N位)以空格分隔顯示,並且有空格時光標會跳過空格定位到下一個輸入位,所以就需要對輸入框內容進行實時監聽,要實現這一點,我們可以使用EditText控件中的addTextChangedListener監聽器,並注冊TextWatcher回調接口,然後在回調方法onTextChanged()中可以實時獲取用戶輸入的全部內容。獲得內容後我們就可以遍歷這個內容串,然後每取4位(或N位)就在後面加一個空格,直到最後把剩下的不夠4位(或N位)的內容拼接到最後,這樣就得到了一個我們期望用戶看到的新的字符串,把這個新的字符串重新設置給輸入框就可以解決銀行卡號分隔顯示問題了。但是當我們運行後發現內容是分隔顯示了,光標卻很不正常,原因是當我們通過代碼的方式為輸入框設置內容後EditText認為是你接管了它,所以光標也一並交由我們去管理,所以我們需要根據顯示的內容定位光標到合適的位置,光標定位可以用EditText的setSelection(int index),除此之外需要考慮當添加一位新數或回退刪除一位當前數時對光標定位帶來的影響。當然要設計一個良好的自定義控件,我們需要考慮更細節的問題,比如這個輸入框如果想通用的話我們需要控制它顯示的內容類型,當作為銀行號卡輸入框時應該控制只能輸入數字,否則作為普通輸入框,讓用戶可以輸入任何原EditText支持的內容類型。並且一般銀行卡號最長21位數(可配置N位),多於21位(或N位)就不讓用戶再輸入了,這樣做的目的是為了減少服務端對無效卡號的校驗時間,從而提高響應客戶端的速度(性能優化必考慮的問題),至此才算基本完成了銀行卡號顯示的邏輯思考。來張圖理一下思路:
\

(2)清空功能邏輯分析:

清空功能邏輯相對要簡單一些,但也有一些值得我們思考的地方,比如清空圖標在輸入框內側右邊,換句話說就是清空圖標首先是在輸入裡,做為輸入框的一部分,然後是在右邊。對EditText控件比較熟悉的朋友可能已經想到可以使用setCompoundDrawables(left, top, right, bottom)方法為一個控件添加左,上,右,下內側圖標,沒錯,我們就用這個方法來顯示清空圖標,但是接下來的問題時,我們怎麼讓它和用讓交互?Android沒有提供對這些內側圖標的點擊監聽器,也就是我們不能指望為控件的內側圖標添加一個onClickListener()來處理交互邏輯,所以我們使用onTouchEvent()為自定義輸入框注冊觸摸監聽器,然後獲得右側圖標的顯示區域和用戶點擊的點上的坐標,通過判斷用戶點擊和點坐標正好落在了右側圖標的顯示區域去觸發圖標的交互邏輯。解決了清空圖標的顯示與交互問題基本就大功告成了,但還有一些細節我們得考慮一下,比如只有當輸入框有內容的時候才顯示清空圖標,當輸入框內容清空後,清空圖標要從顯示變成隱藏或消失,當輸入框失去焦點時(比如切換到下一個輸入框),清空圖標也要隱藏或消失,當輸入框再次獲得焦點時(比如從其它輸入框又切換回來),如果輸入框是有內容的,則顯示清空圖標。所以我們需要監聽輸入框焦點變化,然後處理焦點變化帶來的影響,關於這個問題,因為EditText本身是添加了焦點變化監聽器的,每次焦點變化都會回調onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect)方法,我們只需要重寫這個方法,然後在這個方法中處理焦點變化後的邏輯。至此才算基本完成了清空功能的邏輯思考。來張圖理一下思路:
\ 技術准備: (1)addTextChangedListener(TextWatcher watcher)
為TextView或EditText及子類注冊內容改變監聽器,
(2)TextWatcher
內容改變之後的回調接口,有三個方法需要實現:
a)public void onTextChanged(CharSequence s, int start, int before, int count)
內容一旦改變就回調此方法,無論是內容增加還是減少。
參數說明:
 
s代表內容改變後的全部字符串,
start代表增加或刪除字符時的位置索引
before代表由什麼原因引起的內容變化(0表示由增加字符引起的內容變化,1表示由刪除字符引起的內容變化)
count代表增加或刪除了多少個字符,(測試發現刪除字符時這個值一直為0)
b)public void beforeTextChanged(CharSequence s, int start, int count, int after)
內容改變之前回調的方法,本案例用不到。
c)public void afterTextChanged(Editable s)
內容改變後回調的方法,本案例用不到。
注意:內容改變時這個回調方法會多次調用,導至回調出來的結果不一定是哪次的,所以可以定義一個boolean變化,確保每次內容改變後只使用一次onTextChanged裡的值可解決這個問題。比如:
 
boolean isTextChang = false;
if (isTextChang) {
isTextChang= false;
return;
}
isTextChang= true;
(3)setCompoundDrawables(left, top, right, bottom)
為控件添加左,右,上,下內側圖標,如果不希望添哪個,則傳入null即可,注意參數類型為Drawable。 比如只顯示右內側圖標setCompoundDrawables(null, null, rightDrawable, null);
(4)onTouchEvent(MotionEvent event)
控件觸摸監聽回調方法,當控件按下,移動,抬起時都會回調此方法。
(5)onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect)
控件焦點變化監聽回調方法,當控件獲得焦點或失去焦點都會回調此方法
參數說明:
focused是否獲得焦點,true代表獲得焦點,false代表失去焦點, direction下一個獲取焦點的去向 ,與焦點獲取方案有關,默認方案為從左到右, 從上到下的方向。 previouslyFocusedRect前一個獲得焦點的控件的顯示區域。

實現步驟:

銀行卡號顯示功能實現步驟

(1)自定義一個輸入框控件,繼承於EdiText;

重寫構造方法,並排列構造方法的調用順序

(2)使用自定義輸入框對案例布局進行布局;

此時的自定義輸入框只有構造方法,把它當作普通的EditText進行布局即可,此時布局的目的僅僅是為了占位。
為了突出重點,布局的時候字符串,尺寸,顏色資源什麼的都直接硬編碼了,實際項目中應該提取到對應的資源文件中。

(3)配置自定義控件為普通輸入框控件或銀行卡號輸入框控件;

通過自定義屬性,定義一個boolean類型屬性,為true是代表是銀行卡號模式的控件(默認模式),為false時代表是普通輸入框。並在自定義控件中定義一個與自定義屬性對應的成員變量,然後在初始化時獲得自定義屬性並為自定義屬性對應的成員變量賦值。
a)創建自定義屬性XML文件values/attrs.xml,並定義自定義屬性isCardNumber,值類型為boolean
b)在自定義控件中定義與自定義屬性對應的成員變量
c)定義初始化方法,獲取自定義屬性並為對應的成員變量賦值

(4)初始化輸入框為單行顯示並可獲得焦點

在初始化方法,完成輸入框單選行控制和可獲得焦點控制

(5)配置自定義控件在銀行卡號模式下分隔位數

默認為每4位數進行空格分隔,但為了靈活,我們自定義屬性讓這部分可配置。
a)自定義屬性splitNumber,值類型為integer
b)在自定義控件中定義與自定義屬性對應的成員變量
c)在初始化方法,獲取自定義屬性並為對應的成員變量賦值

(6)實現銀行卡號顯示功能

a)注冊輸入框內容改變監聽器和數據回調接口;
b)定義避免多次使用onTextChange()回調方法返回值的boolean變量,isTextChanged = false;
c)分隔輸入內容,並顯示分隔後的內容;
d)處理光標位置邏輯;
e)在XML布局中使用自定義屬性配置持卡人輸入框為普通輸入框,卡號為銀行卡號輸入框。

清空圖標功能實現步驟

(1)准備清空圖標,並在自定義輸入框中設計顯示清空圖標的方法; (2)重寫onTouchEvent()方法,處理點擊清空圖標邏輯; (3)重寫onFocusChanged()方法,處理焦點改變時,清空圖標顯示與隱藏邏輯。

技術實現:

銀行卡號顯示功能實現

step1:自定義一個輸入框控件,繼承於EditText

 

 

package com.kedi.myedittext;

import android.content.Context;
import android.util.AttributeSet;
import android.widget.EditText;

/**
 * 自定義EditText控件
 * 
 * @author 張科勇
 *
 */
public class MyEditText extends EditText {

	public MyEditText(Context context) {
		this(context, null);
	}

	public MyEditText(Context context, AttributeSet attrs) {
		this(context, attrs, 0);
	}

	public MyEditText(Context context, AttributeSet attrs, int defStyleAttr) {
		super(context, attrs, defStyleAttr);
	}


}

 

step2:使用自定義輸入框對案例布局進行布局

這部分暫時沒什麼特殊的,平時怎麼布局,現在在這裡就怎麼布局,當然因為使用了自定義控件,所以需要加包全名。

 



    

    

        

        
        
    

    

        

        
        
    

此時的布局效果:

 

\ 效果與案例一樣,但邏輯還沒有加,目前只是個架子
step3:配置自定義控件為普通輸入框或銀行卡號輸入框

a)創建自定義屬性XML文件values/attrs.xml

 




    
        
        
    

 

b)自定義控件中定義與自定義屬性對應的成員變量

 

 

//自定義輸入框的模式 當值為true:銀行卡號輸入框模式,false:普通輸入框模式 
	private boolean isCardNumber = true;

 

c)定義初始化方法,獲取自定義屬性並為對應的成員變量賦值

 

核心代碼:

 

/**
	 * 初始化方法
	 */
	private void init(AttributeSet attrs) {
		TypedArray t = this.getResources().obtainAttributes(attrs, R.styleable.MyEditText);
		isCardNumber = t.getBoolean(R.styleable.MyEditText_isCardNumber, isCardNumber);
		t.recycle();
	}
完整代碼:

 

package com.kedi.myedittext;

import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.widget.EditText;

/**
 * 自定義EditText控件
 * 
 * @author 張科勇
 *
 */
public class MyEditText extends EditText {

	//自定義輸入框的模式 當值為true:銀行卡號輸入框模式,false:普通輸入框模式 
	private boolean isCardNumber = true;

	public MyEditText(Context context) {
		this(context, null);
	}

	public MyEditText(Context context, AttributeSet attrs) {
		this(context, attrs, 0);
	}

	public MyEditText(Context context, AttributeSet attrs, int defStyleAttr) {
		super(context, attrs, defStyleAttr);
		init(attrs);
	}

	/**
	 * 初始化方法
	 */
	private void init(AttributeSet attrs) {

		TypedArray t = this.getResources().obtainAttributes(attrs, R.styleable.MyEditText);
		isCardNumber = t.getBoolean(R.styleable.MyEditText_isCardNumber, isCardNumber);
		t.recycle();
	}
}

step4:初始化時設置輸入框為單行顯示並可獲得焦點

	/**
	 * 初始化方法
	 */
	private void init(AttributeSet attrs) {
		// 設置單行顯示所有輸入框內容
		setSingleLine();
		//設置輸入框可獲得焦點
		setFocusable(true);
		setFocusableInTouchMode(true);
		TypedArray t = this.getResources().obtainAttributes(attrs, R.styleable.MyEditText);
		isCardNumber = t.getBoolean(R.styleable.MyEditText_isCardNumber, isCardNumber);
		t.recycle();
	}

 

step5:配置自定義控件在銀行卡號模式下分隔位數

a)自定義屬性splitNumber,值類型為integer




    

        
        
        
        

    

 

b)在自定義控件中定義與自定義屬性對應的成員變量

// 每隔多少位以空格進行分隔一次,卡號一般都是每4位以空格分隔一次
	public int splitNumber = 4;

c)在初始化方法,獲取自定義屬性並為對應的成員變量賦值

	/**
	 * 初始化方法
	 */
	private void init(AttributeSet attrs) {
		// 設置單行顯示所有輸入框內容
		setSingleLine();
		//設置輸入框可獲得焦點
		setFocusable(true);
		setFocusableInTouchMode(true);
		TypedArray t = this.getResources().obtainAttributes(attrs, R.styleable.MyEditText);
		isCardNumber = t.getBoolean(R.styleable.MyEditText_isCardNumber, isCardNumber);
		splitNumber = t.getInt(R.styleable.MyEditText_splitNumber, splitNumber);
		t.recycle();
	}

step6:實現銀行卡號顯示功能

a)注冊輸入框內容改變監聽器和數據回調接口
定義一個專門處理事件的方法initEvent(),將注冊事件邏輯放到這個方法,然後在初始化init()方法中調用

 

private void initEvent() {
			addTextChangedListener(new TextWatcher() {
				@Override
				public void onTextChanged(CharSequence s, int start, int before, int count) {
				
				}

				@Override
				public void beforeTextChanged(CharSequence s, int start, int count, int after) {

				}

				@Override
				public void afterTextChanged(Editable s) {

				}
			});

		}

		/**
		 * 初始化方法
		 */
		private void init(AttributeSet attrs) {
			// 設置單行顯示所有輸入框內容
			setSingleLine();
			//設置輸入框可獲得焦點
			setFocusable(true);
			setFocusableInTouchMode(true);
			TypedArray t = this.getResources().obtainAttributes(attrs, R.styleable.MyEditText);
			isCardNumber = t.getBoolean(R.styleable.MyEditText_isCardNumber, isCardNumber);
			splitNumber = t.getInt(R.styleable.MyEditText_splitNumber, splitNumber);
			t.recycle();
			initEvent();//調用initEvent()方法,在初始化的時候完成對輸入框內容改變的監聽
		}
 

b)定義避免多次使用onTextChange()回調方法返回值的boolean變量,isTextChanged = false;

 

// 輸入框內容改變後onTextChanged方法會調用多次,設置一個變量讓其每次改變之後只調用一次
private boolean isTextChanged = false;

邏輯控制是在onTextChange()回調方法中:代碼如下:

 

/**
* 處理事件的方法
*/
private void initEvent() {
	addTextChangedListener(new TextWatcher() {
	@Override
	public void onTextChanged(CharSequence s, int start, int before, int count) {
		if (isTextChanged ) {
			isTextChanged = false;
			return;
		}
		isTextChanged = true;
	}
	@Override
	public void beforeTextChanged(CharSequence s, int start, int count, int after) {
	}
	@Override
	public void afterTextChanged(Editable s) {
	}
	});
}

 

c)分隔輸入內容,並顯示分隔後的內容

 

設計一個方法,專門處理此邏輯,並在onTextChanged()方法中調用,注意定義的成員變量和傳遞的參數。

 

// 卡號內容
	private String content;
	// 卡號最大長度,卡號一般最長21位
	public static final int MAX_CONTENT_LENGHT = 21;
	// 緩沖分隔後的新內容串
	private String result = "";
	/**
	 * 處理輸入內容空格與位數的邏輯 ,參數s為onTextChanged()方法中獲得的實時輸入內容,before是代碼增加字符還是回退刪除字符,
	 */
	private void handleInputContent(CharSequence s,int before) {
		if (isCardNumber) {
			//如果isCardNumber=true,說明是銀行卡號輸入框,控制只能輸入數字,否則按原特性處理
			setInputType(InputType.TYPE_CLASS_NUMBER);
			content = s.toString();
			//先緩存輸入框內容
			result = content;
			//去掉空格,以防止用戶自己輸入空格
			content = content.replace(" ", "");
			// 限制輸入的數字位數最多21位(銀行卡號一般最多21位)
			if (content != null && content.length() <= MAX_CONTENT_LENGHT) {
				result = "";
				int i = 0;
				// 先把splitNumber倍的字符串進行分隔
				while (i + splitNumber < content.length()) {
					result += content.substring(i, i + splitNumber) + " ";
					i += splitNumber;
				}
				// 最後把不夠splitNumber倍的字符串加到末尾
				result += content.substring(i, content.length());
			} else {
				//如果用戶輸入的位數
				result = result.substring(0, result.length() - 1);
			}
			// 獲取光標開始位置
			// 必須放在設置內容之前
			int j = getSelectionStart();
			setText(result);
		      // 處理光標位置,此邏輯又專門封裝到一個方法 handleCursor(int before, int j)中在下面步驟中實現
}

	}

 

在輸入框內容改變監回調方法中用戶分隔內容的處理方法:

 

/**
	 * 處理事件的方法
	 */
	private void initEvent() {
		addTextChangedListener(new TextWatcher() {
			@Override
			public void onTextChanged(CharSequence s, int start, int before, int count) {
				if (isTextChanged) {
					isTextChanged = false;
					return;
				}
				isTextChanged = true;
				// 處理輸入內容空格與位數以及光標位置的邏輯
				handleInputContent(s,before);
				
			}

			@Override
			public void beforeTextChanged(CharSequence s, int start, int count, int after) {

			}

			@Override
			public void afterTextChanged(Editable s) {

			}
		});

	}

d)處理光標位置邏輯

 

	/**
	 * 處理光標位置
	 *
	 * @param before
	 * @param j
	 */
	private void handleCursor(int before, int j) {
		// 處理光標位置
		try {
			if (j + 1 < result.length()) {
				// 添加字符
				if (before == 0) {
					//遇到空格,光標跳過空格,定位到空格後的位置
					if (j % splitNumber + 1 == 0) {
						setSelection(j + 1);
					} else {
						//否則,光標定位到內容之後 (光標默認定位方式)
						setSelection(result.length());
					}
					// 回退清除一個字符
				} else if (before == 1) {
					//回退到上一個位置(遇空格跳過)
					setSelection(j);
				}
			} else {
				MyEditText.this.setSelection(result.length());
			}
		} catch (Exception e) {

		}
}
什麼時候調用上面的處理光標定位的方法呢?

 

在handleInputContent()處理完分隔與顯示的時候用戶光標定位方法,處理光標定位問題:

 

	// 卡號內容
	private String content;
	// 卡號最大長度,卡號一般最長21位
	public static final int MAX_CONTENT_LENGHT = 21;
	// 緩沖分隔後的新內容串
	private String result = "";
	/**
	 * 處理輸入內容空格與位數的邏輯 ,參數s為onTextChanged()方法中獲得的實時輸入內容,before是代碼增加字符還是回退刪除字符,
	 */
	private void handleInputContent(CharSequence s,int before) {
		if (isCardNumber) {
			//如果isCardNumber=true,說明是銀行卡號輸入框,控制只能輸入數字,否則按原特性處理
			setInputType(InputType.TYPE_CLASS_NUMBER);
			content = s.toString();
			//先緩存輸入框內容
			result = content;
			//去掉空格,以防止用戶自己輸入空格
			content = content.replace(" ", "");
			// 限制輸入的數字位數最多21位(銀行卡號一般最多21位)
			if (content != null && content.length() <= MAX_CONTENT_LENGHT) {
				result = "";
				int i = 0;
				// 先把splitNumber倍的字符串進行分隔
				while (i + splitNumber < content.length()) {
					result += content.substring(i, i + splitNumber) + " ";
					i += splitNumber;
				}
				// 最後把不夠splitNumber倍的字符串加到末尾
				result += content.substring(i, content.length());
			} else {
				//如果用戶輸入的位數
				result = result.substring(0, result.length() - 1);
			}
			// 獲取光標開始位置
			// 必須放在設置內容之前
			int j = getSelectionStart();
			setText(result);
		        // 處理光標位置
                       handleCursor(before, j);
}

	}

 

完整邏輯代碼:

 

package com.kedi.myedittext;

import android.content.Context;
import android.content.res.TypedArray;
import android.text.Editable;
import android.text.InputType;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.widget.EditText;

/**
 * 自定義EditText控件
 * 
 * @author 張科勇
 *
 */
public class MyEditText extends EditText {
	// 每隔多少位以空格進行分隔一次,卡號一般都是每4位以空格分隔一次
	public int splitNumber = 4;
	// 自定義輸入框的模式 當值為true:銀行卡號輸入框模式,false:普通輸入框模式
	private boolean isCardNumber = true;

	public MyEditText(Context context) {
		this(context, null);
	}

	public MyEditText(Context context, AttributeSet attrs) {
		this(context, attrs, 0);
	}

	public MyEditText(Context context, AttributeSet attrs, int defStyleAttr) {
		super(context, attrs, defStyleAttr);
		init(attrs);
	}

	/**
	 * 初始化方法
	 */
	private void init(AttributeSet attrs) {
		// 設置單行顯示所有輸入框內容
		setSingleLine();
		// 設置輸入框可獲得焦點
		setFocusable(true);
		setFocusableInTouchMode(true);
		TypedArray t = this.getResources().obtainAttributes(attrs, R.styleable.MyEditText);
		isCardNumber = t.getBoolean(R.styleable.MyEditText_isCardNumber, isCardNumber);
		splitNumber = t.getInt(R.styleable.MyEditText_splitNumber, splitNumber);
		t.recycle();
		initEvent();
	}

	// 輸入框內容改變後onTextChanged方法會調用多次,設置一個變量讓其每次改變之後只調用一次
	private boolean isTextChanged = false;
	/**
	 * 處理事件的方法
	 */
	private void initEvent() {
		addTextChangedListener(new TextWatcher() {
			@Override
			public void onTextChanged(CharSequence s, int start, int before, int count) {
				if (isTextChanged) {
					isTextChanged = false;
					return;
				}
				isTextChanged = true;
				// 處理輸入內容空格與位數以及光標位置的邏輯
				handleInputContent(s,before);
			}

			@Override
			public void beforeTextChanged(CharSequence s, int start, int count, int after) {

			}

			@Override
			public void afterTextChanged(Editable s) {

			}
		});

	}
	// 卡號內容
	private String content;
	// 卡號最大長度,卡號一般最長21位
	public static final int MAX_CONTENT_LENGHT = 21;
	// 緩沖分隔後的新內容串
	private String result = "";
	/**
	 * 處理輸入內容空格與位數的邏輯
	 */
	private void handleInputContent(CharSequence s,int before) {
		if (isCardNumber) {
			//如果isCardNumber=true,說明是銀行卡號輸入框,控制只能輸入數字,否則按原特性處理
			setInputType(InputType.TYPE_CLASS_NUMBER);
			content = s.toString();
			//先緩存輸入框內容
			result = content;
			//去掉空格,以防止用戶自己輸入空格
			content = content.replace(" ", "");
			// 限制輸入的數字位數最多21位(銀行卡號一般最多21位)
			if (content != null && content.length() <= MAX_CONTENT_LENGHT) {
				result = "";
				int i = 0;
				// 先把splitNumber倍的字符串進行分隔
				while (i + splitNumber < content.length()) {
					result += content.substring(i, i + splitNumber) + " ";
					i += splitNumber;
				}
				// 最後把不夠splitNumber倍的字符串加到末尾
				result += content.substring(i, content.length());
			} else {
				//如果用戶輸入的位數
				result = result.substring(0, result.length() - 1);
			}
			// 獲取光標開始位置
			// 必須放在設置內容之前
			int j = getSelectionStart();
			setText(result);
			// 處理光標位置
			handleCursor(before, j);
		}

	}

	/**
	 * 處理光標位置
	 *
	 * @param before
	 * @param j
	 */
	private void handleCursor(int before, int j) {
		// 處理光標位置
		try {
			if (j + 1 < result.length()) {
				// 添加字符
				if (before == 0) {
					//遇到空格,光標跳過空格,定位到空格後的位置
					if (j % splitNumber + 1 == 0) {
						setSelection(j + 1);
					} else {
						//否則,光標定位到內容之後 (光標默認定位方式)
						setSelection(result.length());
					}
					// 回退清除一個字符
				} else if (before == 1) {
					//回退到上一個位置(遇空格跳過)
					setSelection(j);
				}
			} else {
				MyEditText.this.setSelection(result.length());
			}
		} catch (Exception e) {

		}
	}

}

e)在XML布局中使用自定義屬性配置持卡人輸入框為普通輸入框,卡號為銀行卡號輸入框。

首先在布局根容器中添加一個自定義屬性的命名空間:

 




 

然後在對應的自定義控件上使用自定義屬性,並為其指定屬性值:比如不設置app:isCardNumber屬性代表自定義輸入框為普通輸入框:

 


 
設置app:isCardNumber= "true",則自定義輸入框為銀行卡號輸入框,如果還指定app:splitNumber = "4" ,設置每4位數用空格分隔:

 

 

完整布局:



    

    

        

        
        
    

    

        

        
        
    

 

實現效果:

\

到此銀行卡分隔顯示相關功能就實現完成了,此時如果我們發現輸入的卡號全錯了想重新輸入,如果沒有清空功能,多顯不便,所以接下來就是在前面功能的基礎上為自定義輸入框添加清空輸入內容功能。

 

 

清空圖標功能實現

step1:准備清空圖標,並在自定義輸入框中設計顯示清空圖標的方法

 

a)導入清空圖標到drawable目錄

\clear.png
b)在自定義輸入框中定義一個Drawable類型的成員變量, c)在初始化方法中為成員變量賦值,並為mClearDrawable設置一個交互區域
// 內容清除圖標
	private Drawable mClearDrawable;

	/**
	 * 初始化方法
	 */
	private void init(AttributeSet attrs) {
		// 設置單行顯示所有輸入框內容
		setSingleLine();
		// 設置輸入框可獲得焦點
		setFocusable(true);
		setFocusableInTouchMode(true);
		TypedArray t = this.getResources().obtainAttributes(attrs, R.styleable.MyEditText);
		isCardNumber = t.getBoolean(R.styleable.MyEditText_isCardNumber, isCardNumber);
		splitNumber = t.getInt(R.styleable.MyEditText_splitNumber, splitNumber);
		t.recycle();
		mClearDrawable = this.getResources().getDrawable(R.drawable.clear);
		mClearDrawable.setBounds(0, 0, mClearDrawable.getIntrinsicWidth(), mClearDrawable.getIntrinsicHeight());
		initEvent();
	}

d)設計顯示和控制內側圖標的方法,以供其它地方控件圖標的顯示和隱藏

/**
	 * 設置輸入框的左,上,右,下圖標
	 *
	 * @param left
	 * @param top
	 * @param right
	 * @param bottom
	 */
	private void setEditTextIcon(Drawable left, Drawable top, Drawable right, Drawable bottom) {

		setCompoundDrawables(left, top, right, bottom);
	}
	 /**
	 * 處理清除圖標的邏輯,在onTextChange()方法中,當內容改變,光標位置完成,最後調用此方法處理清空圖標的顯示和隱藏
	 *
	 * @param content
	 */
	 private void handleClearIcon() {
	 if (content != null && content.length() > 0) {
	 // 顯示
	 setEditTextIcon(null, null, mClearDrawable, null);
	 } else {
	 // 隱藏
	 setEditTextIcon(null, null, null, null);
	 }
	 }

step2:重寫onTouchEvent()方法,處理點擊清空圖標邏輯

@Override
	public boolean onTouchEvent(MotionEvent event) {
		//獲取用戶點擊的坐標,這裡只對X軸做了判斷,
		float x = event.getX();
		//當用戶抬起手指時,判斷坐標是否在圖標交互區域,如果在則清空輸入框內容,同時隱藏圖標自己
		if (event.getAction() == MotionEvent.ACTION_UP) {
			if (x > (getWidth() - getPaddingRight() - mClearDrawable.getIntrinsicWidth())) {
				//清空輸入框內容
				setText("");
				//隱藏圖標
				setEditTextIcon(null, null, null, null);
			}
		}
		return super.onTouchEvent(event);
	}

step3: 重寫onFocusChanged()方法,處理焦點改變時,清空圖標顯示與隱藏邏輯

@Override
	protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
		super.onFocusChanged(focused, direction, previouslyFocusedRect);
		//判斷當focused為true時,說明獲取了焦點,此時如果輸入框有內容,則顯示清空圖標,否則顯示清空圖標
		if (focused && (content != null && content.length() > 0)) {
			setEditTextIcon(null, null, mClearDrawable, null);
		} else {
			setEditTextIcon(null, null, null, null);
		}
		//刷新界面,防止有時候出現的不刷新界面情況
		invalidate();
	}
到此實現了案例中的所有功能和邏輯,完整自定義控件代碼:
package com.kedi.myedittext;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.text.Editable;
import android.text.InputType;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.EditText;

/**
 * 自定義EditText控件
 * 
 * @author 張科勇
 *
 */
public class MyEditText extends EditText {

	// 每隔多少位以空格進行分隔一次,卡號一般都是每4位以空格分隔一次
	public int splitNumber = 4;
	// 自定義輸入框的模式 當值為true:銀行卡號輸入框模式,false:普通輸入框模式
	private boolean isCardNumber = true;

	public MyEditText(Context context) {
		this(context, null);
	}

	public MyEditText(Context context, AttributeSet attrs) {
		this(context, attrs, 0);
	}

	public MyEditText(Context context, AttributeSet attrs, int defStyleAttr) {
		super(context, attrs, defStyleAttr);
		init(attrs);
	}

	// 內容清除圖標
	private Drawable mClearDrawable;

	/**
	 * 初始化方法
	 */
	private void init(AttributeSet attrs) {
		// 設置單行顯示所有輸入框內容
		setSingleLine();
		// 設置輸入框可獲得焦點
		setFocusable(true);
		setFocusableInTouchMode(true);
		TypedArray t = this.getResources().obtainAttributes(attrs, R.styleable.MyEditText);
		isCardNumber = t.getBoolean(R.styleable.MyEditText_isCardNumber, isCardNumber);
		splitNumber = t.getInt(R.styleable.MyEditText_splitNumber, splitNumber);
		t.recycle();
		mClearDrawable = this.getResources().getDrawable(R.drawable.clear);
		mClearDrawable.setBounds(0, 0, mClearDrawable.getIntrinsicWidth(), mClearDrawable.getIntrinsicHeight());
		initEvent();
	}

	// 輸入框內容改變後onTextChanged方法會調用多次,設置一個變量讓其每次改變之後只調用一次
	private boolean isTextChanged = false;

	/**
	 * 處理事件的方法
	 */
	private void initEvent() {
		addTextChangedListener(new TextWatcher() {
			@Override
			public void onTextChanged(CharSequence s, int start, int before, int count) {
				if (isTextChanged) {
					isTextChanged = false;
					return;
				}
				isTextChanged = true;
				// 處理輸入內容空格與位數以及光標位置的邏輯
				handleInputContent(s, before);
				// 處理清除圖標的顯示與隱藏邏輯
				 handleClearIcon();
			}

			@Override
			public void beforeTextChanged(CharSequence s, int start, int count, int after) {

			}

			@Override
			public void afterTextChanged(Editable s) {

			}
		});

	}

	// 卡號內容
	private String content;
	// 卡號最大長度,卡號一般最長21位
	public static final int MAX_CONTENT_LENGHT = 21;
	// 緩沖分隔後的新內容串
	private String result = "";

	/**
	 * 處理輸入內容空格與位數的邏輯
	 */
	private void handleInputContent(CharSequence s, int before) {
		if (isCardNumber) {
			// 如果isCardNumber=true,說明是銀行卡號輸入框,控制只能輸入數字,否則按原特性處理
			setInputType(InputType.TYPE_CLASS_NUMBER);
			content = s.toString();
			// 先緩存輸入框內容
			result = content;
			// 去掉空格,以防止用戶自己輸入空格
			content = content.replace(" ", "");
			// 限制輸入的數字位數最多21位(銀行卡號一般最多21位)
			if (content != null && content.length() <= MAX_CONTENT_LENGHT) {
				result = "";
				int i = 0;
				// 先把splitNumber倍的字符串進行分隔
				while (i + splitNumber < content.length()) {
					result += content.substring(i, i + splitNumber) + " ";
					i += splitNumber;
				}
				// 最後把不夠splitNumber倍的字符串加到末尾
				result += content.substring(i, content.length());
			} else {
				// 如果用戶輸入的位數
				result = result.substring(0, result.length() - 1);
			}
			// 獲取光標開始位置
			// 必須放在設置內容之前
			int j = getSelectionStart();
			setText(result);
			// 處理光標位置
			handleCursor(before, j);
		}

	}

	/**
	 * 處理光標位置
	 *
	 * @param before
	 * @param j
	 */
	private void handleCursor(int before, int j) {
		// 處理光標位置
		try {
			if (j + 1 < result.length()) {
				// 添加字符
				if (before == 0) {
					// 遇到空格,光標跳過空格,定位到空格後的位置
					if (j % splitNumber + 1 == 0) {
						setSelection(j + 1);
					} else {
						// 否則,光標定位到內容之後 (光標默認定位方式)
						setSelection(result.length());
					}
					// 回退清除一個字符
				} else if (before == 1) {
					// 回退到上一個位置(遇空格跳過)
					setSelection(j);
				}
			} else {
				MyEditText.this.setSelection(result.length());
			}
		} catch (Exception e) {

		}
	}

	/**
	 * 處理清除圖標的邏輯
	 *
	 * @param content
	 */
	private void handleClearIcon() {
		if (content != null && content.length() > 0) {
			// 顯示
			setEditTextIcon(null, null, mClearDrawable, null);
		} else {
			// 隱藏
			setEditTextIcon(null, null, null, null);
		}
	}

	@Override
	public boolean onTouchEvent(MotionEvent event) {
		// 獲取用戶點擊的坐標,這裡只對X軸做了判斷,
		float x = event.getX();
		// 當用戶抬起手指時,判斷坐標是否在圖標交互區域,如果在則清空輸入框內容,同時隱藏圖標自己
		if (event.getAction() == MotionEvent.ACTION_UP) {
			if (x > (getWidth() - getPaddingRight() - mClearDrawable.getIntrinsicWidth())) {
				// 清空輸入框內容
				setText("");
				// 隱藏圖標
				setEditTextIcon(null, null, null, null);
			}
		}
		return super.onTouchEvent(event);
	}

	@Override
	protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
		super.onFocusChanged(focused, direction, previouslyFocusedRect);
		//判斷當focused為true時,說明獲取了焦點,此時如果輸入框有內容,則顯示清空圖標,否則顯示清空圖標
		if (focused && (content != null && content.length() > 0)) {
			setEditTextIcon(null, null, mClearDrawable, null);
		} else {
			setEditTextIcon(null, null, null, null);
		}
		//刷新界面,防止有時候出現的不刷新界面情況
		invalidate();
	}

	/**
	 * 設置輸入框的左,上,右,下圖標
	 *
	 * @param left
	 * @param top
	 * @param right
	 * @param bottom
	 */
	private void setEditTextIcon(Drawable left, Drawable top, Drawable right, Drawable bottom) {

		setCompoundDrawables(left, top, right, bottom);
	}

}
如果考慮重構代碼的話,顯然handleClearIcon()方法中的邏輯和onFocusChanged()方法中的邏輯很像,如果給handleClearIcon方法的邏輯考慮上焦點情況,那麼handleClearIcon()方法有可以通用了。示例代碼:
	/**
	 * 處理清除圖標的邏輯
	 *
	 * @param content
	 */
	private void handleClearIcon(boolean focused) {
		if (content != null && content.length() > 0) {
			// 顯示
			if (focused) {
				setEditTextIcon(null, null, mClearDrawable, null);
			} else {
				// 隱藏
				setEditTextIcon(null, null, null, null);
			}
		} else {
			// 隱藏
			setEditTextIcon(null, null, null, null);
		}
	}
這樣在onFocusChanged()方法中只需要用戶handleClearIcon()方法,並傳遞focused即可:
@Override
	protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
		super.onFocusChanged(focused, direction, previouslyFocusedRect);
		// 判斷當focused為true時,說明獲取了焦點,此時如果輸入框有內容,則顯示清空圖標,否則顯示清空圖標
		handleClearIcon(focused);
		// 刷新界面,防止有時候出現的不刷新界面情況
		invalidate();
	}
最後因為在onTextChanged()方法中也調用過handleClearIcon(),而onTextChanged()方法中,輸入框肯定有焦點,所以給原來的方法調用上傳true即可:
	public void onTextChanged(CharSequence s, int start, int before, int count) {
				if (isTextChanged) {
					isTextChanged = false;
					return;
				}
				isTextChanged = true;
				// 處理輸入內容空格與位數以及光標位置的邏輯
				handleInputContent(s, before);
				// 處理清除圖標的邏輯
				handleClearIcon(true);
			}
最終效果與案例開始一樣: \
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved