自定义View(一)绘制简单图形(验证码输入框[仿探探])

这是一个仿照探探的验证码输入控件,防止输入溢出,支持自身属性自定义等.
该项目以上传至Github中,欢迎star/fork,项目地址VercideEditText

一、 项目说明

1.1 demo演示

![demo.gif](http://upload-images.jianshu.io/upload_images/2493724-d816fafc6b3d3a8d.gif?imageMogr2/auto-orient/strip =100x100)

1.2 特性

  • [x] 继承EditText,可以当作EditText使用
  • [x] 防止输入溢出
  • [x] 自定义验证码位数
  • [x] 提供输入内容的监听器
  • [x] 高度自适配
  • [x] 属性自定义配置

1.3 集成

JCenter方式

第一步,添加至工程的build.gradle文件中

repositories {
     jcenter()
}

第二步,添加至module的build.gradle文件中

compile 'com.justkiddingbaby:vercodeedittext:1.0.0'

1.4 属性说明

属性 介绍 取值
figures 验证码位数 integer
verCodeMargin 每个验证码的间隔 dimension
bottomLineSelectedColor 底线选择状态下的颜色 reference
bottomLineNormalColor 底线未选中状态下的颜色 reference
bottomLineHeight 底线高度 dimension
selectedBackgroundColor 选中的背景颜色 reference

1.5 使用

在布局中使用

  <com.jkb.vcedittext.VerificationCodeEditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:inputType="number"
        android:text="123"
        android:textColor="@color/colorPrimary"
        android:textSize="40sp"
        app:bottomLineHeight="2dp"
        app:bottomLineNormalColor="@color/gravy_light"
        app:bottomLineSelectedColor="@color/colorAccent"
        app:figures="4"
        app:selectedBackgroundColor="@color/colorPrimary_alpha33"
        app:verCodeMargin="10dp" />

二、 代码分析

下面让我们来一步一步对该项目的源码进行分析.

2.1 需求分析

这是一个验证码输入控件,根据demo演示分析得出,需要支持的属性有:
1、作为EditText控件使用(继承EditText)
2、自定义验证码输入位数,并且支持每位验证码的margin自定义
3、有背景的选择变化和底部下划线的输入变化,并且支持颜色自定义
4、高度自适配,wrap_content时高度和每位验证码宽度一样
5、支持输入内容变化的监听

2.2 声明接口和属性

本节将对2.1中的需求进行分析,并定义出属于该控件独有的属性。

2.2.1 属性声明

在values目录下创建attrs.xml文件(如果无则创建),这是为了支持在xml中直接配置控件的相关属性。

   <declare-styleable name="VerCodeEditText">
        <!--验证码位数-->
        <attr name="figures" format="integer" />
        <!--验证码每位之间间隔-->
        <attr name="verCodeMargin" format="dimension" />
        <!--底线选中状态下的颜色-->
        <attr name="bottomLineSelectedColor" format="reference" />
        <!--底线未选中状态下的颜色-->
        <attr name="bottomLineNormalColor" format="reference" />
        <!--底线高度-->
        <attr name="bottomLineHeight" format="dimension" />
        <!--验证码选中状态下的背景颜色-->
        <attr name="selectedBackgroundColor" format="reference" />
    </declare-styleable>

2.2.2 接口声明

新建一个接口类VerificationAction,(这是为了支持使用Java代码配置控件的属性)。

interface VerificationAction {
    /**
     * 设置位数
     */
    void setFigures(int figures);
    /**
     * 设置验证码之间的间距
     */
    void setVerCodeMargin(int margin);
    /**
     * 设置底部选中状态的颜色
     */
    void setBottomSelectedColor(@ColorRes int bottomSelectedColor);
    /**
     * 设置底部未选中状态的颜色
     */
    void setBottomNormalColor(@ColorRes int bottomNormalColor);
    /**
     * 设置选择的背景色
     */
    void setSelectedBackgroundColor(@ColorRes int selectedBackground);
    /**
     * 设置底线的高度
     */
    void setBottomLineHeight(int bottomLineHeight);
    /**
     * 设置当验证码变化时候的监听器
     */
    void setOnVerificationCodeChangedListener(OnVerificationCodeChangedListener listener);
    /**
     * 验证码变化时候的监听事件
     */
    interface OnVerificationCodeChangedListener {
        /**
         * 当验证码变化的时候
         */
        void onVerCodeChanged(CharSequence s, int start, int before, int count);
        /**
         * 输入完毕后的回调
         */
        void onInputCompleted(CharSequence s);
    }
}

根据上述接口和属性的定义内容可以看出,接口的内容和完全和属性向对应,6个属性对应6个set方法,另外接口中单独定义了验证码输入内容变化的监听接口。

2.3 控件自定义

接下来我们就一步步地完成这个控件。

2.3.1 创建类,并声明相关属性并继承EditText和实现属性接口VerificationAction

public class VerificationCodeEditText extends android.support.v7.widget.AppCompatEditText implements VerificationAction{
    //attrs
    private int mFigures;//需要输入的位数
    private int mVerCodeMargin;//验证码之间的间距
    private int mBottomSelectedColor;//底部选中的颜色
    private int mBottomNormalColor;//未选中的颜色
    private float mBottomLineHeight;//底线的高度
    private int mSelectedBackgroundColor;//选中的背景颜色

    public VerificationCodeEditText(Context context) {
        this(context, null);
    }
    public VerificationCodeEditText(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
    public VerificationCodeEditText(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initAttrs(attrs);
    }
    /**
     * 初始化属性
     */
    private void initAttrs(AttributeSet attrs) {
        TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.VerCodeEditText);
        mFigures = ta.getInteger(R.styleable.VerCodeEditText_figures, 4);
        mVerCodeMargin = (int) ta.getDimension(R.styleable.VerCodeEditText_verCodeMargin, 0);
        mBottomSelectedColor = ta.getColor(R.styleable.VerCodeEditText_bottomLineSelectedColor,
                getCurrentTextColor());
        mBottomNormalColor = ta.getColor(R.styleable.VerCodeEditText_bottomLineNormalColor,
                getColor(android.R.color.darker_gray));
        mBottomLineHeight = ta.getDimension(R.styleable.VerCodeEditText_bottomLineHeight,
                dp2px(5));
        mSelectedBackgroundColor = ta.getColor(R.styleable.VerCodeEditText_selectedBackgroundColor,
                getColor(android.R.color.darker_gray));
        ta.recycle();
    }
    ...set方法
}

上述代码没啥好说的,只是定义了类的基础机构,关于View的三个构造方法的区别,可以参考文章 Android自定义View构造函数详解

2.3.2 测量宽高

测量View的宽高需要重写onMeasure(int,int)方法,因为本控件是继承EditText的,而EditText默认的测量和本需求不同,所以需要完全自己实现该方法,点开super.onMeasure(int,int)方法后,发现最终调用的方法是setMeasuredDimension(int, int),该方法的代码如下:

  @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthResult = 0, heightResult = 0;
        //最终的宽度
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        if (widthMode == MeasureSpec.EXACTLY) {
            widthResult = widthSize;
        } else {
            widthResult = getScreenWidth(getContext());//默认为屏幕的宽度
        }
        //每位验证码的宽度
        mEachRectLength = (widthResult - (mVerCodeMargin * (mFigures - 1))) / mFigures;
        //最终的高度
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        if (heightMode == MeasureSpec.EXACTLY) {
            heightResult = heightSize;
        } else {
            heightResult = mEachRectLength;
        }
        setMeasuredDimension(widthResult, heightResult);
    }

