Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Easy Spannable on Kotlin
val spanned = spannable{ bold("some") + italic(" formatted") + color(Color.RED, " text") }
val nested = spannable{ bold(italic("nested ")) + url("www.google.com", "text") }
val noWrapping = bold("no ") + sub("wrapping ) + sup("also ") + "works"
text_view.text = spanned + nested + noWrapping
import android.text.Spannable
import android.text.SpannableString
import android.text.TextUtils
import android.text.style.*
fun spannable(func: () -> SpannableString) = func()
private fun span(s: CharSequence, o: Any) = (if (s is String) SpannableString(s) else s as? SpannableString
?: SpannableString("")).apply { setSpan(o, 0, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) }
operator fun SpannableString.plus(s: SpannableString) = SpannableString(TextUtils.concat(this, s))
operator fun SpannableString.plus(s: String) = SpannableString(TextUtils.concat(this, s))
fun bold(s: CharSequence) = span(s, StyleSpan(android.graphics.Typeface.BOLD))
fun italic(s: CharSequence) = span(s, StyleSpan(android.graphics.Typeface.ITALIC))
fun underline(s: CharSequence) = span(s, UnderlineSpan())
fun strike(s: CharSequence) = span(s, StrikethroughSpan())
fun sup(s: CharSequence) = span(s, SuperscriptSpan())
fun sub(s: CharSequence) = span(s, SubscriptSpan())
fun size(size: Float, s: CharSequence) = span(s, RelativeSizeSpan(size))
fun color(color: Int, s: CharSequence) = span(s, ForegroundColorSpan(color))
fun background(color: Int, s: CharSequence) = span(s, BackgroundColorSpan(color))
fun url(url: String, s: CharSequence) = span(s, URLSpan(url))
@Dim-Tim-1963

This comment has been minimized.

Copy link

@Dim-Tim-1963 Dim-Tim-1963 commented Sep 3, 2018

I'm new to Android/Kotlin development, certainly I'm doing something wrong, :) but I cannot make color and background work.
The other modifiers work as expected. color makes text invisible (text color == background color); background doesn't change anything.
While Html.fromHtml() works fine...

EDITED: Finally I found out, what's wrong: color is not RGB, but ARGB, there must be an ALPHA value in it.

@m7mdra

This comment has been minimized.

Copy link

@m7mdra m7mdra commented Dec 26, 2018

hey great job you did there but you fall short to notice that if we want to apply multiple spans to the same text its not possible and the solution was to create couple extra function to receive SpannableString

import android.text.Spannable
import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.text.TextUtils
import android.text.style.*

fun spannable(func: () -> SpannableString) = func()
private fun span(s: CharSequence, o: Any) =
    (if (s is String) SpannableString(s) else s as? SpannableString
        ?: SpannableString("")).apply { setSpan(o, 0, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) }

operator fun SpannableString.plus(s: SpannableString) = SpannableString(TextUtils.concat(this, s))
operator fun SpannableString.plus(s: String) = SpannableString(TextUtils.concat(this, s))

fun bold(s: CharSequence) = span(s, StyleSpan(android.graphics.Typeface.BOLD))
fun bold(s: SpannableString) = span(s, StyleSpan(android.graphics.Typeface.BOLD))
fun italic(s: CharSequence) = span(s, StyleSpan(android.graphics.Typeface.ITALIC))
fun italic(s: SpannableString) = span(s, StyleSpan(android.graphics.Typeface.ITALIC))
fun underline(s: CharSequence) = span(s, UnderlineSpan())
fun underline(s: SpannableString) = span(s, UnderlineSpan())
fun strike(s: CharSequence) = span(s, StrikethroughSpan())
fun strike(s: SpannableString) = span(s, StrikethroughSpan())
fun sup(s: CharSequence) = span(s, SuperscriptSpan())
fun sup(s: SpannableString) = span(s, SuperscriptSpan())
fun sub(s: CharSequence) = span(s, SubscriptSpan())
fun sub(s: SpannableString) = span(s, SubscriptSpan())
fun size(size: Float, s: CharSequence) = span(s, RelativeSizeSpan(size))
fun size(size: Float, s: SpannableString) = span(s, RelativeSizeSpan(size))
fun color(color: Int, s: CharSequence) = span(s, ForegroundColorSpan(color))
fun color(color: Int, s: SpannableString) = span(s, ForegroundColorSpan(color))
fun background(color: Int, s: CharSequence) = span(s, BackgroundColorSpan(color))
fun background(color: Int, s: SpannableString) = span(s, BackgroundColorSpan(color))
fun url(url: String, s: CharSequence) = span(s, URLSpan(url))
fun url(url: String, s: SpannableString) = span(s, URLSpan(url))
fun normal(s: CharSequence) = span(s, SpannableString(s))
fun normal(s: SpannableString) = span(s, SpannableString(s))

