preface
In projects, we often inheritAppCompatEditText
orEditText
Customize 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 |
---|---|
![]() 1.gif
|
![]() 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 areonDraw
Method to findonDraw
Method per interval500ms
Left and right are called once

Here is the solution:
When we inheritEditText
After customizing the verification code input box,EditText
The 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 knowinvalidate
Method will trigger page redrawing and then callonDraw
method,EditText
Inherit againTextView
, inTextView
Search in source codeinvalidate
Keyword, then add breakpoints to debug and run, and finally lock the code ininvalidateCursorPath
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 is called againinvalidateCursor
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,invalidateCursor
Method called againinvalidateRegion
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);
}
}
invalidateRegion
Method calledinvaldate
Method forDraws the cursor at the specified location,invalidateCursorPath->invalidateCursor->invalidateRegion->invalidate
At this point, you can answer question 1: what method has been calling all the timeonDraw
How?
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 findTextView
One of themsetCursorVisible
Method, 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);
}
}
Blink
RealizedRunnable
Interface, 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 interval500ms
Will executeTextView
MediuminvalidateCursorPath
Method, we probably understand,EditText
By default, the cursor is displayed every interval500ms
It 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?
althoughEditText
The 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()
}
}
makeBlink
And other methods can be directly fromandroid.widget.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/ViewWorld