Android自定义控件:通用验证码输入框

关于自定义控件的基础知识
-- 可自行查询资料。
-- 也可关注我们,后期介绍自定义统计图表时,会统一介绍自定义控件的常用知识。


需求

4位验证码输入框:
效果图:


4位验证码输入框.gif
1. 输入框一行可输入4位数字类型的验证码;
2. 4位数字之间有间隔(包括底线);
3. 输入框不允许有光标;
4. 底线根据输入位置显示高亮(蓝色);
6. 输入完成,回调结果,输入过程中,也进行回调;

分析

这种效果,很难直接在Edittext上处理:
-- 输入框均分4等份,还要有间隔;
-- 更难处理的是Edittext输入框禁止光标,那么,没有光标,我们如何调起虚拟键盘输入数据?
-- 等...

与其在一个控件上折腾,这么难受,不如自定义一个控件,实现这种效果。
自定义控件最简单的方案:使用多个控件,组合出这种效果。

  1. 布局如何实现?
1.禁止光标,我们直接使用TextView就解决了,而非Edittext;
2.一行显示4位数字,比较简单,可以使用线性布局的权重,对TextView进行控制为4等分;
3.每个TextView下面跟着一个底线,将来我们就能对底线设置高亮颜色了;

这样,基本的布局展示就可以了!!!

  1. 使用了TextView,那么我们如何接收用户的输入呢?
也很简单,我们在4个TextView的上方平铺一个EditText,设置透明,
当用户点击到该控件时,会自动调起软键盘,接收输入的文本。
  1. EditText接收到用户输入的文本,如何显示在TextView呢?
我们监听EditText文本输入事件,最多仅接收4个输入字符,
每接收到一个字符,我们就赋值给对应的TextView;
底线也随要设置的文本切换显示高亮;
  1. 如何删除已输入的数值?
我们监听EditText按键事件,拦截DEL键,从后向前挨着删除字符即可;
底线也随要删除的文本切换显示高亮;
  1. 是否需要自定义属性
分析我们自己的项目,虽然是公用的控件,但是该控件比较简单,没有特别的要求,所以没必要自定义属性了!
如果大家有需要的,可根据需要自己定义;
如何定义属性?请自行查找资料;

既然,问题都分析清楚了,那我们就开始快速实现吧


具体实现

  1. 布局文件 phone_code.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    <LinearLayout
        android:id="@+id/ll_code"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">
        <LinearLayout
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:orientation="vertical"
            android:layout_marginRight="7dp">
            <TextView
                android:id="@+id/tv_code1"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:textColor="#2D2D2D"
                android:textSize="40sp"
                android:background="@null"
                android:gravity="center"/>
            <View
                android:id="@+id/v1"
                android:layout_width="match_parent"
                android:layout_height="1dp"
                android:background="#3F8EED" />
        </LinearLayout>

        <LinearLayout
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:orientation="vertical"
            android:layout_marginRight="7dp"
            android:layout_marginLeft="7dp">
            <TextView
                android:id="@+id/tv_code2"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:textColor="#2D2D2D"
                android:textSize="40sp"
                android:background="@null"
                android:gravity="center"/>
            <View
                android:id="@+id/v2"
                android:layout_width="match_parent"
                android:layout_height="1dp"
                android:background="#999999" />
        </LinearLayout>
        <LinearLayout
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:orientation="vertical"
            android:layout_marginRight="7dp"
            android:layout_marginLeft="7dp">
            <TextView
                android:id="@+id/tv_code3"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:textColor="#2D2D2D"
                android:textSize="40sp"
                android:background="@null"
                android:gravity="center"/>
            <View
                android:id="@+id/v3"
                android:layout_width="match_parent"
                android:layout_height="1dp"
                android:background="#999999" />
        </LinearLayout>
        <LinearLayout
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:orientation="vertical"
            android:layout_marginLeft="7dp">
            <TextView
                android:id="@+id/tv_code4"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:textColor="#2D2D2D"
                android:background="@null"
                android:textSize="40sp"
                android:gravity="center"/>
            <View
                android:id="@+id/v4"
                android:layout_width="match_parent"
                android:layout_height="1dp"
                android:background="#999999" />
        </LinearLayout>
    </LinearLayout>

    <EditText
        android:id="@+id/et_code"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignTop="@+id/ll_code"
        android:layout_alignBottom="@+id/ll_code"
        android:background="@android:color/transparent"
        android:textColor="@android:color/transparent"
        android:cursorVisible="false"
        android:inputType="number"/>
