• Spanantastic TEXT DRIps with Spans
  • Originally written by Florina Muntenescu
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: wzasd
  • Proofreader: luochen1992

To style your text on Android, run Spans! Use span to change the color of some characters so that they can be clicked, resized text, or even drawn with custom bullet points. Spans can change the TextPaint property, draw on a Canvas, and even change your text layout and affect line-height elements. A Span is a tag object that can be attached to and detached from text, and they can be applied to entire paragraphs or portions of text.

Spans Let’s learn how to use SPANS, what spans are available to us, how to easily create spans of your own, and how to test them.

Set text styles in Android

Android provides several methods for setting text styles:

  • Single Style — Styles are used for the entire text displayed by the TextView
  • Multiple styles – Multiple styles for text, characters, and paragraphs

Single styling means styling the entire contents of a TextView using XML attributes or styles and themes. The XML approach is a simpler solution, but it doesn’t change the style in the middle of the text. For example, by setting textStyle= “bold,” the entire text becomes bold, and you can’t just define specific characters as bold.

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textSize="32sp"
    android:textStyle="bold"/>
Copy the code

Multiple styles means multiple styles in the same text. For example, set one word to italic and another to bold. Multiple modes can use HTML tags, spans on a canvas, or apply text styles by handling custom text drawings.

Left: Single style text. TextView Sets textSize= “32SP” and textStyle= “bold”. Right: various pieces of text. Text Settings ForegroundColorSpan, StyleSpan(ITALIC), ScaleXSpan(1.5F), and StrikethroughSpan.

HTML tags are a solution to simple styling problems, such as making text bold, italic, and even bullet points. To set the text that contains the HTML tag, call the html.fromhtml method. In the HTML engine, the HTML format is converted to SPANS. Note that Html classes do not support all Html tags and CSS styles, such as making bullet Points a different color.

val text = "My text <ul><li>bullet one</li><li>bullet two</li></ul>"
myTextView.text = Html.fromHtml(text)
Copy the code

When you find a style requirement that the platform does not support, you can manually draw text on the canvas, such as the need to write a curved text.

Spans allows you to customize various pieces of text for your implementation in a more subtle way. For example, you can define Bullet Point by using BulletSpan. You can also customize the target text margins and colors. Starting with Android P, you can even set the radius of Bullet Point.

val spannable = SpannableString("My text \nbullet one\nbullet two")

spannable.setSpan(
    BulletPointSpan(gapWidthPx, accentColor),
    /* start index */ 9./* end index */ 18,
    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)

spannable.setSpan(
     BulletPointSpan(gapWidthPx, accentColor),
     /* start index */ 20./* end index */ spannable.length,
     Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)

myTextView.text = spannable
Copy the code

Left: Using HTML tags. Middle: Use BulletSpan to set the default bullet size. Right: Using BulletSpan on Android P or custom implementation.

You can combine single styles and styles. You can think of the style in which you set the TextView as the “base” style. Spans text style applies “on top” of the spans base style, and spans the base style. For example, when setting textColor= “@color.blue” to TextView and setting ForegroundColorSpan(color.pink) to the first 4 characters of the text, the first 4 characters will be PINK, which is controlled by SPAN. The rest of it has the TextView property to set it up.

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textColor="@color/blue"/> We are going to get some styling styling done on the calendar. We are going to get some styling styling done on the calendar.0.4, 
    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)

myTextView.text = spannable
Copy the code

Spans use TextView using a combination of XML and text.

Spans in applications

When spans, you use one of the following classes: SpannedString, SpannableString, or SpannableStringBuilder. The difference between them is whether the text or tag objects are mutable and they use internal structures: SpannedString and SpannableString are linear ways to save spans. SpannableStringBuilder uses interval trees to do this.

Here’s how to determine which Spans to use:

  • onlyRead rather than setText or Spans? ->SpannableString
  • Set text and SPANS? ->SpannableStringBuilder
  • Set up aSpans a small amount of text(< ~ 10)? ->SpannableString
  • Set up aSpans a large amount of text(> ~ 10)? ->SpannableStringBuilder

