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

Android OTP输入框连续删除功能失效问题求助

OTP连续删除功能失效问题排查与修复

问题描述

我正在实现OTP账号验证功能,目前连续输入OTP数字的功能正常,但连续删除数字时功能失效,希望得到修复方案。

布局文件代码

<?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="wrap_content"
    android:orientation="vertical">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Enter OTP"
        android:fontFamily="sans-serif-condensed-medium"
        style="@style/TextAppearance.AppCompat.Large"
        android:textStyle="bold"
        android:layout_marginTop="10dp"
        android:layout_gravity="center"/>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="50dp"
        android:layout_marginStart="30dp"
        android:layout_marginEnd="30dp"
        android:orientation="horizontal">

        <EditText
            android:id="@+id/edit1"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:inputType="number"
            android:textColorHint="@color/black"
            android:maxLength="1"
            android:imeOptions="actionPrevious"
            android:textStyle="bold"
            android:backgroundTint="@color/otpEditLineColor"
            style="@style/TextAppearance.AppCompat.Large"
            android:gravity="center"
            android:layout_height="match_parent"/>

        <EditText
            android:id="@+id/edit2"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:inputType="number"
            android:textColorHint="@color/black"
            android:textStyle="bold"
            android:maxLength="1"
            android:imeOptions="actionPrevious"
            android:backgroundTint="@color/otpEditLineColor"
            style="@style/TextAppearance.AppCompat.Large"
            android:gravity="center"
            android:layout_height="match_parent"/>

        <EditText
            android:id="@+id/edit3"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:inputType="number"
            android:textColorHint="@color/black"
            android:textStyle="bold"
            android:maxLength="1"
            android:imeOptions="actionPrevious"
            style="@style/TextAppearance.AppCompat.Large"
            android:gravity="center"
            android:backgroundTint="@color/otpEditLineColor"
            android:layout_height="match_parent"/>

        <EditText
            android:id="@+id/edit4"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:inputType="number"
            android:textColorHint="@color/black"
            android:textStyle="bold"
            android:maxLength="1"
            android:imeOptions="actionPrevious"
            style="@style/TextAppearance.AppCompat.Large"
            android:gravity="center"
            android:backgroundTint="@color/otpEditLineColor"
            android:layout_height="match_parent"/>


        <EditText
            android:id="@+id/edit5"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:inputType="number"
            android:textColorHint="@color/black"
            android:textStyle="bold"
            android:maxLength="1"
            android:imeOptions="actionPrevious"
            android:backgroundTint="@color/otpEditLineColor"
            style="@style/TextAppearance.AppCompat.Large"
            android:gravity="center"
            android:layout_height="match_parent"/>

        <EditText
            android:id="@+id/edit6"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:inputType="number"
            android:textColorHint="@color/black"
            android:textStyle="bold"
            android:backgroundTint="@color/otpEditLineColor"
            style="@style/TextAppearance.AppCompat.Large"
            android:gravity="center"
            android:maxLength="1"
            android:imeOptions="actionPrevious"
            android:layout_height="match_parent"/>

    </LinearLayout>


    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="30dp"
        android:layout_marginBottom="20dp"
        android:layout_gravity="center"
        android:orientation="horizontal">


        <Button
            android:id="@+id/cancel"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Cancel"
            style="@style/RectangleButtonLoginStyle"
            android:layout_gravity="center"/>

        <Button
            android:id="@+id/verify"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Verify"
            style="@style/RectangleButtonLoginStyle"
            android:layout_gravity="center"/>

    </LinearLayout>

</LinearLayout>

当前实现代码

private fun fillInWithOtp(edit1: EditText, edit2: EditText, edit3: EditText, edit4: EditText, edit5: EditText, edit6: EditText) {
        edit1.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) {
                if(s!!.trim().toString().isNotEmpty()){
                    edit2.requestFocus()
                }
            }

            override fun afterTextChanged(s: Editable?) {

            }
        })
        edit2.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) {
                if(s!!.trim().toString().isNotEmpty()){
                    edit3.requestFocus()
                }
            }

            override fun afterTextChanged(s: Editable?) {

            }
        })
        edit3.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) {
                if(s!!.trim().toString().isNotEmpty()){
                    edit4.requestFocus()
                }
            }

            override fun afterTextChanged(s: Editable?) {

            }
        })
        edit4.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) {
                if(s!!.trim().toString().isNotEmpty()){
                    edit5.requestFocus()
                }
            }

            override fun afterTextChanged(s: Editable?) {

            }
        })
        edit5.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) {
                if(s!!.trim().toString().isNotEmpty()){
                    edit6.requestFocus()
                }
            }

            override fun afterTextChanged(s: Editable?) {

            }
        })

        // Set up the key listener for each EditText
        setDeleteKeyListener(edit1, edit2)
        setDeleteKeyListener(edit2, edit3)
        setDeleteKeyListener(edit3, edit4)
        setDeleteKeyListener(edit4, edit5)
        setDeleteKeyListener(edit5, edit6)

    }

    private fun setDeleteKeyListener(currentEdit: EditText, nextEdit: EditText) {
        currentEdit.setOnKeyListener { _, keyCode, event ->
            if (keyCode == KeyEvent.KEYCODE_DEL && event.action == KeyEvent.ACTION_DOWN) {
                currentEdit.text.clear() // Clear the content of current EditText
                if (currentEdit.isFocused && currentEdit.text.isEmpty()) {
                    nextEdit.requestFocus() // Move focus to the next EditText
                }
                true // Return true to indicate that the event is handled
            } else {
                false // Return false if the event is not handled
            }
        }
    }

