心情非常沉重,仅以此篇文章纪念我摔碎的手机屏幕
偶然看到一篇文章,讲解了自定义密码输入框,直接上图:
很多的App都有这样的功能,一般实现这种输入框有以下几种方案:
我看到的文章,讲解的是第三种做法,他直接继承了EditText,然后重写了onDraw方法,有兴趣的朋友可以查看原文:
自定义密码、验证码输入框–关关雎鸠在河之洲_
回想入行以来我还从来没有见过真正有人自定义EditeText,我也曾经试图研究过,但是后来也不了了之,今天是时候完成这个梦想了。
首先我们创建ZEditText文件:
class ZEditText @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {}
先不研究输入法的功能,我们先把UI工作完成,重写onDraw方法,完成文字,边框,光标的绘制:
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
if (canvas == null) return
val fontMetrics: Paint.FontMetrics = mPaint.fontMetrics
val distance = (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.bottom
// 计算每一个字的位置
val centerY = height / 2f
val perWidth = (width - space * (inputCount - 1)) / inputCount
for (index in 0 until inputCount) {
// 绘制边框背景
drawTextBackground(canvas, index, perWidth)
// 绘制数字,垂直居中
if (index < text.length) {
// 如果是密码模式,不显示文字,用圆圈代替
val s = if (isPassword) {
"●"
} else {
text[index].toString()
}
val textWidth = mPaint.measureText(s)
canvas.drawText(
s,
(perWidth * index + perWidth / 2 - textWidth / 2) + space * index,
centerY + distance,
mPaint
)
}
// 在文字的末尾绘制光标
if (index == text.length) {
drawCursor(canvas, index, perWidth)
}
}
}
为了让我们的边框的设置更加的丰富,我选择使用drawable,这样使用者可以使用自定义shape或者图片都可以:
private fun drawTextBackground(
canvas: Canvas,
index: Int,
perWidth: Int
) {
if (textBackgroundDrawable == null) return
// 设置绘制图片的边界
val left = perWidth * index + space * index
textBackgroundDrawable?.setBounds(
left,
0,
left + perWidth,
height
)
// 如果是selector,还可以单独设置获取焦点时的背景
if (textBackgroundDrawable is StateListDrawable) {
if (index == text.length && isFocused) {
textBackgroundDrawable?.state = intArrayOf(android.R.attr.state_focused)
} else {
textBackgroundDrawable?.state = intArrayOf(android.R.attr.state_empty)
}
textBackgroundDrawable?.draw(canvas)
}
// 设置其他背景:shape或者图片
else {
textBackgroundDrawable?.draw(canvas)
}
}
输入的光标是需要闪烁的,关于闪烁的效果,可以有很多种做法,例如Handler,或者写个线程之类的,但是查看了一下EditText的源码,我发现了更舒服的方法:
private fun drawCursor(canvas: Canvas, index: Int, perWidth: Int) {
// 如果没有焦点,不需要绘制cursor
if (!isFocused) {
needDrawCursor = true
return
}
cursorDrawable?.let {
// 判断这一次是否需要绘制cursor
if (needDrawCursor) {
val left = perWidth * index + space * index + perWidth / 2 - cursorDrawableWidth / 2
it.setBounds(
left,
((height - cursorDrawableHeight) / 2),
left + cursorDrawableWidth,
((height + cursorDrawableHeight) / 2)
)
it.draw(canvas)
}
// 重绘延迟1000毫秒
// 为了防止意外启动了多个延迟任务,所以先做一个移除操作
Choreographer.getInstance().removeFrameCallback(drawCursorCallback)
Choreographer.getInstance().postFrameCallbackDelayed(drawCursorCallback, 1000)
}
}
/**重绘任务,仅仅是改变下一次是否要绘制cursor的标记,然后重绘*/
private val drawCursorCallback = Choreographer.FrameCallback {
needDrawCursor = !needDrawCursor
invalidate()
}
// 为了防止占用无用的资源,在移除的时候记得停止重绘任务
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
Choreographer.getInstance().removeFrameCallback(drawCursorCallback)
}
Choreographer的用法与Hanlder类似,他的注释大概是这样描述的:
Choreographer接受了系统的绘制信号,不停的绘制下一帧,每一个线程都会有一个Choreographer。
我们使用了postFrameCallbackDelayed,就是在绘制下一帧时执行的任务,这样就实现了不停重绘的效果。
现在我们已经完成了UI的绘制,接下来我们需要使用软键盘进行输入,首先在点击输入框的时候,我们应该弹出软键盘:
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent?): Boolean {
if (event?.action == MotionEvent.ACTION_UP) {
getInputMethodManager()?.let {
if (isFocusable && !isFocused) {
requestFocus()
}
// 绑定与软键盘的关系
it.viewClicked(this)
it.showSoftInput(this, 0)
}
}
return super.onTouchEvent(event)
}
刚开始我把显示软键盘的操作放在了OnClickListener中,但是测试发现会偶尔有点击事件不能响应的情况,具体原因还没有找到,而且如果我们占用了OnClickListener,就必须定义新的OnClickListener。查看EditText源码,发现它是在onTouchEvent中弹出软键盘,所以我们也跟着写。
因为软键盘的按键太多,而且一般这种输入框只需要输入数字,所以我们需要告诉软键盘我们只需要数字类型的输入:
// 重写此方法,设置软键盘属性
override fun onCreateInputConnection(outAttrs: EditorInfo?): InputConnection? {
outAttrs!!.inputType = EditorInfo.TYPE_CLASS_NUMBER
return null
}
EditorInfo里面有很多软键盘的属性,这里就不做介绍了,这里我们只设置了inputType为数字。如果你需要和软键盘交互,例如光标的位置,局部选择等等,就需要返回InputConnection,这里我们只是单向的接收按键信息,所以直接返回null。
接下来我们可以重写onKeyUp,处理按键的信息:
override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
when (keyCode) {
KeyEvent.KEYCODE_0,
KeyEvent.KEYCODE_1,
KeyEvent.KEYCODE_2,
KeyEvent.KEYCODE_3,
KeyEvent.KEYCODE_4,
KeyEvent.KEYCODE_5,
KeyEvent.KEYCODE_6,
KeyEvent.KEYCODE_7,
KeyEvent.KEYCODE_8,
KeyEvent.KEYCODE_9 -> {
if (text.length < inputCount) {
text += event?.displayLabel
invalidate()
if (text.length == inputCount) {
mOnEditCompleteListener?.onEditComplete(text)
}
}
return true
}
KeyEvent.KEYCODE_DEL -> {
if (!TextUtils.isEmpty(text)) {
text = text.substring(0, text.length - 1)
invalidate()
}
return true
}
KeyEvent.KEYCODE_ENTER -> {
hideSoft()
mOnEditCompleteListener?.onEditComplete(text)
return true
}
}
return super.onKeyUp(keyCode, event)
}
代码非常的简单,在这个输入框中,我们需要的按键只有0~9,回车和删除,所以我们只处理这些按键。
除此之外,我们还需要重写两个方法:
// 如果是一个文本编辑器,需要返回true
override fun onCheckIsTextEditor(): Boolean {
return true
}
// 是否在编辑模式中
override fun isInEditMode(): Boolean {
return isFocused
}
onCheckIsTextEditor方法介绍:
如果View具有编辑功能,需要返回true,否则返回false。请重写onCreateInputConnection设置软键盘并与软键盘交互。但是如果返回false,并不代表onCreateInputConnection不会被执行,它仅仅是对于系统的一个提示作用。
isInEditMode方法介绍:
当前是否处于编辑模式。
到目前为止,我们的ZEditText就已经完成了,看一下效果:
Github地址:https://github.com/li504799868/ZEditText
虽然以上的内容并不复杂,但是从EditText源码中一个个抠出来还是花了我一些时间。下一次我会把此次的经验整理的更加完整与大家分享。