该控件的宽高测量比较简单,相信上述代码还是比较明了的,在此就不再做相关解释了。

2.3.3 绘制

从demo演示中可以看出,该控件并没有什么复杂的场景,只是计算矩形的宽度并绘制矩形、文字、下划线而已,绘制View需要重写onDraw(Canvas)方法。
在重写该方法之前,我们需要定义绘制时候的画笔并进行初始化,一共要绘制的内容有三个,矩形、文字、下划线,而矩形和下划线各自都有两种状态,而文字直接可以用Canvas来绘制,则需要的画笔一共有四个~

    private Paint mSelectedBackgroundPaint;
    private Paint mNormalBackgroundPaint;
    private Paint mBottomSelectedPaint;
    private Paint mBottomNormalPaint;

然后进行画笔的初始化操作,画笔的初始化可以在初始化属性之后调用。

    private void initPaint() {
        mSelectedBackgroundPaint = new Paint();
        mSelectedBackgroundPaint.setColor(mSelectedBackgroundColor);
        mNormalBackgroundPaint = new Paint();
        mNormalBackgroundPaint.setColor(getColor(android.R.color.transparent));

        mBottomSelectedPaint = new Paint();
        mBottomNormalPaint = new Paint();
        mBottomSelectedPaint.setColor(mBottomSelectedColor);
        mBottomNormalPaint.setColor(mBottomNormalColor);
        mBottomSelectedPaint.setStrokeWidth(mBottomLineHeight);
        mBottomNormalPaint.setStrokeWidth(mBottomLineHeight);
    }