问题原因分析

你的删除逻辑存在两个核心问题:

  1. 方向搞反:删除时应该把焦点移到前一个EditText,而不是后一个。当前代码里setDeleteKeyListener传入的是currentEdit和nextEdit,调用nextEdit.requestFocus()会导致删除后焦点往后跳,完全不符合预期。
  2. 时机错误:直接在KEYCODE_DEL事件中清空文本并跳转焦点,会和系统默认的删除行为冲突,而且没有判断当前EditText是否已经为空——如果当前框本来就空,删除时应该直接跳转到前一个框,而不是重复清空。

修复后的代码

优化fillInWithOtp方法

调整删除监听的绑定逻辑,把每个EditText和前一个EditText关联:

private fun fillInWithOtp(edit1: EditText, edit2: EditText, edit3: EditText, edit4: EditText, edit5: EditText, edit6: EditText) {
    // 保留原有的输入跳转逻辑
    edit1.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) {
            if(s?.trim().orEmpty().isNotEmpty()) edit2.requestFocus()
        }
        override fun afterTextChanged(s: Editable?) {}
    })
    edit2.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) {
            if(s?.trim().orEmpty().isNotEmpty()) edit3.requestFocus()
        }
        override fun afterTextChanged(s: Editable?) {}
    })
    edit3.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) {
            if(s?.trim().orEmpty().isNotEmpty()) edit4.requestFocus()
        }
        override fun afterTextChanged(s: Editable?) {}
    })
    edit4.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) {
            if(s?.trim().orEmpty().isNotEmpty()) edit5.requestFocus()
        }
        override fun afterTextChanged(s: Editable?) {}
    })
    edit5.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) {
            if(s?.trim().orEmpty().isNotEmpty()) edit6.requestFocus()
        }
        override fun afterTextChanged(s: Editable?) {}
    })
    edit6.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?) {}
    })

    // 绑定删除监听,关联前一个EditText
    setDeleteKeyListener(edit2, edit1)
    setDeleteKeyListener(edit3, edit2)
    setDeleteKeyListener(edit4, edit3)
    setDeleteKeyListener(edit5, edit4)
    setDeleteKeyListener(edit6, edit5)
}

修复setDeleteKeyListener方法

调整逻辑:判断当前EditText是否为空,为空则跳转到前一个;不为空则先让系统完成删除,再判断是否需要跳转:

private fun setDeleteKeyListener(currentEdit: EditText, prevEdit: EditText) {
    currentEdit.setOnKeyListener { _, keyCode, event ->
        if (keyCode == KeyEvent.KEYCODE_DEL && event.action == KeyEvent.ACTION_DOWN) {
            // 当前框为空时,直接跳转到前一个并选中文本
            if (currentEdit.text.isNullOrEmpty()) {
                prevEdit.requestFocus()
                prevEdit.setSelection(prevEdit.text.length)
                return@setOnKeyListener true
            }
            // 当前框不为空时,延迟处理确保文本已删除
            currentEdit.post {
                if (currentEdit.text.isNullOrEmpty()) {
                    prevEdit.requestFocus()
                    prevEdit.setSelection(prevEdit.text.length)
                }
            }
        }
        false // 非删除事件交给系统处理
    }
}

额外优化建议

可以把所有EditText放到一个List中,通过循环绑定监听,避免重复代码:

private fun setupOtpInputs(vararg edits: EditText) {
    val editList = edits.toList()
    editList.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) {
                if(s?.trim().orEmpty().isNotEmpty() && index < editList.size - 1) {
                    editList[index + 1].requestFocus()
                }
            }
            override fun afterTextChanged(s: Editable?) {}
        })
        // 跳过第一个EditText,绑定删除监听
        if (index > 0) {
            setDeleteKeyListener(editText, editList[index - 1])
        }
    }
}

调用时直接传setupOtpInputs(edit1, edit2, edit3, edit4, edit5, edit6)即可,代码更简洁。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.18 06:17:03