一个仿微信但是样式更加灵活的密码框控件

jCenter

依赖:

implementation 'coder.siy:password-textView:1.0.0'

首先看看效果:

图一

我为它的很多属性都开放了接口,可以根据自己的需要自由修改。

效果看了,接下谈谈它是怎么实现的。

主要是思路可以由下图来表示:

图二

控件是继承于系统控件TextView,然后重写onDraw(Canvas),这样可以减少很多麻烦。

根据图片的显示顺序

首先是绘制黑色的底:

 /**
     * 绘制边框,先绘制一整块区域
     */
    private void drawBoarder(Canvas canvas) {
        canvas.drawRoundRect(rect, borderRadius, borderRadius, borderPaint);
    }

rect:黑色长方形大小。

borderRadius:黑色长方形的圆角度数,当为0时就是直角

borderPaint:黑色长方形的画笔

Canvas.drawRoundRect:绘制的是圆角矩形

然后绘制白色内容区域:

/**
     * 绘制内容区域,和内容边界
     *
     * @param canvas
     */
    private void drawContent(Canvas canvas) {
        //每次绘制是都要重置rectIn的位置
        rectIn.left = rect.left + borderWidth;
        rectIn.top = rect.top + borderWidth;
        rectIn.right = rectIn.left + contentWidth;
        rectIn.bottom = rectIn.top + contentHeight;

        for (int i = 0; i < pwdLen; i++) {
            canvas.drawRoundRect(rectIn, contentRadius, contentRadius, contentPaint);
            canvas.drawRoundRect(rectIn, contentRadius, contentRadius, contentBoardPaint);
            rectIn.left = rectIn.right + contentMargin;
            rectIn.right = rectIn.left + contentWidth;
        }
    }

rectIn:白色的正方形大小。

绘制白色正方形的时候有2个考虑点:borderWidth和contentMargin。

borderWidth:每一个白色的正方形顶部(底部)距离黑色长方形顶部(底部)距离,第一个白色正方形的左边距离黑色长方形左边的距离,最后一个白色正方形的右边距离黑色长方形右边的距离。

contentMargin:每一个白色正方形相互之间的间隔。

然后仔细计算每次绘制rectIn。

最后绘制密码显示的圆点:

 /**
     * 绘制密码
     *
     * @param canvas
     */
    private void drawPwd(Canvas canvas) {
        float cy = rect.top + height / 2;
        float cx = rect.left + contentWidth / 2 + borderWidth;

        CharSequence nowText = getText();
        for (int i = 0; i < curLenght; i++) {
            if (isShowPwdText) {
                String drawText = String.valueOf(nowText.charAt(i));
                canvas.drawText(drawText, 0, drawText.length(), cx, cy - pwdTextOffsetY, pwdTextPaint);
            } else {
                canvas.drawCircle(cx, cy, pwdWidth / 2, pwdPaint);
            }
            cx = cx + contentWidth + contentMargin;
        }
    }

绘制密码显示的圆点主要就是要计算出cx,cy。

cy:


cx:


大方向是解决了。还有一些细节需要处理一下。

1,如何保证内容区域始终是正方形。

2,文本框状态怎么自动保存恢复。

3,明文怎么绘制在对话框中间。

4,继承TextView怎么来的光标。

5,顺便提一下分割线的实现。

如何保证内容区域始终是正方形:

   /**
     * 当borderWidth,pwdLen,contentMargin修改之后需要调用此方法
     * @param w TextView控件的宽
     * @param h TextView控件的高
     */
    private void calculateBorderAndContentSize(int w,int h){
        //算出本应该的宽高
        contentWidth = (w - 2 * borderWidth - (pwdLen - 1) * contentMargin) / (float) pwdLen;
        contentHeight = h - 2 * borderWidth;

        //为了绘制正方形,取小的数值
        contentHeight = contentWidth = contentHeight > contentWidth ? contentWidth : contentHeight;

        //变成正方形之后的宽度和高度
        height = contentHeight + 2 * borderWidth;
        width = (contentHeight * pwdLen) + 2 * borderWidth + contentMargin * (pwdLen - 1);

        //变成正方形之后,重新计算绘制的起点
        drawStartX = (w - width) / 2.0f;
        drawStartY = (h - height) / 2.0f;

        // 外边框
        rect = new RectF(drawStartX, drawStartY, drawStartX + width, drawStartY + height);

        //内容区域边框,也就是输入密码数字的那个格子
        rectIn = new RectF();
    }

contentWidth:图二中白色正方形的宽

contentHeight:图二中白色正方形的高

