验证码验证组件可以说是每个app的必备组件,一般情况下我们都需要根据UI设计自定义样式,
但大体都差不多。都是一个横向的布局,然后是4个或者6个子视图用于显示验证码。
1、因此我们可以使用一个横向的LinearLayout去放置子视图。
2、为了响应键盘弹出和接收键盘的输入,我们同时需要一个EditTextView,它的大小应该和LinearLayout一样,并且在它的正下方重叠。
3、正常情况下为了实现这种样式,我们需要再增加一层布局FrameLayout去放置,LinearLayout和EditTextView,但这样不好复用且会增加布局嵌套。
4、还有一种做法就是自定义viewgroup去实现,但要重写测量和布局。
5、我太懒了,不想写测量和布局,索性就复用LinearLayout的测量和布局,然后利用黑科技欺骗一下父LinearLayout,让它认为它没有子EditTextView,最后简单实现下EditTextView的测量和布局,保证它充满父LinearLayout。
class VerifyCodeView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {
/**
* 输入不可见的[AppCompatEditText],且隐藏光标,禁止光标的选中和移动
*/
private class InVisibleEditText(context: Context) : AppCompatEditText(context) {
init {
setTextIsSelectable(false)
isCursorVisible = false
isLongClickable = false
background = null
setTextColor(Color.TRANSPARENT)
}
override fun onSelectionChanged(start: Int, end: Int) {
text?.also {
if (start != it.length || end != it.length) {
setSelection(it.length, it.length)
return
}
}
super.onSelectionChanged(start, end)
}
}
/**
* 当验证码输入控件可输入时,它的方法将在验证码输入完成时调用
*/
fun interface IVerifyCompletedListener {
/**
* 表示输入完成
* @param code 验证码
* @return 表示是否输入完成,ture表示输入完成,用户将不可再更改验证码,直到[reset]被调用,false表示
* 继续处在输入中的状态,还将允许更改验证码
*/
fun onCompleted(code: String): Boolean
}
/**
* 欺骗[LinearLayout]的测量和布局
*/
private var fakerFlag = false
private var isFinishInflated = false
/**
* 0: 禁用
* 1: 输入中
* 2: 输入完成
*/
private var mInputStatus = 0
private lateinit var numViewArray: Array<TextView>
private val inputView = InVisibleEditText(context)
private var mListener: IVerifyCompletedListener? = null
init {
addView(inputView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
inputView.isEnabled = false
inputView.inputType = InputType.TYPE_CLASS_NUMBER
inputView.addTextChangedListener {
if (mInputStatus != 1) {
return@addTextChangedListener
}
numViewArray.forEachIndexed { index, textView ->
textView.text = it?.getOrNull(index)?.toString()
textView.isSelected = it?.length == index
}
if (it?.length == numViewArray.size) {
sendOnCompleted(it.toString())
}
}
}
override fun onViewAdded(child: View?) {
if (isFinishInflated) {
throw IllegalStateException("不支持动态增删child View")
}
super.onViewAdded(child)
}
override fun onViewRemoved(child: View?) {
if (isFinishInflated) {
throw IllegalStateException("不支持动态增删child View")
}
super.onViewRemoved(child)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
fakerFlag = true
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
measureChild(inputView, MeasureSpec.makeMeasureSpec(measuredWidth, MeasureSpec.EXACTLY), heightMeasureSpec)
fakerFlag = false
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
fakerFlag = true
inputView.layout(0, 0, inputView.measuredWidth, inputView.measuredHeight)
super.onLayout(changed, l, t, r, b)
fakerFlag = false
}
override fun getChildAt(index: Int): View {
return if (fakerFlag) super.getChildAt(index + 1) else super.getChildAt(index)
}
override fun getChildCount(): Int {
return if (fakerFlag) super.getChildCount() - 1 else super.getChildCount()
}
override fun onFinishInflate() {
super.onFinishInflate()
isFinishInflated = true
val mChildCount = childCount
if (mChildCount > 1) {
mInputStatus = 1
inputView.isEnabled = true
inputView.filters = arrayOf<InputFilter>(InputFilter.LengthFilter(mChildCount.minus(1)))
val code = buildString {
var invalid = false
numViewArray = Array(mChildCount.minus(1)) { index ->
(getChildAt(index.plus(1)) as TextView).also {
if (!invalid) {
val mText = it.text
if (mText.length == 1) {
append(mText)
} else {
invalid = true
}
}
it.inputType = InputType.TYPE_CLASS_NUMBER
it.maxEms = 1
}
}
}
if (code.isDigitsOnly()) {
inputView.setText(code)
} else {
inputView.setText("")
}
}
}
/**
* 获取焦点,自动弹出软键盘
*/
fun requestInputFocus(): Boolean {
return inputView.requestFocus().also {
// TODO: 弹出软键盘
}
}
/**
* 清空输入的内容,当不处于输入中的状态,调用无效。当输入完成时,如果也需要清空的话可以使用[reset]
*/
fun clear() {
if (mInputStatus != 1) {
return
}
inputView.text = null
}
/**
* 重置输入的状态,当处于禁用状态时,调用无效
* @param clear 表示是否清空输入内容,true表示需要清空,false表示不清空
*/
fun reset(clear: Boolean = true) {
if (mInputStatus == 0) {
return
}
mInputStatus = 1
inputView.isEnabled = true
if (clear) {
inputView.text = null
}
post {
requestInputFocus()
}
}
/**
* 获取当前输入的验证码,当处于禁用状态时,返回空字符串
* @return 返回当前输入的验证码。
*/
fun getVerifyCode(): String {
if (mInputStatus == 0) {
return ""
}
return inputView.text?.toString().orEmpty()
}
/**
* 填充验证码,当不处于输入中的状态或填充的验证码长度超过最大限制,调用无效
* @param code 验证码
*/
fun autoInputFill(code: String) {
if (mInputStatus != 1) {
return
}
if (code.length > numViewArray.size) {
return
}
inputView.setText(code)
}
/**
* 监听输入完成通知
* @param listener 监听回调
*/
fun invokeOnCompleted(listener: IVerifyCompletedListener) {
mListener = listener
}
/**
* 清空[mListener]
*/
fun clearListener() {
mListener = null
}
private fun sendOnCompleted(code: String) {
if (mListener?.onCompleted(code) == true) {
mInputStatus = 2
inputView.isEnabled = false
}
}
}
使用
<VerifyCodeView
android:layout_width="match_parent"
android:layout_height="60dp"
android:background="@color/white"
android:gravity="center_vertical"
app:layout_constraintBottom_toBottomOf="parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:text="1" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:text="2" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:text="3" />
</VerifyCodeView>