Android custom payment password input box, cursor problem summary!

Posted by waygood on Tue, 28 Dec 2021 13:16:25 +0100

preface

In the project, we often inherit AppCompatEditText or EditText custom verification code input box to replace the system input box to meet UI design requirements, such as:

Linear input boxSquare input box

|

|
This paper mainly analyzes the cursor problem often ignored in the process of user-defined verification code input box and a personal experience summary

The onDraw method has been called all the time

We added a Log to the onDraw method and found that the onDraw method is called every 500ms or so

Here is the solution:

*After we inherit the EditText custom verification code input box, the cursor of EditText is invisible to us and has no meaning. Therefore, we need to hide it to prevent the onDraw method from being called all the time

isCursorVisible = false

problem analysis

Question 1: what method keeps calling the onDraw method?

We know that the invalidate method will trigger page redrawing and then call the onDraw method. EditText inherits TextView, searches the invalidate keyword in the TextView source code, and then adds a breakpoint to debug and run. Finally, lock the code in the invalidateCursorPath method. It is found that this method is constantly called. The code is as follows:

void invalidateCursorPath() {
        if (mHighlightPathBogus) {
            invalidateCursor();
        } else {
            final int horizontalPadding = getCompoundPaddingLeft();
            final int verticalPadding = getExtendedPaddingTop() + getVerticalOffset(true);

            if (mEditor.mDrawableForCursor == null) {
                synchronized (TEMP_RECTF) {
                    /*
                     * The reason for this concern about the thickness of the
                     * cursor and doing the floor/ceil on the coordinates is that
                     * some EditTexts (notably textfields in the Browser) have
                     * anti-aliased text where not all the characters are
                     * necessarily at integer-multiple locations.  This should
                     * make sure the entire cursor gets invalidated instead of
                     * sometimes missing half a pixel.
                     */
                    float thick = (float) Math.ceil(mTextPaint.getStrokeWidth());
                    if (thick < 1.0f) {
                        thick = 1.0f;
                    }

                    thick /= 2.0f;

                    // mHighlightPath is guaranteed to be non null at that point.
                    mHighlightPath.computeBounds(TEMP_RECTF, false);

                    invalidate((int) Math.floor(horizontalPadding + TEMP_RECTF.left - thick),
                            (int) Math.floor(verticalPadding + TEMP_RECTF.top - thick),
                            (int) Math.ceil(horizontalPadding + TEMP_RECTF.right + thick),
                            (int) Math.ceil(verticalPadding + TEMP_RECTF.bottom + thick));
                }
            } else {
                final Rect bounds = mEditor.mDrawableForCursor.getBounds();
                invalidate(bounds.left + horizontalPadding, bounds.top + verticalPadding,
                        bounds.right + horizontalPadding, bounds.bottom + verticalPadding);
            }
        }
    }

This method also calls the invalidecursor method. The code is as follows:

void invalidateCursor() {
        int where = getSelectionEnd();

        invalidateCursor(where, where, where);
    }

    private void invalidateCursor(int a, int b, int c) {
        if (a >= 0 || b >= 0 || c >= 0) {
            int start = Math.min(Math.min(a, b), c);
            int end = Math.max(Math.max(a, b), c);
            invalidateRegion(start, end, true /* Also invalidates blinking cursor */);
        }
    }

Then look at the code. The invalidecursor method calls the invalideregion method. The code is as follows:

/**
     * Invalidates the region of text enclosed between the start and end text offsets.
     */
    void invalidateRegion(int start, int end, boolean invalidateCursor) {
        if (mLayout == null) {
            invalidate();
        } else {
            int lineStart = mLayout.getLineForOffset(start);
            int top = mLayout.getLineTop(lineStart);

            // This is ridiculous, but the descent from the line above
            // can hang down into the line we really want to redraw,
            // so we have to invalidate part of the line above to make
            // sure everything that needs to be redrawn really is.
            // (But not the whole line above, because that would cause
            // the same problem with the descenders on the line above it!)
            if (lineStart > 0) {
                top -= mLayout.getLineDescent(lineStart - 1);
            }

            int lineEnd;

            if (start == end) {
                lineEnd = lineStart;
            } else {
                lineEnd = mLayout.getLineForOffset(end);
            }

            int bottom = mLayout.getLineBottom(lineEnd);

            // mEditor can be null in case selection is set programmatically.
            if (invalidateCursor && mEditor != null && mEditor.mDrawableForCursor != null) {
                final Rect bounds = mEditor.mDrawableForCursor.getBounds();
                top = Math.min(top, bounds.top);
                bottom = Math.max(bottom, bounds.bottom);
            }

            final int compoundPaddingLeft = getCompoundPaddingLeft();
            final int verticalPadding = getExtendedPaddingTop() + getVerticalOffset(true);

            int left, right;
            if (lineStart == lineEnd && !invalidateCursor) {
                left = (int) mLayout.getPrimaryHorizontal(start);
                right = (int) (mLayout.getPrimaryHorizontal(end) + 1.0);
                left += compoundPaddingLeft;
                right += compoundPaddingLeft;
            } else {
                // Rectangle bounding box when the region spans several lines
                left = compoundPaddingLeft;
                right = getWidth() - getCompoundPaddingRight();
            }

            invalidate(mScrollX + left, verticalPadding + top,
                    mScrollX + right, verticalPadding + bottom);
        }
    }

The invalidateRegion method calls the invaldate method to draw cursors at the specified location. InvalidateCursorPath->invalidateCursor->invalidateRegion->invalidate, which can answer the question 1, is what method has been calling the onDraw method continuously?