</RelativeLayout>

et_code 输入框,设置了透明和无光标,仅接收数字;
tv_code1~4 为显示数字的控件;
v1~4 为数字文本的底线,用于设置高亮;

  1. 自定义控件代码 PhoneCode
package iwangzhe.customview2.phonecode;

import android.content.Context;
import android.graphics.Color;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.RelativeLayout;
import android.widget.TextView;

import java.util.ArrayList;
import java.util.List;

import iwangzhe.customview2.R;

/**
 * 类:PhoneCode
 * 作者: qxc
 * 日期:2018/3/14.
 */
public class PhoneCode extends RelativeLayout {
    private Context context;
    private TextView tv_code1;
    private TextView tv_code2;
    private TextView tv_code3;
    private TextView tv_code4;
    private View v1;
    private View v2;
    private View v3;
    private View v4;
    private EditText et_code;
    private List<String> codes = new ArrayList<>();
    private InputMethodManager imm;

    public PhoneCode(Context context) {
        super(context);
        this.context = context;
        loadView();
    }

    public PhoneCode(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.context = context;
        loadView();
    }

    private void loadView(){
        imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
        View view = LayoutInflater.from(context).inflate(R.layout.phone_code, this);
        initView(view);
        initEvent();
    }

    private void initView(View view){
        tv_code1 = (TextView) view.findViewById(R.id.tv_code1);
        tv_code2 = (TextView) view.findViewById(R.id.tv_code2);
        tv_code3 = (TextView) view.findViewById(R.id.tv_code3);
        tv_code4 = (TextView) view.findViewById(R.id.tv_code4);
        et_code = (EditText) view.findViewById(R.id.et_code);
        v1 = view.findViewById(R.id.v1);
        v2 = view.findViewById(R.id.v2);
        v3 = view.findViewById(R.id.v3);
        v4 = view.findViewById(R.id.v4);
    }