For example, if the text you use doesn’t change, but to append it to the text SPANS, you should use SpannableString.

╔ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ╦ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ╦ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ╗ ║ ║ Class * * * * * * Mutable Text ║ * * * * Mutable Markup ║ * * ╠ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ╬ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ╬ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ╣ ║ SpannedString ║ no ║ no ║ ║ SpannableString ║ no ║ Yes ║ ║ SpannableStringBuilder ║ yes ║ yes ║ ╚ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ╩ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ╩ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ╝Copy the code

All of these classes inherit from Spanned’s interface, but with mutable tags (SpannableString and SpannableStringBuilder) also inherit from Spannable.

Spanned -> Immutable text with immutable marks

Spannable (inherited from Spanned) -> Immutable text with mutable markup

Call setSpan(Object What, int Start, int end, int flags) from Spannable objects. The What object is the tag that will be indexed from start to end in the text. This flag indicates whether the span should be inserted at the point where it expands to include a starting or ending point. Regardless of where the mark is made, the span automatically expands as long as the text is inserted at a position greater than the start and less than the end.

For example, setting a ForegroundColorSpan can be done like this:

Val Spannable = SpannableStringBuilder(" Text is spantastic!" ) spannable.setSpan( ForegroundColorSpan(Color.RED),8.12, 
     Spannable.SPAN_EXCLUSIVE_INCLUSIVE)
Copy the code

Because span uses the SPAN_EXCLUSIVE_INCLUSIVE flag, when text is inserted at the end of the text, it will be expanded to include new text.

Val Spannable = SpannableStringBuilder(" Text is spantastic!" ) spannable.setSpan( ForegroundColorSpan(Color.RED),/* start index */ 8./* end index */ 12, 
     Spannable.SPAN_EXCLUSIVE_INCLUSIVE)

spannable.insert(12", "(& fon))Copy the code

Left: Text using ForegroundColorSpan. Right: Text using ForegroundColorSpan and Spannable.SPAN_EXCLUSIVE_INCLUSIVE.

If the SPAN is set to the spannable.SPAN_exclusive_EXCLUSIVE flag, text inserted at the end of the SPAN will not modify the end of the span flag.

More SPANS can be composed and attached to the same piece of text. For example, both bold and red text can be constructed like this:

Val spannable = SpannableString(" Text is spantastic! ) spannable.setSpan( ForegroundColorSpan(Color.RED),8.12, 
     Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)

