一、设计思路
- 使用6个独立输入框组成验证码区域
- 每个输入框只能输入一个数字
- 输入后自动跳转到下一个输入框
- 支持退格键删除并返回上一个输入框
- 支持一次性粘贴6位验证码
二、代码实现
2.1 xml布局
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"android:padding="20dp"android:gravity="center"android:background="#FFF"><!-- 标题 --><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="短信验证码"android:textSize="20sp"android:textColor="#333"android:layout_marginBottom="30dp"android:textStyle="bold"/><!-- 提示信息 --><TextViewandroid:id="@+id/tvTips"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="验证码已发送至 138****8888"android:textSize="14sp"android:textColor="#666"android:layout_marginBottom="20dp"/><!-- 验证码输入区域 --><LinearLayoutandroid:id="@+id/llCodeContainer"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginBottom="30dp"android:orientation="horizontal"><!-- 6个输入框将通过代码动态添加 --></LinearLayout><!-- 重新发送按钮 --><TextViewandroid:id="@+id/tvResend"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="60秒后重新发送"android:textSize="14sp"android:textColor="#FFD700"android:layout_marginBottom="20dp"/><!-- 确定按钮 --><Buttonandroid:id="@+id/btnConfirm"android:layout_width="200dp"android:layout_height="45dp"android:text="确定"android:textColor="#FFF"android:background="@drawable/btn_confirm_bg"android:textSize="16sp"android:enabled="false"/><!-- 隐藏的输入框用于接收输入 3dp只是为了占位获取焦点,否则可能弹不出键盘 --><EditTextandroid:id="@+id/etHidden"android:layout_width="3dp"android:layout_height="3dp"android:inputType="number"android:maxLength="6"android:cursorVisible="false"android:background="@null"/></LinearLayout>
2.2 Activity 代码
package com.vc.psclient.Activityimport android.graphics.Color
import android.os.Bundle
import android.os.CountDownTimer
import android.text.Editable
import android.text.TextWatcher
import android.util.Log
import android.view.Gravity
import android.view.KeyEvent
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.Button
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import com.vc.psclient.R
import com.vc.psclient.databinding.ActivityCodeInputBindingclass CodeInputActivity : AppCompatActivity(){private lateinit var binding: ActivityCodeInputBindingprivate lateinit var llCodeContainer: LinearLayoutprivate val codeViews = arrayOfNulls<TextView>(6)private lateinit var etHidden: EditTextprivate lateinit var btnConfirm: Buttonprivate lateinit var tvResend: TextViewprivate var countdown = 60private var countDownTimer: CountDownTimer? = nulloverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_code_input)initViews()setupCodeInput()startCountdown()}private fun initViews() {llCodeContainer = findViewById<LinearLayout>(R.id.llCodeContainer)etHidden = findViewById<EditText>(R.id.etHidden)btnConfirm = findViewById<Button>(R.id.btnConfirm)tvResend = findViewById<TextView>(R.id.tvResend)// 动态创建6个输入框for (i in 0 until codeViews.size) {val textView = TextView(this)val params = LinearLayout.LayoutParams(dpToPx(40), dpToPx(40))params.setMargins(dpToPx(8), 0, dpToPx(8), 0)textView.layoutParams = paramstextView.setBackgroundResource(R.drawable.code_box_bg)textView.gravity = Gravity.CENTERtextView.textSize = 20ftextView.setTextColor(Color.BLACK)textView.isFocusable = falsellCodeContainer!!.addView(textView)codeViews[i] = textView}btnConfirm.setOnClickListener(View.OnClickListener { v: View? ->// 获取验证码val code = StringBuilder()for (codeView in codeViews) {code.append(codeView?.text.toString())}// 验证验证码if (code.length == 6) {verifyCode(code.toString())}})tvResend.setOnClickListener(View.OnClickListener { v: View? ->if (tvResend.text.toString() == "重新发送") {resendCode()startCountdown()}})}private fun setupCodeInput() {etHidden!!.addTextChangedListener(object : TextWatcher {override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}override fun afterTextChanged(s: Editable) {var input = s.toString()if (input.length > 6) {input = input.substring(0, 6)etHidden.setText(input)etHidden.setSelection(6)return}// 更新显示for (i in 0 until codeViews.size) {if (i < input.length) {codeViews[i]!!.text = input[i].toString()} else {codeViews[i]!!.text = ""}}// 检查是否全部填满val allFilled = input.length == 6btnConfirm!!.isEnabled = allFilled// 更新UI状态updateBoxesStyle(input.length)}})// 设置隐藏输入框的点击监听,确保点击任何输入框区域都能激活输入llCodeContainer!!.setOnClickListener { v: View? ->Log.d("BBBBB","激活输入")etHidden.requestFocus()val imm =getSystemService(INPUT_METHOD_SERVICE) as InputMethodManagerimm.showSoftInput(etHidden, InputMethodManager.SHOW_IMPLICIT)
// EditTextUtils.showSoftInputFromWindow(etHidden)}// 处理删除键etHidden.setOnKeyListener { v: View?, keyCode: Int, event: KeyEvent ->if (keyCode == KeyEvent.KEYCODE_DEL && event.action == KeyEvent.ACTION_DOWN) {val currentText = etHidden.text.toString()if (currentText.length > 0) {etHidden.setText(currentText.substring(0, currentText.length - 1))etHidden.setSelection(etHidden.text.length)return@setOnKeyListener true}}false}}private fun updateBoxesStyle(length: Int) {for (i in 0 until codeViews.size) {if (i == length) {// 当前焦点位置codeViews[i]!!.setBackgroundResource(R.drawable.code_box_bg_focused)} else {codeViews[i]!!.setBackgroundResource(R.drawable.code_box_bg)}}}private fun startCountdown() {countDownTimer = object : CountDownTimer(60000, 1000) {override fun onTick(millisUntilFinished: Long) {countdown = (millisUntilFinished / 1000).toInt()tvResend!!.text = countdown.toString() + "秒后重新发送"tvResend!!.setTextColor(Color.GRAY)tvResend!!.isClickable = false}override fun onFinish() {countdown = 0tvResend!!.text = "重新发送"tvResend?.setTextColor(-0x2900)tvResend?.isClickable = true}}.start()}private fun resendCode() {// 实现重新发送验证码的逻辑Toast.makeText(this, "验证码已重新发送", Toast.LENGTH_SHORT).show()etHidden!!.setText("")}private fun verifyCode(code: String) {// 实现验证码验证逻辑Toast.makeText(this, "正在验证: $code", Toast.LENGTH_SHORT).show()// 这里通常是网络请求验证}private fun dpToPx(dp: Int): Int {return (dp * resources.displayMetrics.density).toInt()}override fun onDestroy() {super.onDestroy()countDownTimer?.cancel()}}
2.3 xml选中效果布局
!-- res/drawable/code_box_bg.xml -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"><solid android:color="#FFF" /><stroke android:width="1dp" android:color="#DDD" /><corners android:radius="4dp" />
</shape><!-- res/drawable/code_box_bg_focused.xml -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"><solid android:color="#FFF" /><stroke android:width="2dp" android:color="#FFD700" /><corners android:radius="4dp" />
</shape><!-- res/drawable/btn_confirm_bg.xml -->
<selector xmlns:android="http://schemas.android.com/apk/res/android"><item android:state_enabled="true"><shape><solid android:color="#FFD700" /><corners android:radius="4dp" /></shape></item><item android:state_enabled="false"><shape><solid android:color="#DDD" /><corners android:radius="4dp" /></shape></item>
</selector>
三、效果图
