Spans are powerful markup objects that you can use to style text at the
character or paragraph level. By attaching spans to text objects, you can change
text in a variety of ways, including adding color, making the text clickable,
scaling the text size, and drawing text in a customized way. Spans can also
change TextPaint
properties, draw on a
Canvas
, and change text layout.
Android provides several types of spans that cover a variety of common text styling patterns. You can also create your own spans to apply custom styling.
Create and apply a span
To create a span, you can use one of the classes listed in the following table. The classes differ based on whether the text itself is mutable, whether the text markup is mutable, and what underlying data structure contains the span data.
Class | Mutable text | Mutable markup | Data structure |
---|---|---|---|
SpannedString |
No | No | Linear array |
SpannableString |
No | Yes | Linear array |
SpannableStringBuilder |
Yes | Yes | Interval tree |
All three classes extend the Spanned
interface. SpannableString
and SpannableStringBuilder
also extend the
Spannable
interface.
Here's how to decide which one to use:
- If you aren't modifying the text or markup after creation, use
SpannedString
. - If you need to attach a small number of spans to a single text object and
the text itself is read-only, use
SpannableString
. - If you need to modify text after creation and you need to attach spans to
the text, use
SpannableStringBuilder
. - If you need to attach a large number of spans to a text object, regardless
of whether the text itself is read-only, use
SpannableStringBuilder
.
To apply a span, call setSpan(Object _what_, int _start_, int _end_, int
_flags_)
on a Spannable
object. The what parameter refers to the span you are
applying to the text, and the start and end parameters indicate the portion
of the text you are applying the span to.
If you insert text inside a span's boundaries, the span automatically expands to
include the inserted text. When inserting text at the span
boundaries—that is, at the start or end indices—the flags
parameter determines whether the span expands to include the inserted text. Use
the
Spannable.SPAN_EXCLUSIVE_INCLUSIVE
flag to include inserted text, and use
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
to exclude the inserted text.
The following example shows how to attach a
ForegroundColorSpan
to a
string:
Kotlin
val spannable = SpannableStringBuilder("Text is spantastic!") spannable.setSpan( ForegroundColorSpan(Color.RED), 8, // start 12, // end Spannable.SPAN_EXCLUSIVE_INCLUSIVE )
Java
SpannableStringBuilder spannable = new SpannableStringBuilder("Text is spantastic!"); spannable.setSpan( new ForegroundColorSpan(Color.RED), 8, // start 12, // end Spannable.SPAN_EXCLUSIVE_INCLUSIVE );
Because the span is set using Spannable.SPAN_EXCLUSIVE_INCLUSIVE
, the span
expands to include inserted text at the span boundaries, as shown in the
following example:
Kotlin
val spannable = SpannableStringBuilder("Text is spantastic!") spannable.setSpan( ForegroundColorSpan(Color.RED), 8, // start 12, // end Spannable.SPAN_EXCLUSIVE_INCLUSIVE ) spannable.insert(12, "(& fon)")
Java
SpannableStringBuilder spannable = new SpannableStringBuilder("Text is spantastic!"); spannable.setSpan( new ForegroundColorSpan(Color.RED), 8, // start 12, // end Spannable.SPAN_EXCLUSIVE_INCLUSIVE ); spannable.insert(12, "(& fon)");
You can attach multiple spans to the same text. The following example shows how to create text that is bold and red:
Kotlin
val spannable = SpannableString("Text is spantastic!") spannable.setSpan(ForegroundColorSpan(Color.RED), 8, 12, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) spannable.setSpan( StyleSpan(Typeface.BOLD), 8, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE )
Java
SpannableString spannable = new SpannableString("Text is spantastic!"); spannable.setSpan( new ForegroundColorSpan(Color.RED), 8, 12, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE ); spannable.setSpan( new StyleSpan(Typeface.BOLD), 8, spannable.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE );
Android span types
Android provides over 20 span types in the android.text.style package. Android categorizes spans in two primary ways:
- How the span affects text: a span can affect text appearance or text metrics.
- Span scope: some spans can be applied to individual characters, while others must be applied to an entire paragraph.
The following sections describe these categories in more detail.
Spans that affect text appearance
Some spans that apply at the character level affect text appearance, such as
changing text or background color and adding underlines or strikethroughs. These
spans extend the
CharacterStyle
class.
The following code example shows how to apply an UnderlineSpan
to underline
the text:
Kotlin
val string = SpannableString("Text with underline span") string.setSpan(UnderlineSpan(), 10, 19, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
Java
SpannableString string = new SpannableString("Text with underline span"); string.setSpan(new UnderlineSpan(), 10, 19, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Spans that affect only text appearance trigger a redraw of the text without
triggering a recalculation of the layout. These spans implement
UpdateAppearance
and extend
CharacterStyle
.
CharacterStyle
subclasses define how to draw text by providing access to
update the TextPaint
.
Spans that affect text metrics
Other spans that apply at the character level affect text metrics, such as line
height and text size. These spans extend the
MetricAffectingSpan
class.
The following code example creates a
RelativeSizeSpan
that
increases text size by 50%:
Kotlin
val string = SpannableString("Text with relative size span") string.setSpan(RelativeSizeSpan(1.5f), 10, 24, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
Java
SpannableString string = new SpannableString("Text with relative size span"); string.setSpan(new RelativeSizeSpan(1.5f), 10, 24, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Applying a span that affects text metrics causes an observing object to re-measure the text for correct layout and rendering—for example, changing text size might cause words to appear on different lines. Applying the preceding span triggers a remeasure, recalculation of the text layout, and redrawing of the text.
Spans that affect text metrics extend the MetricAffectingSpan
class, an
abstract class that lets subclasses define how the span affects text measurement
by providing access to the TextPaint
. Since MetricAffectingSpan
extends
CharacterSpan
, subclasses affect the appearance of the text at the character
level.
Spans that affect paragraphs
A span can also affect text at the paragraph level, such as changing the
alignment or the margin of a block of text. Spans that affect entire paragraphs
implement ParagraphStyle
. To
use these spans, you attach them to the entire paragraph, excluding the ending
new line character. If you try to apply a paragraph span to something other than
a whole paragraph, Android doesn't apply the span at all.
Figure 8 shows how Android separates paragraphs in text.
The following code example applies a
QuoteSpan
to a paragraph. Note that
if you attach the span to any position other than the beginning or end of a
paragraph, Android doesn't apply the style at all.
Kotlin
spannable.setSpan(QuoteSpan(color), 8, text.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
Java
spannable.setSpan(new QuoteSpan(color), 8, text.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
Create custom spans
If you need more functionality than what is provided in the existing Android spans, you can implement a custom span. When implementing your own span, decide whether your span affects text at the character level or the paragraph level and also whether it affects the layout or appearance of the text. This helps you determine which base classes you can extend and which interfaces you might need to implement. Use the following table for reference:
Scenario | Class or interface |
---|---|
Your span affects text at the character level. | CharacterStyle |
Your span affects text appearance. | UpdateAppearance |
Your span affects text metrics. | UpdateLayout |
Your span affects text at the paragraph level. | ParagraphStyle |
For example, if you need to implement a custom span that modifies text size and
color, extend RelativeSizeSpan
. Through inheritance, RelativeSizeSpan
extends CharacterStyle
and implements the two Update
interfaces. Since this
class already provides callbacks for updateDrawState
and updateMeasureState
,
you can override these callbacks to implement your custom behavior. The
following code creates a custom span that extends RelativeSizeSpan
and
overrides the updateDrawState
callback to set the color of the TextPaint
:
Kotlin
class RelativeSizeColorSpan( size: Float, @ColorInt private val color: Int ) : RelativeSizeSpan(size) { override fun updateDrawState(textPaint: TextPaint) { super.updateDrawState(textPaint) textPaint.color = color } }
Java
public class RelativeSizeColorSpan extends RelativeSizeSpan { private int color; public RelativeSizeColorSpan(float spanSize, int spanColor) { super(spanSize); color = spanColor; } @Override public void updateDrawState(TextPaint textPaint) { super.updateDrawState(textPaint); textPaint.setColor(color); } }
This example illustrates how to create a custom span. You can achieve the same
effect by applying a RelativeSizeSpan
and ForegroundColorSpan
to the text.
Test span usage
The Spanned
interface lets you both set spans and also retrieve spans from
text. When testing, implement an Android JUnit
test to verify that the correct spans are added
at the correct locations. The Text Styling sample
app
contains a span that applies markup to bullet points by attaching
BulletPointSpan
to the text. The following code example shows how to test
whether the bullet points appear as expected:
Kotlin
@Test fun textWithBulletPoints() { val result = builder.markdownToSpans("Points\n* one\n+ two") // Check whether the 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) // Check whether the correct number of spans are created. assertEquals(2, spans.size.toLong()) // Check whether the spans are instances of BulletPointSpan. val bulletSpan1 = spans[0] as BulletPointSpan val bulletSpan2 = spans[1] as BulletPointSpan // Check whether the start and end indices are the expected ones. assertEquals(7, result.getSpanStart(bulletSpan1).toLong()) assertEquals(11, result.getSpanEnd(bulletSpan1).toLong()) assertEquals(11, result.getSpanStart(bulletSpan2).toLong()) assertEquals(14, result.getSpanEnd(bulletSpan2).toLong()) }
Java
@Test public void textWithBulletPoints() { SpannedString result = builder.markdownToSpans("Points\n* one\n+ two"); // Check whether the markup tags are removed. assertEquals("Points\none\ntwo", result.toString()); // Get all the spans attached to the SpannedString. Object[] spans = result.getSpans(0, result.length(), Object.class); // Check whether the correct number of spans are created. assertEquals(2, spans.length); // Check whether the spans are instances of BulletPointSpan. BulletPointSpan bulletSpan1 = (BulletPointSpan) spans[0]; BulletPointSpan bulletSpan2 = (BulletPointSpan) spans[1]; // Check whether the start and end indices are the expected ones. assertEquals(7, result.getSpanStart(bulletSpan1)); assertEquals(11, result.getSpanEnd(bulletSpan1)); assertEquals(11, result.getSpanStart(bulletSpan2)); assertEquals(14, result.getSpanEnd(bulletSpan2)); }
For more test examples, see MarkdownBuilderTest on GitHub.
Test custom spans
When testing spans, verify that the TextPaint
contains the expected
modifications and that the correct elements appear on your Canvas
. For
example, consider a custom span implementation that prepends a bullet point to
some text. The bullet point has a specified size and color, and there is a gap
between the left margin of the drawable area and the bullet point.
You can test the behavior of this class by implementing an AndroidJUnit test, checking for the following:
- If you correctly apply the span, a bullet point of the specified size and color appears on the canvas, and the proper space exists between the left margin and the bullet point.
- If you don't apply the span, none of the custom behavior appears.
You can see the implementation of these tests in the TextStyling sample on GitHub.
You can test Canvas interactions by mocking the canvas, passing the mocked
object to the
drawLeadingMargin()
method, and verifying that the correct methods are called with the correct
parameters.
You can find more span test samples in BulletPointSpanTest.
Best practices for using spans
There are several memory-efficient ways to set text in a TextView
, depending
on your needs.
Attach or detach a span without changing the underlying text
TextView.setText()
contains multiple overloads that handle spans differently. For example, you can
set a Spannable
text object with the following code:
Kotlin
textView.setText(spannableObject)
Java
textView.setText(spannableObject);
When calling this overload of setText()
, the TextView
creates a copy of your
Spannable
as a SpannedString
and keeps it in memory as a CharSequence
.
This means that your text and the spans are immutable, so when you need to
update the text or the spans, create a new Spannable
object and call
setText()
again, which also triggers a re-measuring and re-drawing of the
layout.
To indicate that the spans must be mutable, you can instead use
setText(CharSequence text, TextView.BufferType
type)
,
as shown in the following example:
Kotlin
textView.setText(spannable, BufferType.SPANNABLE) val spannableText = textView.text as Spannable spannableText.setSpan( ForegroundColorSpan(color), 8, spannableText.length, SPAN_INCLUSIVE_INCLUSIVE )
Java
textView.setText(spannable, BufferType.SPANNABLE); Spannable spannableText = (Spannable) textView.getText(); spannableText.setSpan( new ForegroundColorSpan(color), 8, spannableText.getLength(), SPAN_INCLUSIVE_INCLUSIVE);
In this example, the
BufferType.SPANNABLE
parameter causes the TextView
to create a SpannableString
, and the
CharSequence
object kept by the TextView
now has mutable markup and
immutable text. To update the span, retrieve the text as a Spannable
and then
update the spans as needed.
When you attach, detach, or reposition spans, the TextView
automatically
updates to reflect the change to the text. If you change an internal attribute
of an existing span, call invalidate()
to make appearance-related changes or
requestLayout()
to make metric-related changes.
Set text in a TextView multiple times
In some cases, such as when using a
RecyclerView.ViewHolder
,
you might want to reuse a TextView
and set the text multiple times. By
default, regardless of whether you set the BufferType
, the TextView
creates
a copy of the CharSequence
object and holds it in memory. This makes all
TextView
updates intentional—you can't update the original
CharSequence
object to update the text. This means every time you set new
text, the TextView
creates a new object.
If want to take more control over this process and avoid the extra object
creation, you can implement your own
Spannable.Factory
and override
newSpannable()
.
Instead of creating a new text object, you can cast and return the existing
CharSequence
as a Spannable
, as demonstrated in the following example:
Kotlin
val spannableFactory = object : Spannable.Factory() { override fun newSpannable(source: CharSequence?): Spannable { return source as Spannable } }
Java
Spannable.Factory spannableFactory = new Spannable.Factory(){ @Override public Spannable newSpannable(CharSequence source) { return (Spannable) source; } };
You must use textView.setText(spannableObject, BufferType.SPANNABLE)
when
setting the text. Otherwise, the source CharSequence
is created as a Spanned
instance and can't be cast to Spannable
, causing newSpannable()
to throw a
ClassCastException
.
After overriding newSpannable()
, tell the TextView
to use the new Factory
:
Kotlin
textView.setSpannableFactory(spannableFactory)
Java
textView.setSpannableFactory(spannableFactory);
Set the Spannable.Factory
object once, right after you get a reference to your
TextView
. If you're using a RecyclerView
, set the Factory
object when you
first inflate your views. This avoids extra object creation when your
RecyclerView
binds a new item to your ViewHolder
.
Change internal span attributes
If you need to change only an internal attribute of a mutable span, such as the
bullet color in a custom bullet span, you can avoid the overhead from calling
setText()
multiple times by keeping a reference to the span as it's created.
When you need to modify the span, you can modify the reference and then call
invalidate()
or requestLayout()
on the TextView
, depending on the type of
attribute you changed.
In the following code example, a custom bullet point implementation has a default color of red that changes to gray when a button is tapped:
Kotlin
class MainActivity : AppCompatActivity() { // Keeping the span as a field. val bulletSpan = BulletPointSpan(color = Color.RED) override fun onCreate(savedInstanceState: Bundle?) { ... val spannable = SpannableString("Text is spantastic") // Setting the span to the bulletSpan field. spannable.setSpan( bulletSpan, 0, 4, Spanned.SPAN_INCLUSIVE_INCLUSIVE ) styledText.setText(spannable) button.setOnClickListener { // Change the color of the mutable span. bulletSpan.color = Color.GRAY // Color doesn't change until invalidate is called. styledText.invalidate() } } }
Java
public class MainActivity extends AppCompatActivity { private BulletPointSpan bulletSpan = new BulletPointSpan(Color.RED); @Override protected void onCreate(Bundle savedInstanceState) { ... SpannableString spannable = new SpannableString("Text is spantastic"); // Setting the span to the bulletSpan field. spannable.setSpan(bulletSpan, 0, 4, Spanned.SPAN_INCLUSIVE_INCLUSIVE); styledText.setText(spannable); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { // Change the color of the mutable span. bulletSpan.setColor(Color.GRAY); // Color doesn't change until invalidate is called. styledText.invalidate(); } }); } }
Use Android KTX extension functions
Android KTX also contains extension functions that make working with spans easier. To learn more, see the documentation for the androidx.core.text package.