How can I prevent a user from deleting pre-existing text in an EditText widget?

Viewed 455

I have an EditText with some pre-existing text that MUST NOT be deleted. The user should only be allowed to append text in the view.

The solution I have right now can be seen below, where I check if the new length of the EditText is shorter than the original text. If it is, the user has attempted to delete a character so I just repopulate the view with the original text and move the cursor. The problem with this solution is that the user could just enter text that is longer than the original length, then change the position of the cursor to somewhere within the original text, and finally delete characters.

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    val originalText = "Hello"
    binding.et.setText(originalText)
    
    binding.et.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) {
            s?.let {
                if (s.length < originalText.length) {
                    binding.et.setText(originalText)
                    binding.et.setSelection(originalText.count())
                }
            }
        }

        override fun afterTextChanged(s: Editable?) {}
    })
}

What should I do? I'm open to all ideas.

Thanks!

Edit:

I've also tried combining an EditText and a TextView (as some have suggested) but the layout is difficult to achieve because both the original text and the new text can span multiple lines. My solution above contains one word in the original text but it could be many. So I need to cater for this scenario:

textview and edittext

...where the white box represents the TextView and the red box represents the EditText.

I don't think Bö macht Blau's answer will work either because the prefix is in its own "column".

I think I will have to use Zain's solution with (startsWith()) or create a custom view (though I've never done one of these before).

3 Answers

Please consider @Gabe Sechan advice in OP comments that the EditText alone is not the optimum solution for that.

In case you still need to use EditText then you can change the condition to check that the returned CharSequence of the EditText watcher starts with the original text using startsWith() method

et.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) {
        s?.let {

            if (!s.startsWith(originalText)) {

                if (s.length <= originalText.length) {
                    et.setText(originalText)
                    et.setSelection(originalText.count())
                } else {
                    et.setText(originalText + s.subSequence(originalText.length, s.length))
                    et.setSelection(s.count())
                }
            }
        }
    }

    override fun afterTextChanged(s: Editable?) {}
})

You can use a text field, that's a combination of a TextInputEditText and a TextInputLayout. Then you can set a prefix like so:

<com.google.android.material.textfield.TextInputLayout
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         app:prefixText="@string/prefix_text">

     <com.google.android.material.textfield.TextInputEditText
             android:layout_width="match_parent"
             android:layout_height="wrap_content"/>

 </com.google.android.material.textfield.TextInputLayout>

The prefix will be visible as soon as the TextInputEditText gains focus. Since it is not part of the EditText content, it can't be modified by the users.

Useful links:

This check should work:

if (s.substring(0, 4) == originalText) {
  // update text
}

But consider looking into InputFilter, as a comment suggested.

Related