Android 自定义验证码View,可控制输入数量,可定制化样式。高度可定制化。Kotlin版本

26 篇文章 2 订阅

前言

需要开发一个自定义验证码输入控件,允许根据业务需求控制输入的验证码的数量,比如:

允许输入6位数的验证码 ⬇️⬇️⬇️⬇️⬇️⬇️
在这里插入图片描述
允许输入4位数的验证码⬇️⬇️⬇️⬇️⬇️⬇️
在这里插入图片描述

所以就手动撸一个吧!🙆‍♀️

分析

既然需要允许可控制输入的数量,那么需要一个容器且其中每一个ItemView都应该是动态添加的。所以下面分别对容器ItemView的实现进行简单的分析。

ItemView

  1. 从上图分析,ItemView需要的元素是这么几个,显示数字、下划线、光标
  2. 需要有这么几种状态,光标闪烁、空白(即还没有被输入到)、数字显示(填充)

容器

  1. 对于容器来说,首先肯定需要输入,所以需要一个背景透明的EditText作为输入
  2. 需要一个容器,这里选用LinearLayout作为容器,动态添加ItemView

代码结构
对于容器层面来说,需要在不同的时候分别调用,ItemView的不同状态显示,所以可以封装接口,用来控制ItemView的几种状态(光标闪烁、空白(即还没有被输入到)、数字显示(填充))来供容器调用。
这样一个封装好的容器,只需要对接实现这个接口的ItemView即可,至于ItemView则可以随时进行替换😎。

好了,上面就是大致思路,下面来核心代码解析。

核心代码

接口
经过上面分析,我们知道需要提供一个接口供ItemView实现,提供三种样式的调用方法。

/**
 * 作者: pumpkin
 * 描述: 验证码View,可灵活配置验证码输入个数,以及个性化UI显示样式
 */
/**
 * 验证码 item view,所需要对外调用的方法。
 * 如果需要自定义验证码View,必须实现VerifyCodeItemViewStyle
 */
interface VerifyCodeItemViewStyle {

    /**
     * 显示数字
     */
    fun displayNumStyle(char: Char)

    /**
     * 光标闪烁样式
     */
    fun cursorBlinksStyle()

    /**
     * 不显示光标,也不显示数字
     */
    fun defaultStyle()
}

默认ItemView实现

/**
 * 作者: pumpkin
 * 描述: 验证码View,可灵活配置验证码输入个数,以及个性化UI显示样式
 */
/**
 * 默认验证码itemView实现
 */
open class DefaultVerifyCodeItemView
@JvmOverloads
constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr), VerifyCodeItemViewStyle {

    /**
     * 显示的textView
     */
    val tvValue = TextView(context)

    /**
     * 控制光标的显示与隐藏
     */
    var cursorControl = false

    /**
     * 光标画笔
     */
    val mPaint: Paint = Paint().apply {
        isAntiAlias = true
        style = Paint.Style.FILL
        color = ContextCompat.getColor(context, R.color.gray_3394EC)
    }

    /**
     * 时间戳闪动控制器
     */
    var timer: CountDownTimer? = null

    init {
        //自身相关属性设置
        orientation = VERTICAL
        gravity = Gravity.CENTER
        layoutParams =
            LayoutParams(LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)
    }
    
    /**
     * 对应activity onResume的适合被调用,即DecorView被添加到ViewRootImpl时,适合初始化操作
     */
    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        //默认ViewGroup不会进行绘制 所以需要允许进行绘制
        setWillNotDraw(false)

        //textView 相关属性设置
        val width = 46F.dpToPx
        val height = 56F.dpToPx
        val lpt = LayoutParams(width, height)
        tvValue.setBackgroundColor(ContextCompat.getColor(context, R.color.transparent))
        tvValue.gravity = Gravity.CENTER
        tvValue.setTextColor(ContextCompat.getColor(context, R.color.black))
        tvValue.textSize = 48F
        tvValue.typeface = Typeface.defaultFromStyle(Typeface.BOLD)
        //添加进容器
        addView(tvValue, lpt)

        //添加下划线
        val heightLine = 2F.dpToPx
        val line = View(context)
            .apply {
                setBackgroundColor(ContextCompat.getColor(context, R.color.gray_d9d9d9))
            }
        val lll = LayoutParams(width, heightLine)
        //添加进容器
        addView(line, lll)
    }

    /**
     * 绘制光标
     */
    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        customOnDraw(canvas)
    }

    open fun customOnDraw(canvas: Canvas?) {
        if (cursorControl) {
            //cursor 宽度
            val cursorWidth = 2F.dpToPx.toFloat()

            //绘制坐标
            val xStart = width / 2 - cursorWidth / 2
            val xEnd = width / 2 + cursorWidth / 2
            val hStart = 10F.dpToPx.toFloat()
            val hEnd = height.toFloat() - 8F.dpToPx

            canvas?.drawRect(xStart, hStart, xEnd, hEnd, mPaint)
        }
    }

    override fun displayNumStyle(char: Char) {
        cursorControl = false
        timer?.cancel()
        tvValue.visibility = View.VISIBLE
        tvValue.text = char.toString()
        invalidate()
    }

    override fun cursorBlinksStyle() {
        tvValue.visibility = View.INVISIBLE
        //光标闪烁
        if (timer == null) {
            timer = object : CountDownTimer(Int.MAX_VALUE.toLong(), 600) {
                override fun onTick(millisUntilFinished: Long) {
                    cursorControl = !cursorControl
                    invalidate()
                }

                override fun onFinish() {

                }

            }
        }
        timer?.start()
    }

    override fun defaultStyle() {
        timer?.cancel()
        cursorControl = false
        tvValue.visibility = View.INVISIBLE
        invalidate()
    }

    /**
     * 对应activity destory的时候被调用,即ViewRootImpl做doDie时,适合销毁操作
     */
    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        timer?.cancel()
        timer = null
    }

}

