前言
需要开发一个自定义验证码输入控件,允许根据业务需求控制输入的验证码的数量,比如:
允许输入6位数的验证码 ⬇️⬇️⬇️⬇️⬇️⬇️
允许输入4位数的验证码⬇️⬇️⬇️⬇️⬇️⬇️
所以就手动撸一个吧!🙆♀️
分析
既然需要允许可控制输入的数量,那么需要一个容器且其中每一个ItemView
都应该是动态添加的。所以下面分别对容器
和ItemView
的实现进行简单的分析。
ItemView
- 从上图分析,
ItemView
需要的元素是这么几个,显示数字、下划线、光标 - 需要有这么几种状态,光标闪烁、空白(即还没有被输入到)、数字显示(填充)
容器
- 对于容器来说,首先肯定需要输入,所以需要一个背景透明的
EditText
作为输入 - 需要一个容器,这里选用
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
设置验证码数量。
创作不易,如有帮助一键三连咯🙆♀️。欢迎技术探讨噢!