You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现由4个独立edit text组成的4位验证码输入组件

4位分框验证码组件实现方案

核心实现不需要依赖第三方库,分布局、交互两层处理即可,完全匹配目标效果:

  • 布局部分:横向排列4个等宽输入控件,每个控件限制最大输入长度为1,仅允许输入数字,控件间留固定间距;给输入控件配置状态选择器背景,聚焦、已输入状态显示高亮边框,未激活状态显示默认灰底边框即可。
  • 交互部分只需要处理三类核心逻辑:
    • 输入正向跳转:当前输入框填入1位有效数字后,自动将焦点移到下一个输入框;如果是最后一个输入框,输入完成后自动收起键盘,拼接4位内容触发完成回调。
    • 删除反向回退:当前输入框为空时,用户触发删除操作,自动将焦点移回上一个输入框,同时清空上一个输入框的内容,适配用户快速删改的操作习惯。
    • 焦点优化:任意输入框获得焦点时,自动定位光标到内容末尾,用户可以直接输入覆盖,不需要额外手动删除原有内容。

以下是Android原生的可复用实现代码,其他技术栈(Flutter、RN、Web)逻辑完全一致,替换对应API即可:

1. 自定义控件布局文件 res/layout/view_verify_code.xml

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:gravity="center"
    android:gap="16dp">

    <EditText
        android:id="@+id/et_code_1"
        android:layout_width="48dp"
        android:layout_height="48dp"
        android:inputType="number"
        android:maxLength="1"
        android:gravity="center"
        android:textSize="20sp"
        android:background="@drawable/bg_code_input"/>

    <EditText
        android:id="@+id/et_code_2"
        android:layout_width="48dp"
        android:layout_height="48dp"
        android:inputType="number"
        android:maxLength="1"
        android:gravity="center"
        android:textSize="20sp"
        android:background="@drawable/bg_code_input"/>

    <EditText
        android:id="@+id/et_code_3"
        android:layout_width="48dp"
        android:layout_height="48dp"
        android:inputType="number"
        android:maxLength="1"
        android:gravity="center"
        android:textSize="20sp"
        android:background="@drawable/bg_code_input"/>

    <EditText
        android:id="@+id/et_code_4"
        android:layout_width="48dp"
        android:layout_height="48dp"
        android:inputType="number"
        android:maxLength="1"
        android:gravity="center"
        android:textSize="20sp"
        android:background="@drawable/bg_code_input"/>

</LinearLayout>

2. 输入框背景状态样式 res/drawable/bg_code_input.xml

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_focused="true">
        <shape android:shape="rectangle">
            <solid android:color="#FFFFFF"/>
            <stroke android:width="2dp" android:color="#1677FF"/>
            <corners android:radius="8dp"/>
        </shape>
    </item>
    <item android:state_selected="true">
        <shape android:shape="rectangle">
            <solid android:color="#FFFFFF"/>
            <stroke android:width="2dp" android:color="#1677FF"/>
            <corners android:radius="8dp"/>
        </shape>
    </item>
    <item>
        <shape android:shape="rectangle">
            <solid android:color="#F5F5F5"/>
            <stroke android:width="1dp" android:color="#D9D9D9"/>
            <corners android:radius="8dp"/>
        </shape>
    </item>
</selector>

3. 自定义控件逻辑封装(Kotlin)

class VerifyCodeInputView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null
) : LinearLayout(context, attrs) {

    private val editTexts = mutableListOf<EditText>()
    private var onCodeCompleteListener: ((String) -> Unit)? = null

    init {
        LayoutInflater.from(context).inflate(R.layout.view_verify_code, this, true)
        initEditTextList()
        setInputWatch()
        setDeleteWatch()
        setFocusOpt()
        // 默认第一个输入框获取焦点
        post { editTexts[0].requestFocus() }
    }

    private fun initEditTextList() {
        editTexts.add(findViewById(R.id.et_code_1))
        editTexts.add(findViewById(R.id.et_code_2))
        editTexts.add(findViewById(R.id.et_code_3))
        editTexts.add(findViewById(R.id.et_code_4))
    }

    private fun setInputWatch() {
        editTexts.forEachIndexed { index, editText ->
            editText.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?) {
                    val input = s?.toString().orEmpty()
                    if (input.length == 1) {
                        editText.isSelected = true
                        editText.clearFocus()
                        if (index < editTexts.lastIndex) {
                            editTexts[index + 1].requestFocus()
                        } else {
                            // 拼接完整验证码,触发回调
                            val fullCode = editTexts.joinToString("") { it.text.toString().trim() }
                            onCodeCompleteListener?.invoke(fullCode)
                            // 自动收起键盘
                            val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
                            imm.hideSoftInputFromWindow(editText.windowToken, 0)
                        }
                    } else {
                        editText.isSelected = false
                    }
                }
            })
        }
    }

    private fun setDeleteWatch() {
        editTexts.forEachIndexed { index, editText ->
            editText.setOnKeyListener { _, keyCode, event ->
                if (keyCode == KeyEvent.KEYCODE_DEL 
                    && event.action == KeyEvent.ACTION_DOWN 
                    && editText.text.isEmpty()
                ) {
                    if (index > 0) {
                        val prevEt = editTexts[index - 1]
                        prevEt.setText("")
                        prevEt.requestFocus()
                        return@setOnKeyListener true
                    }
                }
                false
            }
        }
    }

    private fun setFocusOpt() {
        editTexts.forEach { et ->
            et.setOnFocusChangeListener { _, hasFocus ->
                if (hasFocus && et.text.isNotEmpty()) {
                    et.setSelection(et.text.length)
                }
            }
        }
    }

    /**
     * 监听验证码输入完成
     */
    fun setOnCodeCompleteListener(listener: (String) -> Unit) {
        onCodeCompleteListener = listener
    }

    /**
     * 清空已输入的验证码
     */
    fun clear() {
        editTexts.forEach { it.setText("") }
        editTexts[0].requestFocus()
    }
}

使用时直接在业务布局中引入该自定义控件,调用setOnCodeCompleteListener即可拿到用户输入的完整4位验证码,不需要额外处理其他逻辑。如果需要适配6位等其他长度的验证码,只需要在布局中增加对应数量的EditText,在initEditTextList方法中把新增控件加入列表即可,核心逻辑不需要改动。

效果参考:
4位分框验证码输入效果

内容的提问来源于stack exchange,提问作者Mario Muresean

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 11:18:14