容器实现
下面进行容器代码实现。

/**
 * 作者: pumpkin
 * 描述: 验证码View,可灵活配置验证码输入个数,以及个性化UI显示样式
 */
 /**
 * 验证码容器
 */
open class VerifyCodeView
@JvmOverloads
constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {

    /**
     * 验证码item容器
     */
    val itemContainer = LinearLayout(context)

    /**
     * 输入控件
     */
    val editText = EditText(context).apply {
        inputType = InputType.TYPE_CLASS_NUMBER
        isLongClickable = false
        setBackgroundColor(ContextCompat.getColor(context, R.color.transparent))
        setTextColor(ContextCompat.getColor(context, R.color.transparent))
        isCursorVisible = false
    }

    /**
     * 验证码位数,默认为6,允许更改范围为1-8
     */
    var verifyNum = 6
        set(value) {
            field = if (value >= 1 || value <= 8) {
                value
            } else {
                6
            }

            //设置EditText的最大输入数量
            editText.filters = arrayOf<InputFilter>(InputFilter.LengthFilter(field))
        }

    /**
     * 完整监听
     */
    var inputCompleteListener: InputCompleteListener? = null

    init {
        //解析xml属性
        val attributes = context.obtainStyledAttributes(attrs, R.styleable.VerifyCodeView)
        //验证码位数,默认为6,允许更改
        verifyNum = attributes.getInt(R.styleable.VerifyCodeView_msg_code_length, 6)
        attributes.recycle()
    }

    init {
        //itemContainer 填充默认的itemView
        fillUpDefaultItemView()
    }

    /**
     * 初始化相关操作
     */
    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        //添加 容器
        addView(itemContainer, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
        //添加 输入框EditText
        addView(editText, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))

        //editText监听处理
        editTextListener()

        //默认第一个为itemView 进行光标闪烁
        itemContainer.takeIf { it.childCount > 0 }?.let { linearLayout ->
            val itemView = try {
                linearLayout.getChildAt(0) as VerifyCodeItemViewStyle
            } catch (e: Exception) {
                throw RuntimeException("VerifyItemView 必须是 VerifyCodeItemViewStyle 类型的", e)
            }
            itemView.cursorBlinksStyle()
        }
    }

    /**
     * editTextListener 监听处理
     */
    private fun editTextListener() {
        editText.afterTextChanged { editable: Editable? ->
            val container = itemContainer
            val childCount = container.childCount
            val content = editable.toString()
            val inputCount = content.length

            for (i in 0 until childCount) {
                //获取itemView
                val itemView = try {
                    container.getChildAt(i) as VerifyCodeItemViewStyle
                } catch (e: Exception) {
                    throw RuntimeException("VerifyItemView 必须是 VerifyCodeItemViewStyle 类型的", e)
                }

                if (i < inputCount) {
                    //设置显示的字体
                    val singleChar = content[i]
                    itemView.displayNumStyle(singleChar)
                    continue
                }

                //光标闪烁
                if (i == inputCount) {
                    itemView.cursorBlinksStyle()
                    continue
                }

                //什么都不显示
                itemView.defaultStyle()
            }

            //验证码是否完成回调
            if (inputCount >= childCount) {
                inputCompleteListener?.inputComplete()
            } else {
                inputCompleteListener?.invalidContent()
            }
        }
    }

    /**
     * 对外提供一个getEditContent方法
     */
    open fun getEditContent(): String? = editText.text.toString()

    /**
     * 对外提供清空验证码能力,若为清空就弹出键盘
     */
    open fun setText(activity: Activity?, inputContent: String?) {
        editText.setText(inputContent)
        if (TextUtils.isEmpty(inputContent)) {
            if (editText != null) {
                editText.setFocusable(true)
                editText.setFocusableInTouchMode(true)
                editText.requestFocus()
                if (editText.getContext() is Activity) {
                    (editText.getContext() as Activity).window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
                } else {
                    val imm =
                        application.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
                    imm?.showSoftInput(editText, InputMethodManager.SHOW_FORCED)
                }
            }
        }
    }

    /**
     * 填充默认的itemView
     */
    private fun fillUpDefaultItemView() {
        for (i in 0 until verifyNum) {
            val defaultVerifyCodeItemView = DefaultVerifyCodeItemView(context)
            itemContainer.addView(
                defaultVerifyCodeItemView,
                LinearLayout.LayoutParams(
                    ViewGroup.LayoutParams.WRAP_CONTENT,
                    ViewGroup.LayoutParams.WRAP_CONTENT,
                    1F
                )
            )
        }
    }

    /**
     * 填充自己个性化的itemView
     */
    open fun <T : VerifyCodeItemViewStyle> fillUpCustomItemView(customItemView: Class<T>) {
        if (VerifyCodeItemViewStyle::class.java.isAssignableFrom(customItemView)) {
            //先移除所有的View
            itemContainer.removeAllViews()
            for (i in 0 until verifyNum) {
                val verifyCodeItemView = try {
                    customItemView.getConstructor(Context::class.java).newInstance(context) as View
                } catch (e: Exception) {
                    throw RuntimeException("不能够创建一个实例 $customItemView , 或者不是一个View类型", e)
                }
                //填充View
                itemContainer.addView(
                    verifyCodeItemView,
                    LinearLayout.LayoutParams(
                        ViewGroup.LayoutParams.WRAP_CONTENT,
                        ViewGroup.LayoutParams.WRAP_CONTENT,
                        1F
                    )
                )
            }
        }
    }
}

