The cursor problem that is often ignored in the input box of user-defined verification code, did you get it?

Time:2022-5-13

preface

In projects, we often inheritAppCompatEditTextorEditTextCustomize the verification code input box to replace the system input box to meet the UI design requirements, such as:

Linear input box Square input box
The cursor problem that is often ignored in the input box of user-defined verification code, did you get it?

1.gif
The cursor problem that is often ignored in the input box of user-defined verification code, did you get it?

2.gif

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 areonDrawMethod to findonDrawMethod per interval500msLeft and right are called once

The cursor problem that is often ignored in the input box of user-defined verification code, did you get it?

log.png

Here is the solution:

When we inheritEditTextAfter customizing the verification code input box,EditTextThe built-in cursor is invisible to us and has no meaning, so it needs to be hidden to preventonDraw()Method has been called

isCursorVisible = false

problem analysis

Question 1: what method keeps calling the OnDraw method?

We knowinvalidateMethod will trigger page redrawing and then callonDrawmethod,EditTextInherit againTextView, inTextViewSearch in source codeinvalidateKeyword, then add breakpoints to debug and run, and finally lock the code ininvalidateCursorPathMethod. 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 is called againinvalidateCursorMethod, 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,invalidateCursorMethod called againinvalidateRegionMethod, 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);
        }
    }

invalidateRegionMethod calledinvaldateMethod forDraws the cursor at the specified locationinvalidateCursorPath->invalidateCursor->invalidateRegion->invalidateAt this point, you can answer question 1: what method has been calling all the timeonDrawHow?

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 findTextViewOne of themsetCursorVisibleMethod, 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. Have a lookmEditor.makeBlink()The corresponding codes are 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);
        }
    }

BlinkRealizedRunnableInterface, 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 surprised to findmTextView.invalidateCursorPath()This code, analyze the above code and focus on itmTextView.postDelayed(this, BLINK);This code, every interval500msWill executeTextViewMediuminvalidateCursorPathMethod, we probably understand,EditTextBy default, the cursor is displayed every interval500msIt will draw the cursor and cause the cursor to flicker continuously. Oh, so it is. At this time, you 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 cursor of the verification code input box?

althoughEditTextThe built-in cursor can no longer meet our needs, but we can refer to the source code of its cursor flicker, and then modify it to meet our needs, mainly to modify the position of the cursor when drawing

  • Turn on 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()
        }
    }

makeBlinkAnd other methods can be directly fromandroid.widget.EditorClass. 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/ViewWorld