如何在文本框未填满时添加占位符?

4
我遇到了以下问题:我需要为手机号码在EditText中输入时实现解决方案。该电话号码应具有不可移动的部分,后四位应以下划线填充,并在用户输入这些数字时将下划线更改为数字,例如: +12345____ -> 输入 6 -> +123456___ 我已经实现了不可移动部分。这是我实现的方法:
binding.etPhoneNumber.filters = arrayOf(InputFilter.LengthFilter(args.phoneNumber?.length ?: 0))

binding.etPhoneNumber.doAfterTextChanged {
            val symbolsLeft = it?.toString()?.length ?: 0
            if (symbolsLeft < phoneNumberUi.length) {
                binding.etPhoneNumber.setText(phoneNumberUi)
                binding.etPhoneNumber.setSelection(symbolsLeft + 1)
            }
        }

但是我现在不明白怎样处理带有下划线的逻辑。我尝试在doAfterTextChanged中追加下划线,例如如果长度差距大于零,则追加n个下划线,其中n为长度差距,但在这种情况下,我无法添加新符号,因为EditText已经被填充,而下划线没有被移除。那么,我该如何解决这个问题呢?非常感谢您的任何帮助!


我认为在你的情况下,你可以将不可编辑的部分与可编辑的部分分开。在从不改变的部分上调用回调函数是没有意义的。 - Dennis Nguyen
@PhúcNguyễn 谢谢你的想法,但我仍然不明白它如何实现。 - Sergei Mikhailovskii
@PhúcNguyễn 噢,是的,我的问题有点不正确。我不明白它如何帮助我解决替换下划线的问题。 - Sergei Mikhailovskii
嘿,我已经考虑过了,搜索了一些东西。我不知道Kotlin,但在Java中,TextWatcher有一个方法afterTextChange(Editable editable),您可以使用可编辑的内容来更新下划线。我认为当您插入字符时,可以调用该方法来删除下划线。尝试阅读此SO以获取更多想法。 - Dennis Nguyen
@PhúcNguyễn 是的,我考虑过这个问题,但问题在于,当edittext填充了下划线,比如 +12345____,而maxLength是10时,afterTextChanged不会被调用。 - Sergei Mikhailovskii
显示剩余2条评论
3个回答

1
您可以移除 LengthFilter 并在 doAfterTextChanged 中检查长度:
    val phoneNumberUi = "+12345"
    val length = 10

    binding.etPhoneNumber.doAfterTextChanged {
        when {
            it == null -> {
            }
            // missing / incomplete prefix
            it.length < phoneNumberUi.length -> {
                it.replace(0, it.length, phoneNumberUi)
            }
            // prefix was edited
            !it.startsWith(phoneNumberUi) -> {
                it.replace(0, phoneNumberUi.length, phoneNumberUi)
            }
            // too short
            it.length < length -> {
                it.append("_".repeat(length - it.length))
            }
            // too long
            it.length > length -> {
                it.replace(length, it.length, "")
            }
            // set the cursor at the first _
            it.indexOf("_") >= 0 -> {
                binding.etPhoneNumber.setSelection(it.indexOf("_"))
            }
        }
    }

注意:这里使用了when,因为每次更改都会立即触发对doAfterTextChanged的递归调用。

1
这种方法具有以下条件分支:
  • 用户添加输入的位置(不可移动的部分或可改变的部分)
  • 输入的字符(数字或退格键)
通过在onTextChanged()中获取输入的字符(数字/退格键)和其索引(第二个参数),并根据它们的值设置新的EditText值来实现。
此外,EditText的值由currentText变量跟踪。因此,我们只能替换一次一个字符,即用户输入的字符,以避免操作整个文本的负担。
您可以通过以下代码注释找到其余的解释:
attachTextWatcher(findViewById(R.id.edittext))

fun attachTextWatcher(editText: EditText) {

    // set the cursor to the first underscore
    editText.setSelection(editText.text.indexOf("_"))

    var currentText = editText.text.toString() // which is "+12345____"

    val watcher: TextWatcher = object : TextWatcher {

        override fun onTextChanged(
            s: CharSequence,
            newCharIndex: Int, // "newCharIndex" is the index of the new entered char
            before: Int,
            count: Int
        ) {

            // New entered char by the user that triggers the TextWatcher callbacks
            val newChar = s.subSequence(newCharIndex, newCharIndex + count).toString().trim()

            /* Stop the listener in order to programmatically
            change the EditText Without triggering the TextWatcher*/
            editText.removeTextChangedListener(this)

            // Setting the new text of the EditText upon examining the user input
            currentText =
                if (newChar.isEmpty()) { // User entered backspace to delete a char
                    if (newCharIndex in 0..5) { // The backspace is pressed in the non-removable part
                        "+12345" + currentText.substring(6)

                    } else { // The backspace is pressed in the changeable part
                        val sb = StringBuilder(currentText)
                        // replace the the number at which backspace pressed with underscore
                        sb.setCharAt(newCharIndex, '_')
                        sb.toString()
                    }

                } else { // User entered a number
                    if (newCharIndex in 0..5) { // The number is entered in the non-removable part
                        // replace the first underscore with the entered number
                        val sb = StringBuilder(currentText)
                        sb.setCharAt(sb.indexOf("_"), newChar[0])
                        sb.toString()

                    } else { // The number is entered in the changeable part
                        if (newCharIndex < 10) { // Avoid ArrayOutOfBoundsException as the number length should not exceed 10
                            val sb = StringBuilder(currentText)
                            // replace the the number at which the number is entered with the new number
                            sb.setCharAt(newCharIndex, newChar[0])
                            sb.toString()
                        } else currentText
                    }
                }

            // Set the adjusted text to the EditText
            editText.setText(currentText)

            // Set the current cursor place
            if (editText.text.contains("_"))
                editText.setSelection(editText.text.indexOf("_"))
            else
                editText.setSelection(editText.text.length)

            // Re-add the listener, so that the EditText can intercept the number by the user
            editText.addTextChangedListener(this)
        }

        override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
        }

        override fun afterTextChanged(s: Editable) {
        }
    }

    editText.addTextChangedListener(watcher)
}

