I opted for a solution, similar to another one submitted, of extending EditText
and modifying onSelectionChanged
. This prevents the user from even entering that region.
Here's what I'm using:
import android.content.Context
import android.text.Editable
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatEditText
class PrefixEditText(context: Context, attrs: AttributeSet?) : AppCompatEditText(context, attrs) {
private var prefix: String? = null
fun setPrefix(prefix: String) {
this.prefix = prefix
setText(prefix)
addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit
override fun afterTextChanged(s: Editable?) {
// Block deleting the prefix
if (s?.startsWith(prefix) == false) {
setText(prefix)
}
}
})
}
override fun onSelectionChanged(selStart: Int, selEnd: Int) {
var newStart = selStart
var newEnd = selEnd
prefix?.length?.let {
newStart = if (selStart < it) it else selStart
newEnd = if (selEnd < it) it else selEnd
}
if (selStart != newStart || selEnd != newEnd) {
setSelection(newStart, newEnd)
} else {
super.onSelectionChanged(selStart, selEnd)
}
}
// Here to conform to EditText's API
// Could also just override getText()
fun getPostText(): Editable? {
return prefix?.let {
Editable.Factory.getInstance().newEditable(text)?.delete(0, it.length)
} ?: run {
text
}
}
// Here for convenience, to avoid instantiating a new Editable, if the text is all you need
fun getPostCharSeq(): CharSequence? {
return prefix?.let {
text?.substring(it.length)
} ?: run {
text
}
}
}
With tests:
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.rule.ActivityTestRule
import com.roosterteeth.roosterteeth.TestActivity
import org.junit.Assert
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class PrefixEditTextTest {
@Rule
@JvmField
var activityRule: ActivityTestRule<TestActivity> = ActivityTestRule(TestActivity::class.java, true, true)
private fun setupView(prefix: String, message: String): PrefixEditText {
val editText = PrefixEditText(activityRule.activity, null)
val lp = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
activityRule.activity.addContentView(editText, lp)
editText.setPrefix(prefix)
editText.append(message)
return editText
}
@Test
fun testSelection() {
activityRule.runOnUiThread {
val prefix = "pre: "
val message = "hello world"
val editText = setupView(prefix, message)
editText.setSelection(0)
Assert.assertEquals(editText.selectionStart, prefix.length)
Assert.assertEquals(editText.selectionEnd, prefix.length)
editText.setSelection(0, editText.length())
Assert.assertEquals(editText.selectionStart, prefix.length)
Assert.assertEquals(editText.selectionEnd, editText.length())
}
}
@Test
fun testGetPostText() {
activityRule.runOnUiThread {
val prefix = "pre: "
val message = "hello world"
val editText = setupView(prefix, message)
Assert.assertEquals(message, editText.getPostText().toString())
// This test is after to make sure that getting the post text did not actually modify the contents
Assert.assertEquals("pre: $message", editText.text.toString())
}
}
@Test
fun testGetPostCharSeq() {
activityRule.runOnUiThread {
val prefix = "pre: "
val message = "hello world"
val editText = setupView(prefix, message)
Assert.assertEquals(message, editText.getPostCharSeq())
// This test is after to make sure that getting the post text did not actually modify the contents
Assert.assertEquals("pre: $message", editText.text.toString())
}
}
}