preface

Nowadays, more and more apps have started to add payment functions. No matter what kind of payment, the inevitable step is to enter a password, and even without a password, a verification code is also necessary. Low is implemented with a set of EditTexts that allow only one character to be entered in each input box, listened for by a TextWatcher, and then the next box automatically gets focus. Today introduces a custom View way to achieve, elegant and do not break forced case!

Without further ado, let’s start with the picture above

Github address, if helpful to you, trouble to give a star, manual dog head

features

  • Imitation alipay micro letter style
  • Underline style
  • Can display plain text or ciphertext. Ciphertext can display dots, asterisks, or any character
  • Support to set the interval between the password box and the rounded corner (when the interval is 0, the rounded corner only shows the left and right corner)
  • Supports setting border and password colors
  • Support to set the border color of the input part separately

The principle of

  1. Calculate the width of each password box by View width and interval width
    @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);
    }

    // Calculate boxWidth and check if the rounded corners are too large
    private void checkRadius(int availableWidth, int availableHeight) {
        // Width of each box = (available width - spacing width)/number of boxes
        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; }}// Check whether the spacing is too large
    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; }}Copy the code
  1. Draw borders and passwords
    @Override
    protected void onDraw(Canvas canvas) {
//// super.onDraw(canvas); // Remove the default drawing in 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); }}Copy the code
  • Draw borders (distinguish between different styles and spacing, rounded corners, etc.)

In spacing of 0, if every password directly draw boxes, adjacent edge will be repeated drawing, so I’m on the map, only the first draw boxes, behind each draw up, right, down only three sides (really is a fart big point optimization -_ – | | |), as follows:

Note that if you have rounded corners, you can’t draw three edges directly in the last box. Instead, I draw a rounded rectangle and use XferMode compositing to remove the left edge. If, first draw the rounded rectangle, then draw the left border, and then compose

    private void drawBorder(Canvas canvas, int index) {
        paint.setColor(index < textLength ? inputBorderColor : borderColor);
        paint.setStyle(Paint.Style.STROKE);
        switch (borderStyle) {
            case BorderStyle.BOX:// Frame mode
                if (radius == 0) {
                    // The rounded corner is 0
                    // When spacing is 0, draw the first box, and draw only the top, right, and bottom three edges for each following box. Avoid drawing one edge repeatedly
                    // If the spacing is not zero, draw the box directly
                    if (spacing == 0) {
                        if (index == 0) {
                            canvas.drawRect(rectF, paint);
                        } else{ fillLinesArray(); canvas.drawLines(linesArray, paint); }}else{ canvas.drawRect(rectF, paint); }}else {
                    / / the rounded! = 0
                    // Same as above, only add rounded corners. If there is no space and there are rounded corners, draw only the first and last rounded corners
                    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) {
                            // Draw three edges of the last password box with rounded corners
                            // Draw a box with two rounded corners, then use xferMode compositing to remove the left edge
                            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:// Bottom edge
                canvas.drawLine(rectF.left, rectF.bottom, rectF.right, rectF.bottom, paint);
                break; }}Copy the code
  • Draw password dots (dots, asterisks, and plaintext in different styles)

It is relatively simple to draw a password. It is necessary to measure the text when drawing plaintext or characters to ensure that the text is displayed in the center

    private void drawPassword(Canvas canvas, int index) {
        paint.setColor(pwdColor);
        paint.setStyle(Paint.Style.FILL);
        switch (pwdStyle) {
            case PwdStyle.CIRCLE:// Draw the dots
                canvas.drawCircle((rectF.left + rectF.right) / 2, (rectF.top + rectF.bottom) / 2.8, paint);
                break;
            case PwdStyle.ASTERISK:// Draw the * sign
                canvas.drawText(asterisk, (rectF.left + rectF.right) / 2,
                        (rectF.top + rectF.bottom - metrics.ascent - metrics.descent) / 2, paint);
                break;
            case PwdStyle.PLAINTEXT:// Draw the plaintext
                canvas.drawText(String.valueOf(getText().charAt(index)), (rectF.left + rectF.right) / 2,
                        (rectF.top + rectF.bottom - metrics.ascent - metrics.descent) / 2, paint);
                break; }}Copy the code

Matters needing attention:

  1. Drawing rounded Canvas. DrawRoundRect has API version restrictions, so use path to draw
  2. After setting the rounded corners and spacing, recalculate the width of the password box
  3. Always keep the cursor at the end to prevent confusion
  4. Note that EditText’s default background is going to have margins that affect the drawing, so we need to remove the background android:background=”@null”

Finally, introduce the use mode

  1. Layout:
    <! Android :background="@null" --> 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" />
Copy the code
  1. Setting up listeners
    passwordView.setInputListener(new PasswordInputView.InputListener() {
        @Override
        public void onInputCompleted(String text) {
            // Callback after input is complete}});Copy the code
  1. Attribute reference
    <declare-styleable name="PasswordInputView">
        <attr name="pwv_maxLength" format="integer" />// Maximum length<attr name="pwv_borderColor" format="color" />// Border color<attr name="pwv_pwdColor" format="color" />// Password color<attr name="pwv_haveInputBorderColor" format="color" />// The color of part of the border is entered<attr name="pwv_strokeWidth" format="dimension" />// Frame width<attr name="pwv_radius" format="dimension" />// Rounded radius<attr name="pwv_spacing" format="dimension" />// The spacing between each password box<attr name="pwv_asterisk" format="string" />// If the password style is asterisk, you can replace the asterisk with any character, the first character of pwv_asterisk<attr name="pwv_borderStyle" format="enum">// Border style box and underline<enum name="box" value="0" />
            <enum name="line" value="1" />
        </attr>
        <attr name="pwv_pwdStyle" format="enum">// Password style dot, asterisk, plaintext<enum name="circle" value="0" />
            <enum name="asterisk" value="1" />
            <enum name="plaintext" value="2" />
        </attr>
    </declare-styleable>
Copy the code