自定义view

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

2017-06-22  本文已影响1073人  闹气孩子

这是一个仿照探探的验证码输入控件,防止输入溢出,支持自身属性自定义等.
该项目以上传至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 特性

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

上一篇下一篇

猜你喜欢

热点阅读