    private void initEvent(){
        //验证码输入
        et_code.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
            }
            @Override
            public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
            }
            @Override
            public void afterTextChanged(Editable editable) {
                if(editable != null && editable.length()>0) {
                    et_code.setText("");
                    if(codes.size() < 4){
                        codes.add(editable.toString());
                        showCode();
                    }
                }
            }
        });
        // 监听验证码删除按键
        et_code.setOnKeyListener(new View.OnKeyListener() {
            @Override
            public boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
                if (keyCode == KeyEvent.KEYCODE_DEL && keyEvent.getAction() == KeyEvent.ACTION_DOWN && codes.size()>0) {
                    codes.remove(codes.size()-1);
                    showCode();
                    return true;
                }
                return false;
            }
        });
    }

    /**
     * 显示输入的验证码
     */
    private void showCode(){
        String code1 = "";
        String code2 = "";
        String code3 = "";
        String code4 = "";
        if(codes.size()>=1){
            code1 = codes.get(0);
        }
        if(codes.size()>=2){
            code2 = codes.get(1);
        }
        if(codes.size()>=3){
            code3 = codes.get(2);
        }
        if(codes.size()>=4){
            code4 = codes.get(3);
        }
        tv_code1.setText(code1);
        tv_code2.setText(code2);
        tv_code3.setText(code3);
        tv_code4.setText(code4);        
        
        setColor();//设置高亮颜色
        callBack();//回调
    }

    /**
     * 设置高亮颜色
     */
    private void setColor(){
        int color_default = Color.parseColor("#999999");
        int color_focus = Color.parseColor("#3F8EED");
        v1.setBackgroundColor(color_default);
        v2.setBackgroundColor(color_default);
        v3.setBackgroundColor(color_default);
        v4.setBackgroundColor(color_default);
        if(codes.size()==0){
            v1.setBackgroundColor(color_focus);
        }
        if(codes.size()==1){
            v2.setBackgroundColor(color_focus);
        }
        if(codes.size()==2){
            v3.setBackgroundColor(color_focus);
        }
        if(codes.size()>=3){
            v4.setBackgroundColor(color_focus);
        }
    }

    /**
     * 回调
     */
    private void callBack(){
        if(onInputListener==null){
            return;
        }
        if(codes.size()==4){
            onInputListener.onSucess(getPhoneCode());
        }else{
            onInputListener.onInput();
        }
    }

    //定义回调
    public interface OnInputListener{
        void onSucess(String code);
        void onInput();
    }
    private OnInputListener onInputListener;
    public void setOnInputListener(OnInputListener onInputListener){
        this.onInputListener = onInputListener;
    }

    /**
     * 显示键盘
     */
    public void showSoftInput(){
        //显示软键盘
        if(imm!=null && et_code!=null) {
            et_code.postDelayed(new Runnable() {
                @Override
                public void run() {
                    imm.showSoftInput(et_code, 0);
                }
            },200);
        }
    }

    /**
     * 获得手机号验证码
     * @return 验证码
     */
    public String getPhoneCode(){
        StringBuilder sb = new StringBuilder();
        for (String code : codes) {
            sb.append(code);
        }
        return sb.toString();
    }
}

codes 集合,用于存放用户输入的所有数字。使用该集合,可简化输入框、文本关联逻辑和事件之间处理;
showSoftInput方法:显示输入键盘,可被外界调用;
getPhoneCode方法:获得用户输入的验证码,可被外界调用;
OnInputListener接口:定义的数值输入回调,用于告诉调用者是输入中,还是输入完成;
(OnInputListener用途举例:在实际项目中,当输入完成,底部【下一步】或【确定】按钮变成可点击,否则,变为不可点击。)

  1. 调用者 MainActivity
    布局文件
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="iwangzhe.customview2.MainActivity">
    <iwangzhe.customview2.phonecode.PhoneCode
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/pc_1"
        android:layout_below="@+id/fpc_1"
        android:layout_marginTop="40dp"
        android:layout_marginLeft="20dp"
        android:layout_marginRight="20dp"/>
</RelativeLayout>

代码

package iwangzhe.customview2;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import iwangzhe.customview2.phonecode.PhoneCode;
public class MainActivity extends AppCompatActivity {
    PhoneCode pc_1;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        pc_1 = (PhoneCode) findViewById(R.id.pc_1);
        //注册事件回调(根据实际需要,可写,可不写)
        pc_1.setOnInputListener(new PhoneCode.OnInputListener() {
            @Override
            public void onSucess(String code) {
                //TODO: 例如底部【下一步】按钮可点击
            }

            @Override
            public void onInput() {
                //TODO:例如底部【下一步】按钮不可点击
            }
        });
    }

    private void test(){
        //获得验证码
        String phoneCode = pc_1.getPhoneCode();
        //......
        //......
        //更多操作
    }
}


总结:
此控件实现起来,很简单,代码量也非常少。
本文章,主要是为了让大家了解自定义控件的过程,如果想在自己的项目中使用,请根据需要自行调整优化。

Demo下载地址:
(为了减小Demo大小,我删除了build下的文件,大家获取后rebuild一下代码,就可以了)
https://pan.baidu.com/s/1OAcUdmwC_wFOrkontveAeA

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容