实现一个UI审美的验证码输入框

抽空实现了一个验证码输入框的自定义View,效果图如下,其中每一个小圆圈都是一个selector,所以动态修改只需要改动drawable的内容即可实现修改,这样可发挥的内容也就很多。
WechatIMG4.jpeg
WechatIMG4.jpeg

分析

继承关系

首先是一个输入框,如果要手动实现一个输入框View未免显得太过麻烦,所以这里直接继承自AppCompatEditText实现。

public class VerifyCodeView extends AppCompatEditText{
      public VerifyCodeView(Context context) {
        this(context, null);
    }

    public VerifyCodeView(Context context, AttributeSet attrs) {
        this(context, attrs, android.R.attr.editTextStyle);
    }

    @SuppressLint("ResourceAsColor")
    public VerifyCodeView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.VerifyCodeView);
        verifyCodeLength = typedArray.getInteger(R.styleable.VerifyCodeView_verifyCodeLength, 4);
        strokeWidth = (int) typedArray.getDimension(R.styleable.VerifyCodeView_strokeWidth, ScreenUtil.dip2px(App.context, 50));
        strokeHeight = (int) typedArray.getDimension(R.styleable.VerifyCodeView_strokeHeight, ScreenUtil.dip2px(App.context, 50));
        strokePadding = (int) typedArray.getDimension(R.styleable.VerifyCodeView_strokePadding, ScreenUtil.dip2px(App.context, 30));
        strokeBackground = typedArray.getDrawable(R.styleable.VerifyCodeView_strokeBackground);
        typedArray.recycle();

        //设置数据长度
        if (verifyCodeLength >= 0) {
            setFilters(new InputFilter[]{new InputFilter.LengthFilter(verifyCodeLength)});
        } else {
            setFilters(new InputFilter[0]);
        }
        //禁止长按
        setLongClickable(false);
        //隐藏光标
        setCursorVisible(false);
        //背景透明
        setBackgroundColor(Color.TRANSPARENT);
    }
}

在最后一个构造方法中,获取到一些初始化属性的值,然后就是设置一些必要的UI显示,比如说限制输入最大长度为验证码长度(这边用的是InputFilter,感兴趣的自行了解下),禁止长按,隐藏光标,EditText背景透明(隐藏底部黑线),都是一些简单的配置隐藏官方给的View的UI。

测量

实现一个自定义View包含layout,measure和draw三个步骤,这里layout不需要,measure是需要实现的,其中

  • View最小高度不得小于边框高度,这里的边框高度就是小圆圈的直径,如果小圆圈换成正方形就是正方形的边长。
  • View的最小宽度不得小于(边框宽度 * 边框数)+ (边框间距 * 边框数减1),这个公式也很好理解,就是算四个格子加缝隙的宽度。这里在else里面做了一个简单的处理,就是让View整体居中显示,根据startPos的配置,可以从startPos位置开始画起。
 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = getMeasuredWidth();
        int height = getMeasuredHeight();
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        //最小高度不得低于边框高度
        if (height < strokeHeight) {
            height = strokeHeight;
        }

        //最小宽度不得小于(边框宽度 * 边框数)+ (边框间距 * 边框数减1)
        int editViewWidth = (strokeWidth * verifyCodeLength + strokePadding * (verifyCodeLength - 1));
        if (width < editViewWidth) {
            width = editViewWidth;
        } else {
            //左边起始
            startPos = width / 2 - editViewWidth / 2;
        }

        //重新生成spec
        widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, widthMode);
        heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, heightMode);

        //设置重新测量
        setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
 }
绘制

从图中验证码输入框部分可以看到,主要分为三部分的处理,一是绘制背景圈,二是绘制选中圈,三是绘制文字所以我在onDraw()方法里绘制了这几部分

   @Override
    protected void onDraw(Canvas canvas) {
        //获取字体背景色
        textColor = getCurrentTextColor();
        //设置透明字体防止原来的输入内容显示
        setTextColor(Color.TRANSPARENT);
        super.onDraw(canvas);
        //等原来的绘制完再设置自定义的字体颜色
        setTextColor(textColor);
        //绘制背景
        drawBackground(canvas);
        //绘制验证码
        drawVerifyText(canvas);
    }

注:其中textColor是缓存下当前设置的edittext的颜色,然后设置颜色为透明,这样在之后的输入中就不会看到EditText默认样子的文本,但是这样做又会影响我们要主动绘制文本颜色,所以在默认的绘制完之后,自定义的绘制之前设置回textColor。

首先看下绘制背景,绘制背景包含两部分,一个是背景,一个是选中的前景,其中背景就是循环部分。下面看代码中的注释的详细解释

  • 注释1:这边是配置一个矩形的位置,这个矩形是验证码框的最大边界。我们不管设置什么形状,都是在这个范围之内,其中animOffset是我做的一个小动画,如果不想要的话,可以直接去掉。
image.png
image.png
  • 注释2部分:确定边框后,通过setBounds定位,setState绘制选择器中哪一个drawable,draw绘制,save保存,translate移动到下一个位置
  • 注释3部分:画完之后要恢复到初始的状态,然后才能绘制后面的选中图层。
  • 注释4部分:选中图层类似,要确定一个矩形边界,然后根据当前输入的文本长度确定绘制位置,最后进行绘制。
 private void drawBackground(Canvas canvas) {
        //输入框矩形
        Rect rect = new Rect();
        //获取当前的保存状态
        canvas.translate(startPos, 0);
        int count = canvas.getSaveCount();
        canvas.save();

        //绘制几个框
        for (int i = 0; i < verifyCodeLength; i++) {
            //注释1
            rect.left = 0;
            rect.top = strokeHeight - animOffset;
            rect.right = rect.left + strokeWidth;
            rect.bottom = rect.top + strokeHeight;

            if (rect.top != 0) {
                animOffset += 2;
            }
            //注释2
            //设置框边界
            strokeBackground.setBounds(rect);
            //设置框状态
            strokeBackground.setState(new int[]{android.R.attr.state_enabled});
            //绘制
            strokeBackground.draw(canvas);
            //Ctrl s
            canvas.save();
            //位移
            canvas.translate(rect.right + strokePadding, 0);
        }

        //注释3
        canvas.restoreToCount(count);
        canvas.translate(0, 0);

        //注释4
        if (getEditableText().length() != verifyCodeLength) {
            //确定输入内容长度
            int current = Math.max(0, getEditableText().length());
            //确定方框左右边界
            rect.left = (strokeWidth + strokePadding) * current;
            rect.right = rect.left + strokeWidth;
            strokeBackground.setState(new int[]{android.R.attr.state_focused});
            strokeBackground.setBounds(rect);
            strokeBackground.draw(canvas);
        }
    }

最后是绘制文本的部分,绘制文本重点关注x,y坐标的确定。x坐标是这样确定的:

image.png
image.png
首先确定每个文字中心点x坐标:strokeWidth / 2 + (strokeWidth + strokePadding) * i然后减去文字bounds的一半就是说明从“喵”的最左边开始写起 , y坐标的话同理 画布的一半的y轴坐标就是 (bottom - top )/2, 加上文字的一半,则书写从左下角开始写起

  private void drawVerifyText(Canvas canvas) {
        Rect rect = new Rect();
        canvas.translate(0, 0);
        int count = canvas.getSaveCount();
        canvas.save();
        int length = getEditableText().length();
        TextPaint textPaint = getPaint();
        for (int i = 0; i < length; i++) {
            String s = String.valueOf(getEditableText().charAt(i));
            textPaint.setColor(textColor);
            textPaint.setFakeBoldText(true);
            textPaint.getTextBounds(s, 0, 1, rect);
            //计算位置
            //x坐标以文字基线位置(左)位置为准
            int x = strokeWidth / 2 + (strokeWidth + strokePadding) * i - (rect.centerX());
            //y坐标以文字基线(下)位置为准
            int y = canvas.getHeight() / 2 + rect.height() / 2;
            canvas.drawText(s, x, y, textPaint);
        }
        canvas.restoreToCount(count);
    }

在这里还有一个输入完成的回调,用于给外界获取数据。

  /**
     * 输入内容监听器
     */
    @Override
    protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
        super.onTextChanged(text, start, lengthBefore, lengthAfter);
        int textLength = getEditableText().length();
        if (textLength == verifyCodeLength) {
            Systems.hideIME();
            if (dataCall != null) {
                dataCall.back(getEditableText().toString());
            }
        }
    }

    /**
     * 回调
     */
    public void setDataCall(DataCall<String> dataCall) {
        this.dataCall = dataCall;
    }

这里面有一些我自己封装的方法,如DataCall(就是一个带数据返回的接口),hideIME(隐藏输入法)等。最后贴一下selector:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_focused="true">
        <shape android:shape="oval">
            <stroke android:width="2dp"
                android:color="#FFFB9C00" />
            <corners android:radius="4dp" />
        </shape>
    </item>
    <item>
        <shape android:shape="oval">
            <stroke android:width="2dp" android:color="#EEEEEE" />
            <corners android:radius="4dp" />
        </shape>
    </item>
</selector>

总结

对于这样一个自定义View,绘制的内容比较简单,没有什么复杂的操作,重点是确定输入框的位置。然后处理官方View所显示的内容。相对来说还是简单的。

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

推荐阅读更多精彩内容

  • ¥开启¥ 【iAPP实现进入界面执行逐一显】 〖2017-08-25 15:22:14〗 《//首先开一个线程,因...
    小菜c阅读 6,350评论 0 17
  • 首先,我们来看看实现的是怎么样的效果: 如果我们拿到这样的UI,想到的布局应该是用4个EditText包在横向的L...
    Android高级工程师阅读 875评论 0 13
  • 我想每个写文章的作者都有过相似情况,有时候你打开手机或者电脑,一时间失去灵感。不知道如何敲出那代表你思想的一串串文...
    JANEBURY阅读 616评论 4 13
  • 岁月静静好 小步碎碎跑 跑过 春夏秋冬 还有 那 山花烂漫 静静香 …… 杯酒释怀 几...
    桓舟子阅读 77评论 0 2
  • 对着空调吹, 惬意加迷醉, 不知不觉已入睡, 梦圆与谁?
    鸿怡轩阅读 164评论 1 0