其他非核心辅助代码

//辅助工具类

/**
 * 回调接口
 */
interface InputCompleteListener {
    fun inputComplete()
    fun invalidContent()
}

/**
 * afterTextChanged
 */
inline fun EditText.afterTextChanged(crossinline block: (Editable?) -> Unit = {}): TextWatcher {
    return object : TextWatcher {
        override fun afterTextChanged(s: Editable?) {
            block(s)
        }

        override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
        }

        override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
        }
    }.also {
        addTextChangedListener(it)
    }

}

val Float.dpToPx
    get() = dp2px(this)
    
fun dp2px(dpValue: Float): Int {
    val scale = application.resources.displayMetrics.density
    return (dpValue * scale + 0.5f).toInt()
}

//自定义属性 在attrs.xml 中添加
    <declare-styleable name="VerifyCodeView">
        <!--验证码长度配置-->
        <attr name="msg_code_length" format="integer"/>
    </declare-styleable>

使用

简单使用

    <com.verify.VerifyCodeView
        android:id="@+id/verifycode"
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:layout_marginTop="20dp"
        android:layout_marginEnd="10dp"
        app:msg_code_length="4" /> //设置验证码个数

可以代码中调用fillUpCustomItemView函数,手动填充自定义的ItemView
可以在代码中调用verifyNum设置验证码数量。

创作不易,如有帮助一键三连咯🙆‍♀️。欢迎技术探讨噢!

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

pumpkin的玄学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值