Android 短信验证码输入框实现

发布于:2025-09-01 ⋅ 阅读:(19) ⋅ 点赞:(0)

一、设计思路

  1. 使用6个独立输入框组成验证码区域
  2. 每个输入框只能输入一个数字
  3. 输入后自动跳转到下一个输入框
  4. 支持退格键删除并返回上一个输入框
  5. 支持一次性粘贴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">

    <!-- 标题 -->
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="短信验证码"
        android:textSize="20sp"
        android:textColor="#333"
        android:layout_marginBottom="30dp"
        android:textStyle="bold"/>

    <!-- 提示信息 -->
    <TextView
        android: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"/>

    <!-- 验证码输入区域 -->
    <LinearLayout
        android:id="@+id/llCodeContainer"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="30dp"
        android:orientation="horizontal">

        <!-- 6个输入框将通过代码动态添加 -->

    </LinearLayout>

    <!-- 重新发送按钮 -->
    <TextView
        android: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"/>

    <!-- 确定按钮 -->
    <Button
        android: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只是为了占位获取焦点,否则可能弹不出键盘 -->
    <EditText
        android: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.Activity

import 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.ActivityCodeInputBinding


class CodeInputActivity : AppCompatActivity(){
    private lateinit var binding: ActivityCodeInputBinding
    
    private lateinit var llCodeContainer: LinearLayout
    private val codeViews = arrayOfNulls<TextView>(6)
    private lateinit var etHidden: EditText
    private lateinit var btnConfirm: Button
    private lateinit var tvResend: TextView
    private var countdown = 60
    private var countDownTimer: CountDownTimer? = null


    override 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 = params

            textView.setBackgroundResource(R.drawable.code_box_bg)
            textView.gravity = Gravity.CENTER
            textView.textSize = 20f
            textView.setTextColor(Color.BLACK)
            textView.isFocusable = false

            llCodeContainer!!.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 == 6
                btnConfirm!!.isEnabled = allFilled


                // 更新UI状态
                updateBoxesStyle(input.length)
            }
        })

        // 设置隐藏输入框的点击监听,确保点击任何输入框区域都能激活输入
        llCodeContainer!!.setOnClickListener { v: View? ->
            Log.d("BBBBB","激活输入")
            etHidden.requestFocus()
            val imm =
                getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
            imm.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 = 0
                tvResend!!.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>

三、效果图