Kotlin
Complete solution with custom attributes
<declare-styleable name="EditTextView">
<attr name="enableTagging" format="boolean"/>
</declare-styleable>
class EditTextView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : AppCompatEditText(context, attrs, defStyleAttr) {
private var hashtagindex = -1
private var enableTagging:Boolean = false
set(value) {
field = value
invalidate()
requestLayout()
}
init {
context.theme.obtainStyledAttributes(
attrs,
R.styleable.EditTextView, 0, 0).apply {
try {
enableTagging = getBoolean(R.styleable.EditTextView_enableTagging, false)
} finally {
recycle()
}
}
}
override fun onTextChanged(text: CharSequence?, start: Int, lengthBefore: Int, lengthAfter: Int) {
text?.let { handleTagging(text, start, lengthBefore, lengthAfter) }
}
private fun handleTagging(text: CharSequence, start: Int, lengthBefore: Int, lengthAfter: Int) {
if (!enableTagging || text.length <= start) return
if (text[start] == '#') hashtagindex = start
if (text[start] == ' ') hashtagindex = -1
// If hashtag then color the work
if (hashtagindex >= 0) {
val spannableString = getText()
val foregroundSpan = ForegroundColorSpan(ContextCompat.getColor(context, R.color.colorPrimary))
spannableString?.setSpan(foregroundSpan, hashtagindex, text.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
}
This is how you can enable tagging for the EditText
<com.example.xyz.util.editbox.EditTextView
xmlns:custom="http://schemas.android.com/apk/res-auto"
android:id="@+id/et"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
custom:enableTagging="true"
tools:textAppearance="@style/TextAppearance.AppCompat.Moment.Date"
tools:text="Name the "/>
EDIT :
The above mechanism does not work in case you delete any char or your edit text is multiline, the below should work :
private fun handleTagging(text: CharSequence, start: Int, lengthBefore: Int, lengthAfter: Int) {
if (!enableTagging || text.length <= start) return
val formattedText = text.substring(0, start).trim();
val lastWord = formattedText.split(Regex("\\s+")).last()
val tagIndex = if (lastWord.isNotEmpty() && lastWord[0] == '#') formattedText.lastIndexOf('#') else -1
if (tagIndex >= 0) {
val foregroundSpan = ForegroundColorSpan(ContextCompat.getColor(context, R.color.colorPrimary))
getText()?.setSpan(foregroundSpan, tagIndex, start + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
Or if you like extensions as I do, this can be refactored as
fun CharSequence.getTagIndices(toIndex:Int = 0):Pair<Int, Int>? {
val formattedText = substring(0, toIndex).trim()
val lastWord = formattedText.split(Regex("\\s+")).last().trim()
if (!lastWord.startsWith('#') || lastWord.endsWith('#')) return null
val startIndex = formattedText.lastIndexOf('#')
val endIndex = formattedText.length - 1
if (startIndex < 0 || endIndex < 0) return null
return Pair(startIndex, endIndex)
}
private fun handleTagging(text: CharSequence, start: Int, lengthBefore: Int, lengthAfter: Int) {
if (!enableTagging || text.length <= start) return
text.getTagIndices(start + 1)?.let {
val foregroundSpan = ForegroundColorSpan(ContextCompat.getColor(context, R.color.colorPrimary))
getText()?.setSpan(foregroundSpan, it.first, it.second + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}