spannable.setSpan(
     StyleSpan(BOLD), 
     8, spannable.length, 
     Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
Copy the code

Spans: ForegroundColorSpan(color.red) and StyleSpan(BOLD).

The framework of spans

The Android framework defines several interfaces and abstract classes to examine when measuring and rendering graphics. These classes have methods that allow span access to TextPaint or Canvas objects.

The Android framework provides more than 20 spans in the Android.text.style package, subclassing major interfaces and abstract classes. We can classify them in several ways:

  • Depending on whether SPAN just changes the appearance or changes the measurement/layout of the text
  • Depending on whether they affect the level of text in a character or paragraph

Span type: characters and paragraphs, appearance and metrics.

Appearance and metrics affect span, respectively

The first group of classification effects character-level texts can modify their appearance: text or background colors, underscores, strikeout lines, etc., will be redrawn without causing the text to be rearranged. These spans implement UpdateAppearance and inherit CharacterStyle. The CharacterStyle subclass defines how to access text by providing updated TextPaint.

The span that affects the appearance.

Metrics affect SPANS modify text metrics and layout, so objects observing spans remeasure the text to make it easier to layout and render properly.

For example, a span that affects text size will need to be remeasured, laid out, and drawn. These Spans typically extend to the MetricAffectingSpan class. This abstract class allows subclasses to determine how to measure text by accessing TextPaint. Because MetricAffectingSpan inherits CharacterSpan, subclasses affect the appearance of character-level text.

Span of impact metrics.

You might always want to recreate a CharSequence with text and markup and call TextView.settext (CharSequence). But this will result in remeasuring, redrawing the layout, and creating additional objects every time. To reduce the performance cost, use TextView.settext (Spannable, bufferType.spannable). Then, when you need to modify the span, Retrieve the Spannable object from TextView by casting textView.gettext () to Spannable. We’ll cover the principle behind TextView.settext in more detail later, as well as the different performance optimizations

For example, consider the following Spannable object and retrieve it like this:

Val spannableString = spannableString (" Spantastic text ")// setting the text as a Spannable
textView.setText(spannableString, BufferType.SPANNABLE)

// later getting the instance of the text object held 
// by the TextView
// this can can be cast to Spannable only because we set it as a
// BufferType.SPANNABLE before
val spannableText = textView.text as Spannable
Copy the code

Now, when we set the span in spannableText, we don’t need to call TextView.settext again, because we are directly modifying the CharSequence object instance held by the textView.

Here’s what happens when we set different spans:

Case 1: Span that affects appearance

spannableText.setSpan(
     ForegroundColorSpan(colorAccent), 
     0.4, 
     Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
Copy the code

Instead of textView.onLayout, we called textView.onLayout because we attached a span that affects the look and feel. The text will be redrawn, but the width and height will be the same.

Case 2: Span of impact metrics

spannableText.setSpan(
     RelativeSizeSpan(2f), 
     0.4, 
     Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
Copy the code

Because RelativeSizeSpan can change the size, width, and height of the text (for example, a specific word may appear on the next line, but the size of the TextView is not modified). TextView needs to compute the new size, so onMeasure and onLayout will be called.

Left: ForegroundColorSpan – the span that affects the appearance. Right: RelativeSizeSpan – span of impact metrics.

Affects Spans of characters and paragraphs

Span can change not only character-level text, updating elements such as background color, style, or size, but also paragraph-level text, changing the alignment or margins of entire text blocks. Spans inherits CharacterStyle or implements ParagraphStyle, depending on the style you want. Spans that inherit ParagraphStyle must append from the first character to the last character of a single paragraph, or Spans won’t be shown. On Android, paragraphs are defined in terms of (\n) characters.

On Android, paragraphs are defined in terms of (\n) characters.

Spans of Impact paragraphs.

For example, something like BackgroundColorSpan CharacterStyle Span can be attached to any character in the text. Here we append it to characters 5 through 8:

Val spannable = SpannableString(" Text is\nspantastic ") spannable.setspan (BackgroundColorSpan(color), 5, 8, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)Copy the code

A ParagraphStyle span, like QuoteSpan, can only be appened at the beginning of a paragraph, otherwise the margins for the text don’t work. For example, “Text is\nspantastic” contains line breaks in the 8th character of the Text, so we can append QuoteSpan to it, and just the paragraph from there will be formatted. If we append span to any position other than 0 or 8, the text will not be set to the target style.

spannable.setSpan(
    QuoteSpan(color), 
    8, text.length, 
    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
Copy the code

Left: BackgroundColorSpan – the span that affects the appearance. Right: QuoteSpan – Affects the span of the paragraph.

Create a custom SPANS

When implementing your own SPAN, you need to determine whether the SPAN needs to affect character or paragraph text, and whether it affects the layout or the appearance of the text. But before you write your own implementation from scratch, check to see if you can use the functionality provided in the SPAN framework.

TL; DR:

  • inCharacter levelModify text ->CharacterStyle
  • inThe paragraph levelModify text ->ParagraphStyle
  • Modify theText appearance -> UpdateAppearance
  • Modify theText metric -> UpdateLayout

If we need to implement a span, allow the text to increase in size proportionate, like with a RelativeSizeSpan, and set the text color, like ForegroundColorSpan. To do this, we can inherit from RelativeSizeSpan, and since it provides the updateDrawState and updateMeasureState callbacks, we can override the draw state callback and set the color of the TextPaint.

class RelativeSizeColorSpan( @ColorInt private val color: Int, size: Float ) : RelativeSizeSpan(size) { override fun updateDrawState(textPaint: TextPaint?) { super.updateDrawState(ds) textPaint? .color = color } }Copy the code

Tip: You can get the same effect by setting both RelativeSizeSpan and ForegroundColorSpan within the same text.

Test your implementation of custom SPANS

Testing SPANS means checking to see if you actually modified TextPaint as you intended or drew the right element on your Canvas. As an example, consider a custom implementation of a SPAN that adds a bullet Point with size and color and a gap between the left margin and bullet Point to a paragraph. Please refer to android-text sample. To test this class, an AndroidJUnit test class is implemented to check if it works as expected:

  • Draws a circle of a specific size on the canvas
  • If the span is not attached to the text, nothing is drawn
  • Set the correct margins based on the constructor parameter values

Test the Canvas interaction by emulating a Canvas, passing the simulated object to the drawLeadingMargin method, and verifying that the method called has the correct parameters.

val canvas = mock(Canvas::class.java)
val paint = mock(Paint::class.java)
val text = SpannableString("text")

@Test fun drawLeadingMargin() {
    val x = 10
    val dir = 15
    val top = 5
    val bottom = 7
    val color = Color.RED

    // Given a span that is set on a text
    val span = BulletPointSpan(GAP_WIDTH, color)
    text.setSpan(span, 0, 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)

    // When the leading margin is drawn
    span.drawLeadingMargin(canvas, paint, x, dir, top, 0, bottom,
            text, 0, 0, true, mock(Layout::class.java))

    // Check that the correct canvas and paint methods are called, 
    //in the correct order
    val inOrder = inOrder(canvas, paint)

    // bullet point paint color is the one we set
    inOrder.verify(paint).color = color
    inOrder.verify(paint).style = eq<Paint.Style>(Paint.Style.FILL)

    // a circle with the correct size is drawn 
    // at the correct location
    val xCoordinate = GAP_WIDTH.toFloat() + x.toFloat()
    +dir * BulletPointSpan.DEFAULT_BULLET_RADIUS
    val yCoord = (top + bottom) / 2f

    inOrder.verify(canvas)
           .drawCircle(
                eq(xCoordinate),
                eq(yCoord), 
                eq(BulletPointSpan.DEFAULT_BULLET_RADIUS), 
                eq(paint))
    verify(canvas, never()).save()
    verify(canvas, never()).translate(
               eq(xCoordinate), 
               eq(yCoordinate))
}
Copy the code

Check out the rest of the tests in Bulletpoints Most.

Test the use of SPANS

The Spanned interface allows you to set and retrieve spans from text. Implement the Android JUnit test to check that the right span is added in the right place. In the Android-text sample, we convert the bullet Point tag tag to bullet Points. You do this by attaching BulletPointSpans in the right place. Here’s how it can be tested:

@Test fun textWithBulletPoints() {val result = builder.markdownToSpans(" Points\n* one\n+ two ") // check that markup tags are removed AssertEquals (" Points \ none \ ntwo ", result.toString()) // get all the spans attached to the SpannedString val spans = result.getSpans<Any>(0, result.length, Any::class.java)assertEquals(2, spans.size.toLong()) // check that the span is indeed a BulletPointSpan val bulletSpan = spans[0] as BulletPointSpan // check that the start and end indexes are the expected ones assertEquals(7, result.getSpanStart(bulletSpan).toLong()) assertEquals(11, result.getSpanEnd(bulletSpan).toLong()) val bulletSpan2 = spans[1] as BulletPointSpan assertEquals(11, result.getSpanStart(bulletSpan2).toLong()) assertEquals(14, result.getSpanEnd(bulletSpan2).toLong()) }Copy the code

See MarkdownBuilderTest for more test examples.

Tip: If you need to spans off your test, use Spanned#nextSpanTransition instead of Spanned#getSpans, because it’s more efficient.


Spans is a powerful concept, and there’s a lot of power in text rendering. They allow access to components like TextPaint and Canvas, which can do highly customizable styled text on Android. On Android P, we’ve added a ton of documentation to the SPANS framework, so before you implement your own Spans, check to see if you have the spans you need.

In a future article, we’ll go into more detail about how spans can be used in an efficient way under the engine. For example, you need to use TextView.settext (CharSequence, BufferType). For details, please pay attention!

A big thank you to Siyamed Sinir, Clara Bayarri and Nick Butcher.


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.