共计 10080 个字符,预计需要花费 26 分钟才能阅读完成。
前言
在我的项目中咱们经常继承 AppCompatEditText
或EditText
自定义验证码输入框来代替零碎输入框,以满足 UI 设计需要,如:
直线形输入框 | 方形输入框 |
---|---|
|
|
本文次要剖析自定义验证码输入框过程中常被忽视的光标问题及集体的一点经验总结
onDraw 办法始终被调用
咱们在 onDraw
办法中增加 Log 日志,发现 onDraw
办法每距离 500ms 左右被调用一次
此处先给出解决办法:
* 当咱们继承 EditText 自定义验证码输入框后,EditText 自带的光标对咱们来说不可见,曾经没有意义,因而须要将其暗藏掉,避免 onDraw 办法始终被调用
isCursorVisible = false
问题剖析
问题 1:是什么办法始终在不停的调用 onDraw 办法呢?
咱们晓得 invalidate
办法会触发页面重绘进而调用 onDraw
办法,EditText
又继承 TextView
,在TextView
源码中搜寻 invalidate
关键字而后加断点调试运行,最初将代码锁定在 invalidateCursorPath
办法,发现此办法不停被调用,代码如下:
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);
}
}
}
此办法又调用了 invalidateCursor
办法,代码如下:
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 */);
}
}
接着看代码,invalidateCursor
办法又调用了 invalidateRegion
办法,代码如下:
/**
* 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
办法中调用了 invaldate 办法,用于在指定地位绘制光标,invalidateCursorPath->invalidateCursor->invalidateRegion->invalidate
,此时能够解答问题 1 了:是什么办法始终在不停的调用 onDraw 办法呢?
答案 1:invalidateCursorPath 办法始终被调用,最初导致 onDraw 办法被调用
问题 2:什么办法在始终调用 invalidateCursorPath 办法呢?
持续剖析,发现 TextView 中有一个 setCursorVisible
办法,代码如下:
/**
* 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();}
}
此办法是设置光标是否可见,默认光标可见,看一下 mEditor.makeBlink()
对应的代码,如下:
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 实现了 Runnable 接口,对应的代码如下:
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;}
}
在下面的代码里,咱们惊喜的发现了 mTextView.invalidateCursorPath()
这句代码,剖析以上代码,重点关注 mTextView.postDelayed(this, BLINK);
这句代码,作用就是每距离 500ms 就会执行 TextView 中的invalidateCursorPath
办法,此时咱们大略明确了,EditText 默认会显示光标,每距离 500ms 就会绘制光标,造成光标不停闪动的成果,哦,原来是这样,当初能够解答问题 2 了
答案 2:Editor 中 Blink 类的 run 办法每隔 500ms 会调用 TextView 中的 invalidateCursorPath
办法
问题 3:如何自定义验证码输入框光标?
尽管 EditText 自带的光标曾经不能满足咱们的需要,但咱们能够参考其光标闪动的源码,而后批改一下来满足咱们的需要,重点是批改光标绘制时的显示地位
- 在控件可见时开启光标闪动,控件不可见时勾销光标闪动
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
等办法能够间接从 android.widget.Editor
类中 copy 过去,此处不再贴代码了
- 在
onDraw
办法里绘制光标,重点是计算光标显示地位
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)
}
}
}
答案 3:参考 android.widget.Editor
类中光标闪动代码,批改光标显示地位相干代码,即可实现光标闪动成果
GitHub
本文相干代码可在 GitHub 上获取,地址如下:
https://github.com/kongpf8848…
Android 高级开发零碎进阶笔记、最新面试温习笔记 PDF,我的 GitHub
文末
您的点赞珍藏就是对我最大的激励!
欢送关注我,分享 Android 干货,交换 Android 技术。
对文章有何见解,或者有何技术问题,欢送在评论区一起留言探讨!