How To Make Clickable Words In A TextView? [Kotlin/Android]

Emirhan Kolver
6 min readAug 2, 2022

Hello! Creating Clickable Texts in the TextView has always been one of the toughest challenges I’ve had when developing apps. We will also be able to resolve this significant issue by using SpannableString and some basic Regex knowledge.

Let’s first create a sample text for ourselves and test our results there first.

Our Sample Text should be something I’ve always had trouble with;
“By continuing you agree to the Privacy Policy and EULA.”
By converting 31–45 and 50–55 to ClickableSpan in SpannableString, we can create this sentence with code.

However, the worst part is that when we want to try them in different languages. we must either add indexes specifically for those languages or we can make them easier to find and translate by adding special characters to the beginning and end of these words.

So in this case we have the text
“By continuing you agree to the __Privacy Policy__ and __EULA.__”
We should replace it with In this way, we will generally preserve the order when translated into other languages.

Now all that’s left is to get the positions of the words that start and end with “__”.
Let’s invite Regex to the table, which will do some of the work for us.

Let’s get in to the ‘RegExr’ site and paste our sample text there.

First Look After Paste

The current Regex only catches words starting with capital letters. Let’s delete that and write the Regex we need…

The Final Look

Also remember that we can replace the “__” phrases we have added to the beginning of the words with anything we want.

We’re done with Regex now. Now we will slowly return to Kotlin.
We need to set a name for the class that will do all this work for us.
I’m going to name my own class InteractiveTextMaker.

class InteractiveTextMaker private constructor(
private val textView: TextView,
private val context: Context,
){
// Empty as much as sky
}

We created our class and added the “textView” and “context” parameters to the constructor. Now we will make this class accessible only with the companion function “of”.

class InteractiveTextMaker private constructor(
private val textView: TextView,
private val context: Context,
) {
companion object {
fun of(textView: TextView): InteractiveTextMaker =
InteractiveTextMaker(textView, textView.context)
// Will be used at debugging.
private const val TAG = "InteractiveTextMaker"
}
}

Now we can access our class using the “of” function. Our next step is to make our class more customizable.

private var specialWordSeparator: String = "__"
private var specialTextColor: Int = textView.currentTextColor
private var specialTextHighLightColor: Int = ColorUtils.setAlphaComponent(specialTextColor, 50)
private var underlinedSpecialText: Boolean = false
private var onTextClicked: ((index: Int) -> Unit) = {}

SpecialWordSeprator: We will use it for our special words to distinguish them from other words and to perceive them.
specialTextColor: Color of custom words. For now it is getting default color of TextView.
specialTextHighLightColor: The color of the background that appears only when you tap on our Custom words.
underlinedSpecialText: It makes our special words appear underlined.
onTextClicked: The function that will be executed when our custom words are clicked.

Set Functions

After defining our variables, let’s define a set function for each variable so that we can change them to the values we want later.

fun setSpecialTextSeparator(separator: String): InteractiveTextMaker {
specialWordSeparator = separator
return this
}
fun setSpecialTextUnderLined(boolean: Boolean): InteractiveTextMaker {
underlinedSpecialText = boolean
return this
}

fun setSpecialTextColor(@ColorRes colorId: Int): InteractiveTextMaker {
val color = context.getColor(colorId)
specialTextColor = color
specialTextHighLightColor = ColorUtils.setAlphaComponent(color, 50)
return this
}
fun setSpecialTextHighlightColor(@ColorRes color: Int): InteractiveTextMaker {
specialTextHighLightColor = context.getColor(color)
return this
}
fun setOnTextClickListener(func: (index: Int) -> Unit): InteractiveTextMaker {
onTextClicked = func
return this
}

Final Step — Initialize Function

Now that we’ve defined our work with variables, it’s time to write the main function of our class. First, I’ll set the name of our function to initialize for now. And we should call it after the set functions.

fun initialize() {

}

Now in our function, we need to create the Regex in which we will find the words first. Right after, we need to have Regex find our special words inside the current TextView.

We create a new SpannableString without specialWordSeparator. In this way, we get rid of the “__” characters in the string.

Finally, if the TextView does not contain any specialWords, we print a warning log in Logcat.

val regex = Regex("$specialWordSeparator(.*?)$specialWordSeparator")
val words = regex.findAll(textView.text)
val span = SpannableString(textView.text.replace(Regex(specialWordSeparator), ""))
val actionTextLength = specialWordSeparator.length * 2// A word result has twice of it
if (words.toList().isEmpty()) {
Log.w(
TAG,
"initialize: WARNING No special words has found with separator:'$specialWordSeparator'"
)
return
}

We have now written half of our function. All that’s left is to write the other half. However, this part is a bit complicated. But I will tell you every single detail.

words.forEachIndexed { index:Int, wordResult:MatchResult ->
val startIndex = wordResult.range.first - (actionTextLength * index)
val endIndex =
wordResult.range.last - (actionTextLength * (index + 1)) + 1
span.setSpan(
object : ClickableSpan() {
override fun onClick(p0: View) {
onTextClicked(index)
}

override fun updateDrawState(ds: TextPaint) {
super.updateDrawState(ds)
ds.isUnderlineText = underlinedSpecialText
ds.color = specialTextColor
}
},
startIndex,
endIndex,
0
)
}

“.forEachIndexedBy using the function, we first start reading our results in an indexed way. Each wordResult contains the start and end index of our special word.

startIndex”: Starting index, but just using “wordResult.range.first” variable is not enough for us. Because we deleted our Special characters (“__”) from the TextView in the first place, all the indexes have shifted backwards. We get the right result with a simple multiplication and then subtraction.

Formula: startIndex = wordStartIx — ((“__”).length * listIndex)

endIndex”: End index. For the reasons I have stated above, this constant is also valid. But unlike it, we must add 2 (+1). Otherwise we will not get the correct results.

Formula: “endIndex = wordEndIx — ((“__”).length * (listIndex + 1))+1

span.setSpan()”: Now, after we have finally found the position of our word, all that remains is to give it its qualities. We assign a ClickableSpan to it using the span.setSpan() function. Then we add the onClick and updateDrawState methods to our object.

onClick We add a method that will be called when our word is touched
updateDrawState In the function, we give our word its color and set whether it will be underlined or not.

We’re almost done now!

textView.linksClickable = true
textView.isClickable = true
textView.movementMethod = LinkMovementMethod.getInstance()
textView.text = span
textView.highlightColor = specialTextHighLightColor

By adding these last 5 lines to the end of the initialize function, we make the TextView clickable, change our text and finally change the Highlight color.

Voila!

We finally finished our class! You can view the final version of the class on Github!

It’s Time To Try Now!

Let’s add a TextView in the design somewhere in our project and give the sentence we want to try to the TextView.
I take as an example I’ll try: “How To Make Clickable Text In A TextView?”

Let’s call our class in onCreate and do our experiment

InteractiveTextMaker.of(binding.textView)
.setSpecialTextColor(R.color.purple_500)
.setSpecialTextUnderLined(true)
.setOnTextClickListener {
when (it) {
0 -> Toast.makeText(this, "'Make' has been clicked", Toast.LENGTH_SHORT).show()
1 -> Toast.makeText(this, "'Text' has been clicked", Toast.LENGTH_SHORT).show()
2 -> Toast.makeText(this, "'TextView' has been clicked", Toast.LENGTH_SHORT).show()
}
}
.initialize()
A working example of our class

Github: https://github.com/emirhankolver/InteractiveTextMaker
Thanks for reading!

--

--