首页 > 代码库 > Android自己定义控件系列案例【五】

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

案例效果:

技术分享技术分享

案例分析:

在开发银行相关client的时候或者开发在线支付相关client的时候常常要求用户绑定银行卡,当中银行卡号一般须要空格分隔显示。最常见的就是每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位)就不让用户再输入了,这样做的目的是为了降低服务端对无效卡号的校验时间,从而提高响应client的速度(性能优化必考虑的问题),至此才算基本完毕了银行卡号显示的逻辑思考。来张图理一下思路:

技术分享

(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:使用自己定义输入框对案例布局进行布局

这部分临时没什么特殊的。平时怎么布局。如今在这里就怎么布局。当然由于使用了自己定义控件,所以须要加包全名。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#F0F0F0"
    android:orientation="vertical" >

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="5dp"
        android:layout_marginLeft="5dp"
        android:layout_marginRight="5dp"
        android:layout_marginTop="20dp"
        android:text="请绑定持卡人本人的银行卡" />

    <LinearLayout
        android:id="@+id/ll_name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginBottom="1dp"
        android:background="#ffffff"
        android:gravity="center_vertical"
        android:orientation="horizontal"
        android:paddingBottom="5dp"
        android:paddingLeft="10dp"
        android:paddingRight="10dp"
        android:paddingTop="5dp" >

        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="left"
            android:text="持卡人" />

        <com.kedi.myedittext.MyEditText
            android:id="@+id/et_name"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="5"
            android:hint="请输入姓名"
            android:padding="5dp" >
        </com.kedi.myedittext.MyEditText>
    </LinearLayout>

    <LinearLayout
        android:id="@+id/ll_card_number"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#ffffff"
        android:gravity="center_vertical"
        android:orientation="horizontal"
        android:paddingBottom="5dp"
        android:paddingLeft="10dp"
        android:paddingRight="10dp"
        android:paddingTop="5dp" >

        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="left"
            android:text="卡号" />

        <com.kedi.myedittext.MyEditText
            android:id="@+id/et_number"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="5"
            android:hint="请输入银行卡号"
            android:padding="5dp" >
        </com.kedi.myedittext.MyEditText>
    </LinearLayout>

</LinearLayout>
此时的布局效果:

技术分享
效果与案例一样。但逻辑还没有加。眼下仅仅是个架子
step3:配置自己定义控件为普通输入框或银行卡号输入框

a)创建自己定义属性XML文件values/attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <declare-styleable name="MyEditText">
        <!-- 设置自己定义输入框的模式 true:银行卡号输入框模式。false:普通输入框模式 -->
        <attr name="isCardNumber" format="boolean" />
    </declare-styleable>

</resources>

 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

<span style="font-size:14px;"><?xml version="1.0" encoding="utf-8"?>
<resources>

    <declare-styleable name="MyEditText">

        <!-- 设置自己定义输入框的模式 当值为true:银行卡号输入框模式。false:普通输入框模式 -->
        <attr name="isCardNumber" format="boolean" />
        <!-- 配置自己定义控件在银行卡号模式下分隔位数 ,默觉得4位 -->
        <attr name="splitNumber" format="integer" />

    </declare-styleable>

</resources></span>

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布局中使用自己定义属性配置持卡人输入框为普通输入框,卡号为银行卡号输入框。

首先在布局根容器中加入一个自己定义属性的命名空间:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#F0F0F0"
    android:orientation="vertical" >


</LinearLayout>

然后在相应的自己定义控件上使用自己定义属性,并为其指定属性值:比方不设置app:isCardNumber属性代表自己定义输入框为普通输入框:

<com.kedi.myedittext.MyEditText
            android:id="@+id/et_name"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="5"
            android:hint="请输入姓名"
            android:padding="5dp" 
            >
 </com.kedi.myedittext.MyEditText>
设置app:isCardNumber= "true"。则自己定义输入框为银行卡号输入框,假设还指定 app:splitNumber = "4" 。设置每4位数用空格分隔:

 <com.kedi.myedittext.MyEditText
            android:id="@+id/et_number"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="5"
            android:hint="请输入银行卡号"
            android:padding="5dp" 
            app:isCardNumber= "true"
            app:splitNumber = "4">
</com.kedi.myedittext.MyEditText>

完整布局:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#F0F0F0"
    android:orientation="vertical" >

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="5dp"
        android:layout_marginLeft="5dp"
        android:layout_marginRight="5dp"
        android:layout_marginTop="20dp"
        android:text="请绑定持卡人本人的银行卡" />

    <LinearLayout
        android:id="@+id/ll_name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginBottom="1dp"
        android:background="#ffffff"
        android:gravity="center_vertical"
        android:orientation="horizontal"
        android:paddingBottom="5dp"
        android:paddingLeft="10dp"
        android:paddingRight="10dp"
        android:paddingTop="5dp" >

        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="left"
            android:text="持卡人" />

        <com.kedi.myedittext.MyEditText
            android:id="@+id/et_name"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="5"
            android:hint="请输入姓名"
            android:padding="5dp" 
           >
        </com.kedi.myedittext.MyEditText>
    </LinearLayout>

    <LinearLayout
        android:id="@+id/ll_card_number"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#ffffff"
        android:gravity="center_vertical"
        android:orientation="horizontal"
        android:paddingBottom="5dp"
        android:paddingLeft="10dp"
        android:paddingRight="10dp"
        android:paddingTop="5dp" >

        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="left"
            android:text="卡号" />

        <com.kedi.myedittext.MyEditText
            android:id="@+id/et_number"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="5"
            android:hint="请输入银行卡号"
            android:padding="5dp" 
            app:isCardNumber= "true"
            app:splitNumber = "4">
        </com.kedi.myedittext.MyEditText>
    </LinearLayout>

</LinearLayout>

实现效果:

技术分享

到此银行卡分隔显示相关功能就实现完毕了,此时假设我们发现输入的卡号全错了想又一次输入。假设没有清空功能,多显不便。所以接下来就是在前面功能的基础上为自己定义输入框加入清空输入内容功能。


清空图标功能实现

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);
			}
终于效果与案例開始一样:
技术分享









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