width:图二中黑色长方形的宽

height:图二中黑色长方形的高

drawStartX:黑色长方形左上角的X坐标(注:有一个透明底,原TextView站住的大小)

drawStartY:黑色长方形左上角的Y坐标(注:有一个透明底,原TextView站住的大小)

rect:黑色长方形的大小

rectIn:白色正方形大小

首先根据TextView的宽(w)高(h)计算出contentWidth和contentHeight,然后比较contentWidth和contentHeight的大小,取较小者赋值给conentWidth和cotentHeight,然后根据重新赋值的contentWidth和contentHeight计算出width和height,最后根据w,h和width,height计算出drawStartX和drawStartY。这样我就能根据计算出来的width,height,contentWidth,contentHeight,drawStartX和drawStartY保存绘制出来的内容框始终是正方形并且绘制的区域始终位于TextView的中心区域。

文本框状态怎么自动保存恢复:

@Override
    public Parcelable onSaveInstanceState() {
        Bundle bundle = new Bundle();
        bundle.putParcelable("instanceState", super.onSaveInstanceState());
        bundle.putString("curtext", getText().toString());
        bundle.putBoolean("isShowCursor",isShowCursor);
        bundle.putBoolean("isShowPwdText",isShowPwdText);
        bundle.putInt("borderColor",borderColor);
        bundle.putInt("borderRadius",borderRadius);
        bundle.putInt("contentColor",contentColor);
        bundle.putInt("contentBoardColor",contentBoardColor);
        bundle.putInt("contentBoardWidth",contentBoardWidth);
        bundle.putInt("contentRadius",contentRadius);
        bundle.putInt("contentMargin",contentMargin);
        bundle.putInt("splitLineColor",splitLineColor);
        bundle.putInt("cursorColor",cursorColor);
        bundle.putInt("cursorMargin",cursorMargin);
        bundle.putInt("pwdLen",pwdLen);
        bundle.putInt("pwdColor",pwdColor);
        bundle.putInt("pwdWidth",pwdWidth);
        return bundle;
    }

    @Override
    public void onRestoreInstanceState(Parcelable state) {
        if (state instanceof Bundle) {
            Bundle bundle = (Bundle) state;
            String curText = bundle.getString("curtext");
            if (!TextUtils.isEmpty(curText)) {
                setText(curText);
            }

            boolean isShowCursor = bundle.getBoolean("isShowCursor");
            if (isShowCursor){
                showCursor();
            }else{
                hideCursor();
            }

            boolean isShowPwdText = bundle.getBoolean("isShowPwdText");
            showPwdText(isShowPwdText);

            int borderColor = bundle.getInt("borderColor");
            setBorderColor(borderColor);

            int borderRadius = bundle.getInt("borderRadius");
            setBorderRadius(borderRadius);

            int contentColor = bundle.getInt("contentColor");
            setContentColor(contentColor);

            int contentBoardColor = bundle.getInt("contentBoardColor");
            setContentBoardColor(contentBoardColor);

            int contentBoardWidth = bundle.getInt("contentBoardWidth");
            setContentBoardWidth(contentBoardWidth);

            int contentRadius = bundle.getInt("contentRadius");
            setContentRadius(contentRadius);

            int contentMargin = bundle.getInt("contentMargin");
            setContentMargin(contentMargin);

            int splitLineColor = bundle.getInt("splitLineColor");
            setSplitLineColor(splitLineColor);

            int cursorColor = bundle.getInt("cursorColor");
            setCursorColor(cursorColor);

            int cursorMargin = bundle.getInt("cursorMargin");
            setCursorMargin(cursorMargin);

            int pwdLen = bundle.getInt("pwdLen");
            setPwdLen(pwdLen);

            int pwdColor = bundle.getInt("pwdColor");
            setPwdColor(pwdColor);

            int pwdWidth = bundle.getInt("pwdWidth");
            setPwdWidth(pwdWidth);

            state = bundle.getParcelable("instanceState");
        }
        super.onRestoreInstanceState(state);
    }

这个简单不做过多解释。

明文怎么绘制在对话框中间:

这个问题看起来简单其实并不是很简单。

如果你这样 canvas.drawText(drawText, 0, drawText.length(), cx, cy, pwdTextPaint);绘制明文你会发现明文其实是偏中上位置的。因为cy所代表的是基线的位置。具体可以查看这篇文章

