天天看點

一個仿微信但是樣式更加靈活的密碼框控件

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位址:這裡