手记

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:使用自定义输入框对案例布局进行布局
这部分暂时没什么特殊的,平时怎么布局,现在在这里就怎么布局,当然因为使用了自定义控件,所以需要加包全名。
<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
      <?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>
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);
    }
    最终效果与案例开始一样:
44人推荐
随时随地看视频
慕课网APP

热门评论

非常棒

不错

不如web方便

查看全部评论