Working with spans in Android

One of the most used cases for spannables is UrlSpan

Example 1

<string name="temrs_of_service">
<![CDATA[By continuing, you agree to the <a href="terms_of_service_url">Terms of Service</a> and <a href="privacy_policy_url">Privacy Policy</a>.]]>
</string>
val termsOfService = context.getString(R.string.terms_of_service)
val privacyPolicy = context.getString(R.string.privacy_policy)

val phrase = context.getString(R.string.terms, termsOfService, privacyPolicy)
val termsOfServicesStart = phrase.indexOf(termsOfService)
val privacyPolicyStart = phrase.indexOf(privacyPolicy)
val spannable = SpannableStringBuilder(phrase)
spannable.setSpan(URLSpan("terms_of_service_url"), termsOfServicesStart, termsOfServicesStart + termsOfService.length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)spannable.setSpan(URLSpan("privacy_policy_url"), privacyPolicyStart, privacyPolicyStart + privacyPolicy.length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
context.resources.getSpannable(R.string.terms,
context.getString(R.string.terms_of_service) to listOf(URLSpan("terms_of_service_url")),
context.getString(R.string.privacy_policy) to listOf(URLSpan("privacy_policy_url")))

Example 2

val firstSent = "This is the first sentence."
val
secondSent = "This is the second sentence."
val
thirdSent = "This is the third sentence."

var
index = 0
val spannable = SpannableStringBuilder()
spannable.append(firstSent)
spannable.setSpan(clickableSpan { context.showToast("clicked on first") }, index, index + firstSent.length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
spannable.setSpan(ForegroundColorSpan(ContextCompat.getColor(context, R.color.colorPrimary)), index, index + firstSent.length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
spannable.setSpan(AbsoluteSizeSpan(context.resources.getDimensionPixelSize(R.dimen.first_size)), index, index + firstSent.length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
spannable.append("\n")

index = spannable.length
spannable.append(secondSent)
spannable.setSpan(clickableSpan { context.showToast("clicked on second") }, index, index + secondSent.length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
spannable.setSpan(ForegroundColorSpan(ContextCompat.getColor(context, R.color.colorPrimaryDark)), index, index + secondSent.length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
spannable.setSpan(AbsoluteSizeSpan(context.resources.getDimensionPixelSize(R.dimen.second_size)), index, index + secondSent.length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
spannable.append("\n")

index = spannable.length
spannable.append(thirdSent)
spannable.setSpan(clickableSpan { context.showToast("clicked on third") }, index, index + thirdSent.length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
spannable.setSpan(ForegroundColorSpan(ContextCompat.getColor(context, R.color.colorAccent)), index, index + thirdSent.length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
spannable.setSpan(AbsoluteSizeSpan(context.resources.getDimensionPixelSize(R.dimen.third_size)), index, index + thirdSent.length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)

return spannable
val firstSent = "This is the first sentence."
val
secondSent = "This is the second sentence."
val
thirdSent = "This is the third sentence."

val
spannable = SpannableStringBuilder()
spannable.append(context, firstSent, R.color.colorPrimary, R.dimen.first_size) { context.showToast("clicked on first") }
spannable.append(context, secondSent, R.color.colorPrimaryDark, R.dimen.second_size) { context.showToast("clicked on second") }
spannable.append(context, thirdSent, R.color.colorAccent, R.dimen.third_size) { context.showToast("clicked on third") }
return
spannable
fun SpannableStringBuilder.append(
context: Context,
text: CharSequence,
@ColorRes textColorRes: Int,
@DimenRes textSizeRes: Int,
clickAction: () -> Unit) {
val index = length
append(text)
setSpan(clickableSpan(clickAction), index, index + text.length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
setSpan(ForegroundColorSpan(ContextCompat.getColor(context, textColorRes)), index, index + text.length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
setSpan(AbsoluteSizeSpan(context.resources.getDimensionPixelSize(textSizeRes)), index, index + text.length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
}
return SpannableStringCreator()
.append("This is the first sentence.", context.resSpans {
color(R.color.colorPrimary)
size(R.dimen.first_size)
click { context.showToast("clicked on first") }
})
.appendLn("This is the second sentence.", context.resSpans {
color(R.color.colorPrimaryDark)
size(R.dimen.second_size)
click { context.showToast("clicked on second") }
})
.appendLn("This is the third sentence.", context.resSpans {
color(R.color.colorAccent)
size(R.dimen.third_size)
click { context.showToast("clicked on third") }
})
.toSpannableString()

Solution

inline fun Context.resSpans(options: ResSpans.() -> Unit) =
ResSpans(this).apply(options)
fun Resources.getSpannable(@StringRes id: Int, vararg spanParts: Pair<Any, Iterable<Any>>): CharSequence {
val resultCreator = SpannableStringCreator()
Formatter(
SpannableAppendable(resultCreator, *spanParts),
configuration.locale)
.format(getString(id), *spanParts.map { it.first }.toTypedArray())
return resultCreator.toSpannableString()
}
fun Resources.getText(@StringRes id: Int, vararg formatArgs: Any?) =
getSpannable(id, *formatArgs.filterNotNull().map { it to emptyList<Any>() }.toTypedArray())

Afterwords

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Michael Spitsin

Michael Spitsin

650 Followers

Love being creative to solve some problems with an simple and elegant ways