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

2019-12-01  本文已影响0人  做梦枯岛醒
抽空实现了一个验证码输入框的自定义View,效果图如下,其中每一个小圆圈都是一个selector,所以动态修改只需要改动drawable的内容即可实现修改,这样可发挥的内容也就很多。 WechatIMG4.jpegWechatIMG4.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是需要实现的,其中

 @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。

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

image.pngimage.png
 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.pngimage.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所显示的内容。相对来说还是简单的。

上一篇 下一篇

猜你喜欢

热点阅读