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

案例效果:

实例分析:

在开发银行相关客户端的时候或者开发在线支付相关客户端的时候经常要求用户绑定银行卡,其中银行卡号一般需要空格分隔显示,最常见的就是每4位数以空格进行分隔,以方便用户实时比对自己输入的卡号是否正确。当产品经理或UI设计师把这样的需求拿给我们的时候,我们的大脑会马上告诉我们Android中有个EditText控件可以用来输入卡号,但好像没见过可以分隔显示的属性或方法啊。当我们睁大眼睛对着效果图正发呆的时候,突然发现当用户输入内容的时候还出现了清除图标,点击清空图标还可以清空用户输入的内容。

本案例将带大家解决银行卡号分隔显示与添加清空图标这两个问题,采用的解决方式依然是自定义控件,并且扩展于EditText控件。

银行卡号分隔逻辑分析:

因为要在用户输入内容过程中实时的让用户看到每4位(可配置N位)以空格分隔显示,并且有空格时光标会跳过空格定位到下一个输入位,所以就需要对输入框内容进行实时监听,要实现这一点,我们可以使用EditText控件中的addTextChangedListener监听器,并注册TextWatcher回调接口,然后在回调方法onTextChanged()中可以实时获取用户输入的全部内容。获得内容后我们就可以遍历这个内容串,然后每取4位(或N位)就在后面加一个空格,直到最后把剩下的不够4位(或N位)的内容拼接到最后,这样就得到了一个我们期望用户看到的新的字符串,把这个新的字符串重新设置给输入框就可以解决银行卡号分隔显示问题了。但是当我们运行后发现内容是分隔显示了,光标却很不正常,原因是当我们通过代码的方式为输入框设置内容后EditText认为是你接管了它,所以光标也一并交由我们去管理,所以我们需要根据显示的内容定位光标到合适的位置,光标定位可以用EditText的setSelection(int
index),除此之外需要考虑当添加一位新数或回退删除一位当前数时对光标定位带来的影响。当然要设计一个良好的自定义控件,我们需要考虑更细节的问题,比如这个输入框如果想通用的话我们需要控制它显示的内容类型,当作为银行号卡输入框时应该控制只能输入数字,否则作为普通输入框,让用户可以输入任何原EditText支持的内容类型。并且一般银行卡号最长21位数(可配置N位),多于21位(或N位)就不让用户再输入了,这样做的目的是为了减少服务端对无效卡号的校验时间,从而提高响应客户端的速度(性能优化必考虑的问题),至此才算基本完成了银行卡号显示的逻辑思考。来张图理一下思路:

清空功能逻辑分析:

清空功能逻辑相对要简单一些,但也有一些值得我们思考的地方,比如清空图标在输入框内侧右边,换句话说就是清空图标首先是在输入里,做为输入框的一部分,然后是在右边。对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()方法,处理焦点改变时,清空图标显示与隐藏逻辑。

具体实现:

  1. 银行卡号显示功能实现
  • 自定义一个输入框控件,继承于EdiText

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);
	}

}

  • 使用自定义输入框对案例布局进行布局

这部分暂时没什么特殊的,平时怎么布局,现在在这里就怎么布局,当然因为使用了自定义控件,所以需要加包全名。

<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>

此时的布局效果:

效果与案例一样,但逻辑还没有加,目前只是个架子

  • 配置自定义控件为普通输入框控件或银行卡号输入框控件

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

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

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

</resources></span>

b)自定义控件中定义与自定义属性对应的成员变量

<span style="font-size:14px;">//自定义输入框的模式 当值为true:银行卡号输入框模式,false:普通输入框模式
	private boolean isCardNumber = true;</span>

c)定义初始化方法,获取自定义属性并为对应的成员变量赋值

核心代码:

<span style="font-size:14px;">/**
	 * 初始化方法
	 */
	private void init(AttributeSet attrs) {
		TypedArray t = this.getResources().obtainAttributes(attrs, R.styleable.MyEditText);
		isCardNumber = t.getBoolean(R.styleable.MyEditText_isCardNumber, isCardNumber);
		t.recycle();
	}</span>

完整代码:

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();
	}
}

  • 初始化输入框为单行显示并可获得焦点

	/**
	 * 初始化方法
	 */
	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();
	}

  • 配置自定义控件在银行卡号模式下分隔位数

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)在自定义控件中定义与自定义属性对应的成员变量

<span style="font-size:14px;">	// 每隔多少位以空格进行分隔一次,卡号一般都是每4位以空格分隔一次
	public int splitNumber = 4;</span>

c)在初始化方法,获取自定义属性并为对应的成员变量赋值

<span style="font-size:14px;">	/**
	 * 初始化方法
	 */
	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();
	}</span>

  • 实现银行卡号显示功能

a)注册输入框内容改变监听器和数据回调接口

    
    定义一个专门处理事件的方法initEvent(),将注册事件逻辑放到这个方法,然后在初始化init()方法中调用