这是我正在测试的布局:


<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <EditText
        android:id="@+id/edittext"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:inputType="number"
        android:maxLength="11"
        android:text="+12345____"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

注意:确保在配置更改时存活currentText的值。
预览


1
我认为PhúcNguyễn的想法很好,将TextViewEditText结合起来以实现您要寻找的内容。您可以将它们作为单独的字段放置在布局中,或者将它们放置在组合视图中。无论哪种方式,效果都是相同的,您都可以实现所需的内容。
您已经知道如何处理字段开头的静态文本。下面我将介绍如何处理下划线,以便输入的字符看起来像是覆盖了下划线。
对于演示,我将一个带有静态文本的TextView与自定义的EditText放在一起。真正有趣的是自定义的EditText。通过自定义视图,覆盖了onDraw()函数以将下划线写成背景的一部分。尽管这些下划线看起来像字段中的任何其他字符,但除了用户输入时逐个覆盖下划线外,它们不能被选中、删除、跳过或以任何方式操作。自定义视图的末尾填充被操纵以提供下划线和文本的空间。

enter image description here

这是自定义视图:

EditTextFillInBlanks.kt

class EditTextFillInBlanks @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : androidx.appcompat.widget.AppCompatEditText(context, attrs, defStyleAttr) {

    // Right padding before we manipulate it
    private var mBaseRightPadding = 0

    // Width of text that has been entered
    private var mTextWidth = 0f

    // Mad length of data that can be entered in characters
    private var mMaxLength = 0

    // The blanks (underscores) that we will show
    private lateinit var mBlanks: String

    // MeasureSpec for measuring width of entered characters.
    private val mUnspecifiedWidthHeight = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)

    init {
        mBaseRightPadding = paddingRight
        doOnTextChanged { text, _, _, _ ->
            measure(mUnspecifiedWidthHeight, mUnspecifiedWidthHeight)
            mTextWidth = measuredWidth.toFloat() - paddingStart - paddingEnd
            updatePaddingForBlanks(text)
        }
        setText("", BufferType.EDITABLE)
    }

    /*
        Make sure that the end padding is sufficient to hold the blanks that we are showing.
        The blanks (underscores) are written into the expanded padding.
     */
    private fun updatePaddingForBlanks(text: CharSequence?) {
        if (mMaxLength <= 0) {
            mMaxLength = determineMaxLen()
            check(mMaxLength > 0) { "Maximum length must be > 0" }
        }
        text?.apply {
            val blanksCount = max(0, mMaxLength - length)
            mBlanks = "_".repeat(blanksCount).apply {
                updatePadding(right = mBaseRightPadding + paint.measureText(this).toInt())
            }
        }
    }

    /*
        Draw the underscores on the canvas. They will appear as characters in the field but
        cannot be manipulated by the user.
     */
    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        if (mBlanks.isNotEmpty()) {
            canvas?.withSave {
                drawText(mBlanks, paddingStart + mTextWidth, baseline.toFloat(), paint)
            }
        }
    }

    fun setMaxLen(maxLen: Int) {
        mMaxLength = maxLen
    }

    private fun determineMaxLen(): Int {
        // Before Lollipop, we can't get max for InputFilter.LengthFilter
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return 0

        return filters.firstOrNull { it is InputFilter.LengthFilter }
            ?.let {
                it as InputFilter.LengthFilter
                it.max
            } ?: 0
    }
}

activity_main.xml

<androidx.constraintlayout.widget.ConstraintLayout 
    android:id="@+id/layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/holo_blue_light"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@android:color/white"
        android:paddingStart="8dp"
        android:paddingTop="8dp"
        android:text="+12345"
        android:textColor="@android:color/black"
        android:textSize="36sp"
        app:layout_constraintBaseline_toBaselineOf="@id/editableSuffix"
        app:layout_constraintEnd_toStartOf="@+id/editableSuffix"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintHorizontal_chainStyle="packed"
        app:layout_constraintStart_toStartOf="@+id/guideline2" />

    <com.example.edittextwithblanks.EditTextFillInBlanks
        android:id="@+id/editableSuffix"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@drawable/edittext_background"
        android:inputType="number"
        android:maxLength="@integer/blankFillLen"
        android:paddingTop="8dp"
        android:paddingEnd="8dp"
        android:textColor="@android:color/black"
        android:textSize="36sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/textView"
        app:layout_constraintTop_toTopOf="parent"
        tools:text="____">

        <requestFocus />
    </com.example.edittextwithblanks.EditTextFillInBlanks>

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guideline2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        app:layout_constraintGuide_begin="92dp" />

</androidx.constraintlayout.widget.ConstraintLayout>

MainActivity.kt

class MainActivity : AppCompatActivity() {
    private val mStaticStart = "+12345"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        if (BuildConfig.VERSION_CODE < Build.VERSION_CODES.P) {
            val maxLen = resources.getInteger(R.integer.blankFillLen)
            findViewById<EditTextFillInBlanks>(R.id.editableSuffix).setMaxLen(maxLen)
        }
    }
}

很可能你可以将静态文本处理并入自定义视图中,以获得完整的解决方案。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接