Recently, I wrote a blog post about the NewStand app and its ActionBar icon translation effect. Cyril Mottier suggested me to use Spans to fade in/out the ActionBar title which is a very elegant solution.
Moreover, I always wanted to try all available types of Span: ImageSpan, BackgroundColorSpan etc. They are very usefull and simple to use but there is not any documentation and details about them.
So, in this article, I’m going to explore what can be done with Spans of the framework and then, I will show you how to push Spans to the next level.
You can download & install the sample application. Checkout the source.
In the framework
Hierarchy
Main rules:
- if a Span affects character-level text formatting, it extends CharacterStyle.
- if a Span affects paragraph-level text formatting, it implements ParagraphStyle
- if a Span modifies the character-level text appearance, it implements UpdateAppearance
- if a Span modifies the character-level text metrics|size, it implements UpdateLayout
It gives us beautiful class diagrams like this.
As it’s a bit complicated so I advise you to use a class visualizer (like this) to fully understand the hierarchy.
How it works?
Layout
When you set text on a TextView, it uses the base class Layout to manage text rendering.
The Layout class contains a boolean mSpannedText
: true when the text is an instance of Spanned (SpannableString implements Spanned). This class only processes ParagraphStyle Spans.
The draw method calls 2 others methods:
- drawBackground
For each line of text, if there is a LineBackgroundSpan for a current line, LineBackgroundSpan#drawBackground is called.
- drawText
For each line of text, it computes LeadingMarginSpan and LeadingMarginSpan2 and calls LeadingMarginSpan#drawLeadingMargin when it’s necessary. This is also where AlignmentSpan is used to determine the text alignment. Finally, if the current line is spanned, Layout calls TextLine#draw (a TextLine object is created for each line).
TextLine
android.text.TextLine documentation says: Represents a line of styled text, for measuring in visual order and for rendering.
TextLine class contains 3 sets of Spans:
- MetricAffectingSpan set
- CharacterStyle set
- ReplacementSpan set
The interesting method is TextLine#handleRun. It’s where all Spans are used to render the text. Relative to the type of Span, TextLine calls:
- CharacterStyle#updateDrawState to change the TextPaint configuration for MetricAffectingSpan and CharacterStyle Spans.
- TextLine#handleReplacement for ReplacementSpan. It calls Replacement#getSize to get the replacement width, update the font metrics if it’s needed and finally call Replacement#draw.
FontMetrics
If you want to know more about what is font metrics, just look at the following schema:
Playground
BulletSpan
The BulletSpan affects paragraph-level text formatting. It allows you to put a bullet on paragraph start.
1 2 3 4 5 6 7 8 |
|
QuoteSpan
The QuoteSpan affects paragraph-level text formatting. It allows you to put a quote vertical line on a paragraph.
1 2 3 4 5 6 7 |
|
AlignmentSpan.Standard
android.text.style.AlignmentSpan.Standard
The AlignmentSpan.Standard affects paragraph-level text formatting. It allows you to align (normal, center, opposite) a paragraph.
1 2 3 4 5 6 7 |
|
UnderlineSpan
android.text.style.UnderlineSpan
The UnderlineSpan affects character-level text formatting. It allows you to underline a character thanks to Paint#setUnderlineText(true) .
1 2 |
|
StrikethroughSpan
android.text.style.StrikethroughSpan
The StrikethroughSpan affects character-level text formatting. It allows you to strikethrough a character thanks to Paint#setStrikeThruText(true)) .
1 2 |
|
SubscriptSpan
android.text.style.SubscriptSpan
The SubscriptSpan affects character-level text formatting. It allows you to subscript a character by reducing the TextPaint#baselineShift .
1 2 |
|
SuperscriptSpan
android.text.style.SuperscriptSpan
The SuperscriptSpan affects character-level text formatting. It allows you to superscript a character by increasing the TextPaint#baselineShift .
1 2 |
|
BackgroundColorSpan
android.text.style.BackgroundColorSpan
The BackgroundColorSpan affects character-level text formatting. It allows you to set a background color on a character.
1 2 3 4 5 6 7 |
|
ForegroundColorSpan
android.text.style.ForegroundColorSpan
The ForegroundColorSpan affects character-level text formatting. It allows you to set a foreground color on a character.
1 2 3 4 5 6 7 |
|
ImageSpan
The ImageSpan affects character-level text formatting. It allows you to a character by an image. It’s one of the few span that is well documented so enjoy it!
1 2 |
|
StyleSpan
The StyleSpan affects character-level text formatting. It allows you to set a style (bold, italic, normal) on a character.
1 2 3 4 5 6 7 |
|
TypefaceSpan
android.text.style.TypefaceSpan
The TypefaceSpan affects character-level text formatting. It allows you to set a font family (monospace, serif etc) on a character.
1 2 3 4 5 6 7 |
|
TextAppearanceSpan
android.text.style.TextAppearanceSpan
The TextAppearanceSpan affects character-level text formatting. It allows you to set a appearance on a character.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
1 2 3 4 5 6 7 8 |
|
AbsoluteSizeSpan
android.text.style.AbsoluteSizeSpan
The AbsoluteSizeSpan affects character-level text formatting. It allows you to set an absolute text size on a character.
1 2 3 4 5 6 7 8 |
|
RelativeSizeSpan
android.text.style.RelativeSizeSpan
The RelativeSizeSpan affects character-level text formatting. It allows you to set an relative text size on a character.
1 2 3 4 5 6 7 |
|
ScaleXSpan
The ScaleXSpan affects character-level text formatting. It allows you to scale on x a character.
1 2 3 4 5 6 7 |
|
MaskFilterSpan
android.text.style.MaskFilterSpan
The MaskFilterSpan affects character-level text formatting. It allows you to set a android.graphics.MaskFilter on a character.
Warning: BlurMaskFilter is not supported with hardware acceleration.
1 2 3 4 5 6 7 8 9 |
|
BlurMaskFilter
EmbossMaskFilter with a blue ForegroundColorSpan and a bold StyleSpan
Pushing Spans to the next level
Animate the foreground color
ForegroundColorSpan is read-only. It means that you can’t change the foreground color after instanciation. So, the first thing to do is to code a MutableForegroundColorSpan.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
|
Now, we can change alpha or foreground color on the same instance. But when you set those properties, it doesn’t refresh the View: you have to do this manually by re-setting the SpannableString.
1 2 3 4 5 6 7 8 9 |
|
Now, we want to animate the foreground color. We use a custom android.util.Property.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Finally, we animate the custom property with an ObjectAnimator. Don’t forget to refresh the View on update.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
ActionBar ‘fireworks’
The ‘fireworks’ animation is to make letter fade in randomly. First, cut the text into multiple spans (for example, one span by character) and fade in spans after spans. Using the previously introduced MutableForegroundColorSpan, we are going to create a special object representing a group of span. And for each call to setAlpha on the group, we randomly set the alpha for each span.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
|
We create a custom property to animate the alpha of a FireworksSpanGroup.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Finally, we create the group and animate it with an ObjectAnimator.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Draw with your own Span
In this section, we are going to see a way to draw via a custom Span. This opens interesting perspectives for text customization.
First, we have to create a custom Span that extends the abstract class ReplacementSpan.
If you only want to draw a custom background, you can implements LineBackgroundSpan which is at paragraph-level.
We have to implement 2 methods:
- getSize: this method returns the new with of your replacement.
text: text managed by the Span
start: start index of text
end: end index of text
fm: font metrics, can be null
- draw: it’s here you can draw with the Canvas.
x: x-coordinate where to draw the text
top: top of the line
y: the baseline
bottom: bottom of the line
Let’s see an example where we draw a blue rectangle around the text.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Bonus
The Sample app contains some examples of pushing Spans to the next level like:
- Progressive blur
- Typewriter
Conclusion
Working on this article, I realised Spans are really powerfull and like Drawables, I think they are not used enough. Text is the main content of an application, it’s everywhere so don’t forget to make it more dynamic and attractive with Spans!