<pre name="code" class="java"><span style="font-size:14px;font-weight: normal;">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()方法,在初始化的时候完成对输入框内容改变的监听
	}</span>

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

<span style="font-size:14px;">// 输入框内容改变后onTextChanged方法会调用多次,设置一个变量让其每次改变之后只调用一次
private boolean isTextChanged = false;</span>
逻辑控制是在onTextChange()回调方法中

	<span style="font-size:14px;font-weight: normal;">/**
	 * 处理事件的方法
	 */
	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) {

			}
		});

	}</span>

c)分隔输入内容,并显示分隔后的内容

设计一个方法,专门处理此逻辑,并在onTextChanged()方法中调用,注意定义的成员变量和传递的参数。

<span style="font-size:14px;">// 卡号内容
	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)中在下面步骤中实现
}

	}</span>

在输入框内容改变监回调方法中用户分隔内容的处理方法:

<span style="font-size:14px;">/**
	 * 处理事件的方法
	 */
	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) {

			}
		});

	}</span>

d)处理光标位置逻辑

	<span style="font-size:14px;">/**
	 * 处理光标位置
	 *
	 * @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) {

		}
}</span>

什么时候调用上面的处理光标定位的方法呢?

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

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

<span style="font-size:14px;"><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></span>

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

<span style="font-size:14px;">  <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></span>

设置app:isCardNumber= "true",则自定义输入框为银行卡号输入框,如果还指定 app:splitNumber = "4" ,设置每4位数用空格分隔:

<span style="font-size:14px;">       <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></span>

完整布局:

<span style="font-size:14px;"><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></span>

实现效果:

到此银行卡分隔显示相关功能就实现完成了,此时如果我们发现输入的卡号全错了想重新输入,如果没有清空功能,多显不便,

所以接下来就是在前面功能的基础上为自定义输入框添加清空输入内容功能。

2、清空图标功能实现:

  • 准备清空图标,并在自定义输入框中设计显示清空图标的方法

a)导入清空图标到drawable目录

clear.png

b)在自定义输入框中定义一个Drawable类型的成员变量,

c)在初始化方法中为成员变量赋值,并为mClearDrawable设置一个交互区域

<span style="font-size:14px;">	// 内容清除图标
	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();
	}</span>

d)设计显示和控制内侧图标的方法,以供其它地方控件图标的显示和隐藏

<span style="font-size:14px;">/**
	 * 设置输入框的左,上,右,下图标
	 *
	 * @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);
	 }
	 }</span>
  • 重写onTouchEvent()方法,处理点击清空图标逻辑
<span style="font-size:14px;">	@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);
	}</span>
  • 重写onFocusChanged()方法,处理焦点改变时,清空图标显示与隐藏逻辑
<span style="font-size:14px;">	@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();
	}</span>

到此实现了案例中的所有功能和逻辑,完整自定义控件代码:

<span style="font-size:14px;">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);
	}

}</span>

如果考虑重构代码的话,显然handleClearIcon()方法中的逻辑和onFocusChanged()方法中的逻辑很像,如果给handleClearIcon方法

的逻辑考虑上焦点情况,那么handleClearIcon()方法有可以通用了。示例代码:

	<span style="font-size:14px;">/**
	 * 处理清除图标的逻辑
	 *
	 * @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);
		}
	}</span>

这样在onFocusChanged()方法中只需要用户handleClearIcon()方法,并传递focused即可:

<span style="font-size:14px;">	@Override
	protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
		super.onFocusChanged(focused, direction, previouslyFocusedRect);
		// 判断当focused为true时,说明获取了焦点,此时如果输入框有内容,则显示清空图标,否则显示清空图标
		handleClearIcon(focused);
		// 刷新界面,防止有时候出现的不刷新界面情况
		invalidate();
	}</span>

最后因为在onTextChanged()方法中也调用过handleClearIcon(),而onTextChanged()方法中,输入框肯定有焦点,所以给原来的方法调用上传true即可:

<span style="font-size:14px;">	public void onTextChanged(CharSequence s, int start, int before, int count) {
				if (isTextChanged) {
					isTextChanged = false;
					return;
				}
				isTextChanged = true;
				// 处理输入内容空格与位数以及光标位置的逻辑
				handleInputContent(s, before);
				// 处理清除图标的逻辑
				handleClearIcon(true);
			}</span>

最终效果与案例开始一样:

时间: 2024-11-05 02:28:08

Android自定义控件系列案例【五】的相关文章

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

案例效果: 模拟器上运行有些锯齿,真机上和预期一样好 案例分析: 看效果,第一直觉肯定是Android原生态控件中没有这样的控件实现这种效果,自然想到应该需要自定义控件了,没错,这就是通过自定义控件来绘制的一个圆环进度条.仔细分析发现这个效果的进度条应该由几个部分组成,首先是无进度时的浅色圆环,然后是一个随进度变化的深色圆弧,而中间部分是一个深蓝色的实心圆,最后就是显示进度百分比的文字.这几部分大部分都是图形,所以使用图形绘制技术应该可以绘制出分部分效果,然后加上进度控制部分应该心里就有底了.

[转]Android自定义控件系列五:自定义绚丽水波纹效果

出处:http://www.2cto.com/kf/201411/353169.html 今天我们来利用Android自定义控件实现一个比较有趣的效果:滑动水波纹.先来看看最终效果图: 图一 效果还是很炫的:饭要一口口吃,路要一步步走,这里我们将整个过程分成几步来实现 一.实现单击出现水波纹单圈效果: 图二 照例来说,还是一个自定义控件,这里我们直接让这个控件撑满整个屏幕(对自定义控件不熟悉的可以参看我之前的一篇文章:Android自定义控件系列二:自定义开关按钮(一)).观察这个效果,发现应该

Android自定义控件系列五:自定义绚丽水波纹效果

尊重原创!转载请注明出处:http://blog.csdn.net/cyp331203/article/details/41114551 今天我们来利用Android自定义控件实现一个比较有趣的效果:滑动水波纹.先来看看最终效果图: 图一 效果还是很炫的:饭要一口口吃,路要一步步走,这里我们将整个过程分成几步来实现 一.实现单击出现水波纹单圈效果: 图二 照例来说,还是一个自定义控件,这里我们直接让这个控件撑满整个屏幕(对自定义控件不熟悉的可以参看我之前的一篇文章:Android自定义控件系列二

Android自定义控件系列 十:利用添加自定义布局来搞定触摸事件的分发,解决组合界面中特定控件响应特定方向的事件

这个例子是比较有用的,基本上可以说,写完这一次,以后很多情况下,直接拿过来addView一下,然后再addInterceptorView一下,就可以轻轻松松的达到组合界面中特定控件来响应特定方向的触摸事件了. 请尊重原创劳动成果,转载请注明出处:http://blog.csdn.net/cyp331203/article/details/45198549,非允许请勿用于商业或盈利用途,违者必究. 在写Android应用的过程之中,经常会遇到这样的情况:界面包含了多个控件,我们希望触摸在界面上的不

Android自定义控件系列二:如何自定义属性

上一篇Android自定义控件系列一:如何测量控件尺寸 我们讲了如何确定控件的属性,这篇接着也是讲个必要的知识-如何自定义属性.对于一个完整的或者说真正有实用价值的控件,自定义属性是必不可少的. 如何为控件定义属性 在res/values/attrs.xml(attrs.xml如果不存在,可以创建个)中使用<declare-styleable>标签定义属性,比如我想定义个显示头像的圆形的图片控件(AvatarImageView): 01.<?xml version="1.0&q

Android自定义控件系列七:详解onMeasure()方法中如何测量一个控件尺寸(一)

转载请注明出处:http://blog.csdn.net/cyp331203/article/details/45027641 自定义view/viewgroup要重写的几个方法:onMeasure(),onLayout(),onDraw().(不熟悉的话可以查看专栏的前几篇文章:Android自定义控件系列二:自定义开关按钮(一)). 今天的任务就是详细研究一下protected void onMeasure(int widthMeasureSpec, int heightMeasureSpe

android自定义控件系列教程----视图的测量和布局

前面说点什么 当我们的一个视图界面绘制在android屏幕上面的时候其实都必须经过这几步measure. layout.draw这几个阶段,我们可以在view类里面看到这几个函数,然后里面有几个函数是onmeasure.onlayout.ondraw这几个函数是我们重写控件需要注意的这几个函数,下面我们就来讲讲这几个函数的功能和作用. onMeasure 正如这个函数的名子一样就是测量,所有的图示其实系统在绘制之前都不知道它到底有多大的,所以在很多时候我们在初始化界面oncreate的时候直接去

Android自定义控件系列三:自定义开关按钮(三)--- 自定义属性

尊重原创,转载请注明出处:http://blog.csdn.net/cyp331203/article/details/40855377 接之前的:Android自定义控件系列二:自定义开关按钮(一)和Android自定义控件系列三:自定义开关按钮(二)继续,今天要讲的就是如何在自定义控件中使用自定义属性,实际上这里有两种方法,一种是配合XML属性资源文件的方式,另一种是不需要XML资源文件的方式:下面我们分别来看看: 一.配合XML属性资源文件来使用自定义属性: 那么还是针对我们之前写的自定义

Android自定义控件系列之应用篇——圆形进度条

一.概述 在上一篇博文中,我们给大家介绍了Android自定义控件系列的基础篇.链接:http://www.cnblogs.com/jerehedu/p/4360066.html 这一篇博文中,我们将在基础篇的基础上,再通过重写ondraw()方法和自定义属性实现圆形进度条,效果如图所示: 二.实现步骤   1.  编写自定义组件MyCircleProgress扩展View public class MyCircleProgress extends View { - } 2.  在MyCircl