接下来,我们需要根据验证码的位数绘制不同的矩形颜色和下划线颜色,定义一个全局变量mCurrentPosition 在绘制的时候使用,然后重写onDraw(Canvas)方法。

    @Override
    protected void onDraw(Canvas canvas) {
        mCurrentPosition = getText().length(); //获取验证码位数
      //支持padding属性
        int width = mEachRectLength - getPaddingLeft() - getPaddingRight();
        int height = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
        //循环绘制
        for (int i = 0; i < mFigures; i++) {
            canvas.save();
            int start = width * i + i * mVerCodeMargin;
            int end = width + start;
            //画一个矩形
            if (i == mCurrentPosition) {//选中的下一个状态
                canvas.drawRect(start, 0, end, height, mSelectedBackgroundPaint);
            } else {
                canvas.drawRect(start, 0, end, height, mNormalBackgroundPaint);
            }
            canvas.restore();
        }
        //绘制文字
        String value = getText().toString();
        for (int i = 0; i < value.length(); i++) {
            canvas.save();
            int start = width * i + i * mVerCodeMargin;
            float x = start + width / 2;
            TextPaint paint = getPaint();
            paint.setTextAlign(Paint.Align.CENTER);
            paint.setColor(getCurrentTextColor());
            Paint.FontMetrics fontMetrics = paint.getFontMetrics();
            float baseline = (height - fontMetrics.bottom + fontMetrics.top) / 2
                    - fontMetrics.top;
            canvas.drawText(String.valueOf(value.charAt(i)), x, baseline, paint);
            canvas.restore();
        }
        //绘制底线
        for (int i = 0; i < mFigures; i++) {
            canvas.save();
            float lineY = height - mBottomLineHeight / 2;
            int start = width * i + i * mVerCodeMargin;
            int end = width + start;
            if (i < mCurrentPosition) {
                canvas.drawLine(start, lineY, end, lineY, mBottomSelectedPaint);
            } else {
                canvas.drawLine(start, lineY, end, lineY, mBottomNormalPaint);
            }
            canvas.restore();
        }
    }

以上代码中绘制矩形和下划线并没有什么复杂的,主要是在绘制文字的时候需要注意文字的居中问题,关于该问题可以参考文章Android Canvas drawText实现中文垂直居中

2.3.4 防止输入溢出

在输入验证码之后,我们需要对其进行边界控制,默认会一直向后绘制,这样在超出验证码位数后点击删除则不会第一时间删除最后一位验证码,这也是为了逻辑的完整性,我们需要对输入内容进行监听,则需要调用方法addTextChangedListener(),该方法可以在构造方法中进行调用。

  @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
        mCurrentPosition = getText().length();
        postInvalidate();
    }
    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {
        mCurrentPosition = getText().length();
        postInvalidate();
        if (onCodeChangedListener != null) {
            onCodeChangedListener.onVerCodeChanged(getText(), start, before, count);
        }
    }
    @Override
    public void afterTextChanged(Editable s) {
        mCurrentPosition = getText().length();
        postInvalidate();
        if (getText().length() == mFigures) {
            if (onCodeChangedListener != null) {
                onCodeChangedListener.onInputCompleted(getText());
            }
        } else if (getText().length() > mFigures) {
            getText().delete(mFigures, getText().length());//防止输入溢出
        }
    }

以上代码在防止输入溢出的同时监听了验证码输入内容变化时候的监听。

以上是对该控件的代码分析过程,因为控件比较简单,没有进行详细的说明,要是有问题请留言,同时该项目已经上传至github中,项目地址VercideEditText

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,472评论 25 707
  • 刚看完几集 欢乐颂。 挺羡慕刘涛和奇点在见面之前的交流方式,不知道对方性别、年龄、通讯方式等一些富有主观因素的外在...
    盐小茴香阅读 208评论 0 0
  • 二十一二岁,阳光正好,三两好友,背起行囊,开始一段新的旅程。 理想是丰满的,现实是骨感的。我理想中的二十来岁的生活...
    小小小沐戈阅读 718评论 0 0
  • 人为什么会因胖而死 体脂训练营学习时看到过这样一段BBC的纪录片,内容是解剖一位心脏衰竭也就是心脏病去世的女士,...
    Liu刘美芳健康减脂阅读 419评论 0 0
  • 随手来一张阅读 325评论 0 0