this way it possible to nest spanns

spannable {
            italic(underline(bold(size(2f, color(Color.RED, "Red Bold Color")))))
        }

thanks.

@karrel84

This comment has been minimized.

Copy link

@karrel84 karrel84 commented Feb 14, 2019

nice i love it!

@Danielvgftv

This comment has been minimized.

Copy link

@Danielvgftv Danielvgftv commented Jun 19, 2019

This is amazing ♥️

@radoyankov

This comment has been minimized.

Copy link
Owner Author

@radoyankov radoyankov commented Jun 19, 2019

This is amazing ❤️

Glad you like it ❤️

@Scorpio93

This comment has been minimized.

Copy link

@Scorpio93 Scorpio93 commented Jun 28, 2019

Thank you so much for your great idea.
My solution with using Kotlin extensions.

private const val EMPTY_STRING = ""
private const val FIRST_SYMBOL = 0

fun spannable(func: () -> SpannableString) = func()

private fun span(s: CharSequence, o: Any) = getNewSpannableString(s).apply {
    setSpan(o, FIRST_SYMBOL, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}

private fun getNewSpannableString(charSequence: CharSequence): SpannableString{
    return if (charSequence is String){
        SpannableString(charSequence)
    }else{
        charSequence as? SpannableString ?: SpannableString(EMPTY_STRING)
    }
}

operator fun SpannableString.plus(s: CharSequence) = SpannableString(TextUtils.concat(this, "", s))

fun CharSequence.makeSpannableString() = span(this, Spanned.SPAN_COMPOSING)
fun CharSequence.makeBold() = span(this, StyleSpan(BOLD))
fun CharSequence.makeItalic() = span(this, StyleSpan(ITALIC))
fun CharSequence.makeUnderline() = span(this, UnderlineSpan())
fun CharSequence.makeStrike() = span(this, StrikethroughSpan())
fun CharSequence.makeSuperscript() = span(this, SuperscriptSpan())
fun CharSequence.makeSubscript() = span(this, SubscriptSpan())
fun CharSequence.makeAnotherSize(size : Float) = span(this, RelativeSizeSpan(size))
fun CharSequence.makeAnotherColor(color : Int) = span(this, ForegroundColorSpan(color))
fun CharSequence.makeAnotherBackground(color : Int) = span(this, BackgroundColorSpan(color))
fun CharSequence.makeUrl(url : String) = span(this, URLSpan(url))

Using example:

textView.text = spannable{ 
    "Example".makeSpannableString()
             .makeBold()
             .makeItalic()
             .makeUnderline()
}
@HxBreak

This comment has been minimized.

Copy link

@HxBreak HxBreak commented Nov 30, 2020

nice job

@gonztirado

This comment has been minimized.

Copy link

@gonztirado gonztirado commented Mar 26, 2021

Thanks for all you guys! I'm using @Scorpio93 implementation, very nice job!

I also added a two more extensions for ClickableSpan:

/** You will need set movementMethod = LinkMovementMethod.getInstance() in the TextView to allow clicking on the span */
fun CharSequence.clickable(listener: View.OnClickListener) : SpannableString {
    val clickSpan = object : ClickableSpan() {
        override fun onClick(widget: View) {
            listener.onClick(widget)
        }
    }
    return span(this, clickSpan)
}
/** You will need set movementMethod = LinkMovementMethod.getInstance() in the TextView to allow clicking on the span */
fun CharSequence.clickableWithoutUnderline(listener: View.OnClickListener) : SpannableString {
    val clickSpan = object : ClickableSpan() {
        override fun onClick(widget: View) {
            listener.onClick(widget)
        }
        override fun updateDrawState(ds: TextPaint) {
            super.updateDrawState(ds)
            ds.isUnderlineText = false
        }
    }
    return span(this, clickSpan)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment