一個優雅的仿微信支付寶密碼(驗證碼)輸入框

前言

現在愈來愈多的app開始添加支付功能,不論哪一種支付,不可避免的步驟就是密碼的輸入,就算沒有密碼輸入,驗證碼也是少不了的。比較low的實現方式是用一組EditText來拼,每一個輸入框只容許輸入1個字符,經過TextWatcher來監聽,輸入完成後下一個輸入框自動獲取焦點。今天介紹一種自定義View的方式實現,優雅而不失逼格!java

廢話很少說,先上圖

Github地址,若是對您有幫助,麻煩給個star,手動狗頭 android

效果圖

特性

  • 仿支付寶微信風格
  • 下劃線風格
  • 可顯示明文或者密文,密文支持顯示圓點,星號,或者任意字符
  • 支持設置密碼框之間的間隔和圓角(間隔爲0時圓角只顯示最左和最右的圓角)
  • 支持設置邊框和密碼的顏色
  • 支持單獨設置已輸入部分的邊框顏色

原理

  1. 經過View寬度和間隔寬度計算每一個密碼框的寬度
@Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        int availableWidth = w - getPaddingLeft() - getPaddingRight();
        int availableHeight = h - getPaddingTop() - getPaddingBottom();

        checkSpacing(availableWidth);
        checkRadius(availableWidth, availableHeight);
    }

    // 計算boxWidth並檢查圓角大小是否過大
    private void checkRadius(int availableWidth, int availableHeight) {
        // 每一個盒子的寬度 = (可用寬度 - 間隔寬度)/ 盒子個數
        boxWidth = (availableWidth - (maxLength - 1f) * spacing) / maxLength;

        float availableRadius = Math.min(availableHeight / 2f, boxWidth / 2);
        if (radius > availableRadius) {
            Log.d(TAG, "radius is too large, reset it");
            radius = (int) availableRadius;
        } else if (radius < 0) {
            radius = 0;
        }
    }

    // 檢查間距是否過大
    private void checkSpacing(int availableWidth) {
        if (spacing < 0 || (maxLength - 1) * spacing >= availableWidth) {
            Log.d(TAG, "spacing is too large, reset it to zero");
            spacing = 0;
        }
    }
複製代碼
  1. 繪製邊框和密碼
@Override
    protected void onDraw(Canvas canvas) {
//// super.onDraw(canvas); // 去掉EditText默認的繪製

        int top = getPaddingTop();
        int bottom = getHeight() - getPaddingBottom();
        int start = getPaddingLeft();
        float left;

        for (int i = 0; i < maxLength; i++) {
            left = start + (boxWidth + spacing) * i;
            rectF.set(left, top, left + boxWidth, bottom);
            
            drawBorder(canvas, i);

            if (i >= textLength) continue;

            drawPassword(canvas, i);
        }
    }
複製代碼
  • 繪製邊框(區分不一樣的風格和間距、圓角等)

在間距爲0的時候,若是直接每一位密碼都繪製方框,相鄰的邊就會重複繪製,因此我在繪製時,只有第一個繪製方框,後邊的每個只繪製上、右、下三條邊(真的是屁大點優化 -_-|||),以下: git

繪製順序
注意,若是有圓角,在最後一個方框不能直接繪製三條邊,我是採用繪製一個圓角矩形,而後再用xfermode合成去掉左邊的一條邊來完成。若是,先繪製圓角矩形,再繪製左邊邊框,而後再合成
繪製順序

private void drawBorder(Canvas canvas, int index) {
        paint.setColor(index < textLength ? inputBorderColor : borderColor);
        paint.setStyle(Paint.Style.STROKE);
        switch (borderStyle) {
            case BorderStyle.BOX:// 邊框模式
                if (radius == 0) {
                    // 圓角爲0,判斷間距
                    // 間距爲0時第一個繪製方框,後邊的每個只繪製上、右、下三條邊,避免重複繪製一條邊
                    // 若是間距不爲0,直接繪製方框
                    if (spacing == 0) {
                        if (index == 0) {
                            canvas.drawRect(rectF, paint);
                        } else {
                            fillLinesArray();
                            canvas.drawLines(linesArray, paint);
                        }
                    } else {
                        canvas.drawRect(rectF, paint);
                    }
                } else {
                    // 圓角!=0
                    // 策略同上,只是增長了圓角,沒有間距而且有圓角的狀況只繪製第一個和最後一個圓角
                    if (spacing == 0) {
                        if (index == 0) {
                            fillRadiusArray(true);
                            path.reset();
                            path.addRoundRect(rectF, radiusArray, Path.Direction.CCW);
                            canvas.drawPath(path, paint);
                        } else if (index == maxLength - 1) {
                            // 這裏繪製最後一個密碼框的三條邊,帶圓角
                            // 先繪製一個帶兩個圓角的方框,而後用xfermode合成去掉左邊的一條邊
                            int layer = canvas.saveLayer(null, null, Canvas.ALL_SAVE_FLAG);

                            fillRadiusArray(false);
                            path.reset();
                            path.addRoundRect(rectF, radiusArray, Path.Direction.CCW);
                            canvas.drawPath(path, paint);

                            paint.setXfermode(xfermode);
                            canvas.drawLine(rectF.left, rectF.top, rectF.left, rectF.bottom, paint);
                            paint.setXfermode(null);

                            canvas.restoreToCount(layer);
                        } else {
                            fillLinesArray();
                            canvas.drawLines(linesArray, paint);
                        }
                    } else {
                        path.reset();
                        path.addRoundRect(rectF, radius, radius, Path.Direction.CCW);
                        canvas.drawPath(path, paint);
                    }
                }
                break;
            case BorderStyle.LINE:// 底部邊線
                canvas.drawLine(rectF.left, rectF.bottom, rectF.right, rectF.bottom, paint);
                break;
        }
    }