/**
     * 绘制密码
     *
     * @param canvas
     */
    private void drawPwd(Canvas canvas) {
        float cy = rect.top + height / 2;
        float cx = rect.left + contentWidth / 2 + borderWidth;

        CharSequence nowText = getText();
        for (int i = 0; i < curLenght; i++) {
            if (isShowPwdText) {
                String drawText = String.valueOf(nowText.charAt(i));
                canvas.drawText(drawText, 0, drawText.length(), cx, cy - pwdTextOffsetY, pwdTextPaint);
            } else {
                canvas.drawCircle(cx, cy, pwdWidth / 2, pwdPaint);
            }
            cx = cx + contentWidth + contentMargin;
        }
    }

这里要如何计算出drawText的x,y的坐标呢?

x其实可以直接使用cx的,只要设置pwdTextPaint的setTextAlign(Paint.Align.CENTER),默认是Paint.Align.LEFT。这个属性是x相对于绘制字符串的位置,如果是Paint.Align.LEFT则x在绘制字符串的左边,如果是Paint.Align.CENTER则x在绘制字符串的中间,显然符合我们的需求。

y的坐标怎么计算呢?

目标:通过cy把baseline计算出来。


我觉得这幅图解释的很好了。为什么没有拿top和bottom计算baseLineY呢?因为系统建议的,绘制单个字符时字符的最高高度应该是ascent最低高度应该是descent。所以计算出来baseLineY:cy-(paint.ascent()+paint.descent())/2。

继承TextView怎么来的光标:

当然是自己绘制啊!!!

绘制光标:

 /**
     * 绘制光标
     *
     * @param canvas
     */
    private void drawCursor(Canvas canvas) {
        float startX, startY, stopY;
        int sin = curLenght - 1;
        float half = contentWidth / 2;

        if (sin == -1) {
            startX = borderWidth + half;
            startY = cursorMargin + borderWidth;
            stopY = height - borderWidth - cursorMargin;
            canvas.drawLine(drawStartX + startX, drawStartY + startY, drawStartX + startX, drawStartY + stopY, cursorPaint);
        } else {
            startY = cursorMargin + borderWidth;
            stopY = height - borderWidth - cursorMargin;

            if (isShowPwdText) {
                String s = String.valueOf(getText().charAt(sin));
                pwdTextPaint.getTextBounds(s, 0, s.length(), textBoundrect);
                startX = borderWidth + sin * (contentWidth + contentMargin) + half + textBoundrect.width() / 2 + cursourMarginPwd;
            } else {
                startX = borderWidth + sin * (contentWidth + contentMargin) + half + pwdWidth / 2 + cursourMarginPwd;
            }
            canvas.drawLine(drawStartX + startX, drawStartY + startY, drawStartX + startX, drawStartY + stopY, cursorPaint);
        }
    }

绘制光标的时候需要注意:第一个密码输入框在没有输入字符时它应该显示在密码输入框中间,如果输入了字符就显示在字符右边.还需要注意字符是明文还是密文,因为我们需要分别计算明文和密文的宽度。


光标闪烁:

/**
     * 显示光标
     */
    public void showCursor() {
        if (handler == null) {
            handler = new TimerHandler(this);
        }

        //只有当光标没有显示的时候才让它显示
        if (!isShowCursor) {
            isShowCursor = true;
            handler.sendEmptyMessageDelayed(0, 500);
        }
    }

    /**
     * 隐藏光标
     */
    public void hideCursor() {
        if (handler != null) {
            handler.removeCallbacksAndMessages(null);
        }
        showCursorSwitch = false;
        isShowCursor = false;
        invalidateView();
    }

    private static class TimerHandler extends Handler {
        private WeakReference<PwdView> reference;

        TimerHandler(PwdView view) {
            reference = new WeakReference<>(view);
        }

        @Override
        public void handleMessage(Message msg) {
            PwdView view = reference.get();
            if (view != null) {
                view.showCursorSwitch = !view.showCursorSwitch;
                view.invalidateView();
                sendEmptyMessageDelayed(0, 500);
            }
        }
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        hideCursor();
    }

光标的闪烁是通过Handler来实现。如果不是特别需要不建议打开光标闪烁。

顺便提一下分割线的实现:

就是在2个密码框的间隔距离之间画一个竖线。

/**
     * 绘制分割线
     *
     * @param canvas
     */

    private void drawSplitLine(Canvas canvas) {
        float startX = rect.left + borderWidth + contentWidth + (contentMargin / 2.0f);
        for (int i = 1; i < pwdLen; i++) {
            canvas.drawLine(startX, rect.top + borderWidth, startX, rect.bottom - borderWidth, splitLinePaint);
            startX = startX + contentWidth + contentMargin;
        }
    }

github地址:这里

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值