Answer 1: the invalidecursorpath method is always called, resulting in the onDraw method being called

Question 2: what method keeps calling the invalidecursorpath method?

Continue to analyze and find a setCursorVisible method in TextView. The code is as follows:

/**
     * Set whether the cursor is visible. The default is true. Note that this property only
     * makes sense for editable TextView.
     *
     * @see #isCursorVisible()
     *
     * @attr ref android.R.styleable#TextView_cursorVisible
     */
    @android.view.RemotableViewMethod
    public void setCursorVisible(boolean visible) {
        if (visible && mEditor == null) return; // visible is the default value with no edit data
        createEditorIfNeeded();
        if (mEditor.mCursorVisible != visible) {
            mEditor.mCursorVisible = visible;
            invalidate();

            mEditor.makeBlink();

            // InsertionPointCursorController depends on mCursorVisible
            mEditor.prepareCursorControllers();
        }
    }

This method is to set whether the cursor is visible. The default cursor is visible. Take a look at meditor The code corresponding to makeblink() is as follows:

void makeBlink() {
        if (shouldBlink()) {
            mShowCursor = SystemClock.uptimeMillis();
            if (mBlink == null) mBlink = new Blink();
            mTextView.removeCallbacks(mBlink);
            mTextView.postDelayed(mBlink, BLINK);
        } else {
            if (mBlink != null) mTextView.removeCallbacks(mBlink);
        }
    }

Blink implements the Runnable interface, and the corresponding code is as follows:

static final int BLINK = 500;

       /**
     * @return True when the TextView isFocused and has a valid zero-length selection (cursor).
     */
    private boolean shouldBlink() {
        if (!isCursorVisible() || !mTextView.isFocused()) return false;

        final int start = mTextView.getSelectionStart();
        if (start < 0) return false;

        final int end = mTextView.getSelectionEnd();
        if (end < 0) return false;

        return start == end;
    }

    private class Blink implements Runnable {
        private boolean mCancelled;

        public void run() {
            if (mCancelled) {
                return;
            }

            mTextView.removeCallbacks(this);

            if (shouldBlink()) {
                if (mTextView.getLayout() != null) {
                    mTextView.invalidateCursorPath();
                }

                mTextView.postDelayed(this, BLINK);
            }
        }

        void cancel() {
            if (!mCancelled) {
                mTextView.removeCallbacks(this);
                mCancelled = true;
            }
        }

        void uncancel() {
            mCancelled = false;
        }
    }

In the above code, we were pleasantly surprised to find mtextview Invalidecursorpath(), analyze the above code and focus on {mtextview postDelayed(this, BLINK); The purpose of this code is to execute the invalidecursorpath method in TextView every 500ms. At this time, we probably understand that EditText will display the cursor by default, and the cursor will be drawn every 500ms, resulting in the effect that the cursor keeps flashing. Oh, so it is. Now we can answer question 2

Answer 2: the run method of Blink class in Editor will call the invalidecursorpath method in TextView every 500ms

Question 3: how to customize the verification code input box cursor?

Although the built-in cursor of EditText can no longer meet our needs, we can refer to the source code of its cursor flicker, and then modify it to meet our needs. The focus is to modify the display position when the cursor is drawn

  • Enable cursor blinking when the control is visible and cancel cursor blinking when the control is not visible
override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
        super.onWindowFocusChanged(hasWindowFocus)
        if (hasWindowFocus) {
            mBlink?.uncancel()
            makeBlink()
        } else {
            mBlink?.cancel()
        }
    }

    override fun onFocusChanged(focused: Boolean, direction: Int, previouslyFocusedRect: Rect?) {
        super.onFocusChanged(focused, direction, previouslyFocusedRect)
        if (focused) {
            makeBlink()
        }
    }

Methods such as makeBlink can be directly from Android widget. copy from the editor class. No more code will be posted here

  • Draw the cursor in the onDraw method, focusing on calculating the cursor display position
private fun drawCursor(canvas: Canvas) {
        if (!mCursorVisible) return
        mCursorFlag = !mCursorFlag
        if (mCursorFlag) {
            if (mCursorDrawable == null && mCursorDrawableRes != 0) {
                mCursorDrawable = context.getDrawable(mCursorDrawableRes)
            }
            mCursorDrawable?.apply {
                val currentIndex = 0.coerceAtLeast(editableText.length)
                val count = canvas.save()
                val line = layout.getLineForOffset(selectionStart)
                val top = layout.getLineTop(line)
                val bottom = layout.getLineBottom(line)
                val mTempRect = Rect()
                getPadding(mTempRect)
                bounds = Rect(0, top - mTempRect.top, intrinsicWidth, bottom + mTempRect.bottom)
                canvas.translate(
                    (mCodeWidth + mCodeMargin) * currentIndex + mCodeWidth / 2f - intrinsicWidth / 2f,
                    (mCodeHeight - bounds.height()) / 2f
                )
                draw(canvas)
                canvas.restoreToCount(count)
            }
        }
    }

Answer 3: refer to Android widget. The cursor blinking code in the editor class can be modified to achieve the cursor blinking effect

GitHub

The relevant codes of this article can be obtained on GitHub at the following address:

https://github.com/kongpf8848...

Advanced notes of Android advanced development system, latest interview review notes PDF, My GitHub

end of document

Your favorite collection is my greatest encouragement!
Welcome to follow me, share Android dry goods and exchange Android technology.
If you have any opinions on the article or any technical problems, please leave a message in the comment area for discussion!

Topics: Android Back-end