複製代碼
  • 繪製密碼圓點(不一樣風格有圓點、星號和明文)

繪製密碼就比較簡單,須要注意的是繪製明文或者字符時須要進行文字的測量,保證居中顯示github

private void drawPassword(Canvas canvas, int index) {
        paint.setColor(pwdColor);
        paint.setStyle(Paint.Style.FILL);
        switch (pwdStyle) {
            case PwdStyle.CIRCLE:// 繪製圓點
                canvas.drawCircle((rectF.left + rectF.right) / 2, (rectF.top + rectF.bottom) / 2, 8, paint);
                break;
            case PwdStyle.ASTERISK:// 繪製*號
                canvas.drawText(asterisk, (rectF.left + rectF.right) / 2,
                        (rectF.top + rectF.bottom - metrics.ascent - metrics.descent) / 2, paint);
                break;
            case PwdStyle.PLAINTEXT:// 繪製明文
                canvas.drawText(String.valueOf(getText().charAt(index)), (rectF.left + rectF.right) / 2,
                        (rectF.top + rectF.bottom - metrics.ascent - metrics.descent) / 2, paint);
                break;
        }
    }
複製代碼

注意事項:canvas

  1. 繪製圓角canvas.drawRoundRect有api版本限制,因此採用path的方式來繪製
  2. 設置圓角、間距後須要從新計算密碼框的寬度
  3. 須要始終保持光標在最後,防止出現錯亂
  4. 注意EditText的默認背景會有邊距,會影響繪製,因此須要去掉背景 android:background="@null"

最後介紹下使用方式

  1. 佈局中:
<!-- 注意EditText的默認背景會有邊距,會影響繪製,因此須要去掉 android:background="@null" -->
    <com.matthew.passwordinput.lib.PasswordInputView android:layout_width="match_parent" android:layout_height="50dp" android:layout_marginTop="10dp" android:background="@null" android:padding="1dp" android:text="123" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:pwv_haveInputBorderColor="@color/colorAccent" app:pwv_pwdStyle="plaintext" app:pwv_radius="10dp" app:pwv_spacing="12dp" />
複製代碼
  1. 設置監聽器
passwordView.setInputListener(new PasswordInputView.InputListener() {
        @Override
        public void onInputCompleted(String text) {
            // 輸入完成後回調
        }
    });
複製代碼
  1. 屬性參考
<declare-styleable name="PasswordInputView">
        <attr name="pwv_maxLength" format="integer" /> // 最大長度

        <attr name="pwv_borderColor" format="color" /> // 邊框顏色
        <attr name="pwv_pwdColor" format="color" /> // 密碼顏色
        <attr name="pwv_haveInputBorderColor" format="color" /> // 已輸入部分邊框的顏色

        <attr name="pwv_strokeWidth" format="dimension" /> // 邊框寬度
        <attr name="pwv_radius" format="dimension" /> // 圓角半徑
        <attr name="pwv_spacing" format="dimension" /> // 每一個密碼框之間的間距
        <attr name="pwv_asterisk" format="string" />
        // 當密碼風格爲星號風格時,能夠用任意字符替換星號,替換的字符爲pwv_asterisk的第一個字符

        <attr name="pwv_borderStyle" format="enum"> // 邊框風格 方框 和 下劃線
            <enum name="box" value="0" />
            <enum name="line" value="1" />
        </attr>
        <attr name="pwv_pwdStyle" format="enum"> // 密碼風格 圓點、星號、明文
            <enum name="circle" value="0" />
            <enum name="asterisk" value="1" />
            <enum name="plaintext" value="2" />
        </attr>
    </declare-styleable>
複製代碼
相關文章
相關標籤/搜索