ZEditText:自定义密码输入框

前言

心情非常沉重,仅以此篇文章纪念我摔碎的手机屏幕

偶然看到一篇文章,讲解了自定义密码输入框,直接上图:
ZEditText:自定义密码输入框_第1张图片
很多的App都有这样的功能,一般实现这种输入框有以下几种方案:

  1. 实实在在的写了5个EditText,然后各种焦点切换逻辑,哪怕增多或者减少一个输入框,都要改很多东西,最不推荐的一种方法;
  2. 隐藏一个EditText,把输入的内容显示在可见的TextView上,相对于第一种做法肯定是高级了很多;
  3. 继承EditText,修改onDraw绘制边框以及文字;

我看到的文章,讲解的是第三种做法,他直接继承了EditText,然后重写了onDraw方法,有兴趣的朋友可以查看原文:
自定义密码、验证码输入框–关关雎鸠在河之洲_

回想入行以来我还从来没有见过真正有人自定义EditeText,我也曾经试图研究过,但是后来也不了了之,今天是时候完成这个梦想了。

正文

第一步绘制UI

首先我们创建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源码中一个个抠出来还是花了我一些时间。下一次我会把此次的经验整理的更加完整与大家分享。

你可能感兴趣的:(自定义View系列,Android,android,Edittext,自定义输入框,输入法,软键盘)