转载自:http://flavienlaurent.com/blog/2014/01/31/spans/
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.
/* public BulletSpan (int gapWidth, int color) -gapWidth: gap in px between bullet and text -color: bullet color (optionnal, default is transparent) */ //create a black BulletSpan with a gap of 15px span = new BulletSpan(15, Color.BLACK);
QuoteSpan
The QuoteSpan affects paragraph-level text formatting. It allows you to put a quote vertical line on a paragraph.
/* public QuoteSpan (int color) -color: quote vertical line color (optionnal, default is Color.BLUE) */ //create a red quote span = new QuoteSpan(Color.RED);
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.
/* public Standard(Layout.Alignment align) -align: alignment to set */ //align center a paragraph span = new AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER);
UnderlineSpan
android.text.style.UnderlineSpan
The UnderlineSpan affects character-level text formatting. It allows you to underline a character thanks to Paint#setUnderlineText(true) .
//underline a character span = new UnderlineSpan();
StrikethroughSpan
android.text.style.StrikethroughSpan
The StrikethroughSpan affects character-level text formatting. It allows you to strikethrough a character thanks to Paint#setStrikeThruText(true)) .
//strikethrough a character span = new StrikethroughSpan();
SubscriptSpan
android.text.style.SubscriptSpan
The SubscriptSpan affects character-level text formatting. It allows you to subscript a character by reducing the TextPaint#baselineShift .
//subscript a character span = new SubscriptSpan();
SuperscriptSpan
android.text.style.SuperscriptSpan
The SuperscriptSpan affects character-level text formatting. It allows you to superscript a character by increasing the TextPaint#baselineShift .
//superscript a character span = new SuperscriptSpan();
BackgroundColorSpan
android.text.style.BackgroundColorSpan
The BackgroundColorSpan affects character-level text formatting. It allows you to set a background color on a character.
/* public BackgroundColorSpan (int color) -color: background color */ //set a green background span = new BackgroundColorSpan(Color.GREEN);
ForegroundColorSpan
android.text.style.ForegroundColorSpan
The ForegroundColorSpan affects character-level text formatting. It allows you to set a foreground color on a character.
/* public ForegroundColorSpan (int color) -color: foreground color */ //set a red foreground span = new ForegroundColorSpan(Color.RED);
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!
//replace a character by pic1_small image span = new ImageSpan(this, R.drawable.pic1_small);
StyleSpan
The StyleSpan affects character-level text formatting. It allows you to set a style (bold, italic, normal) on a character.
/* public StyleSpan (int style) -style: int describing the style (android.graphics.Typeface) */ //set a bold+italic style span = new StyleSpan(Typeface.BOLD | Typeface.ITALIC);
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.
/* public TypefaceSpan (String family) -family: a font family */ //set the serif family span = new TypefaceSpan("serif");
TextAppearanceSpan
android.text.style.TextAppearanceSpan
The TextAppearanceSpan affects character-level text formatting. It allows you to set a appearance on a character.
/* public TextAppearanceSpan(Context context, int appearance, int colorList) -context: a valid context -appearance: text appearance resource (ex: android.R.style.TextAppearance_Small) -colorList: a text color resource (ex: android.R.styleable.Theme_textColorPrimary) public TextAppearanceSpan(String family, int style, int size, ColorStateList color, ColorStateList linkColor) -family: a font family -style: int describing the style (android.graphics.Typeface) -size: text size -color: a text color -linkColor: a link text color */ //set the serif family span = new TextAppearanceSpan(this/*a context*/, R.style.SpecialTextAppearance);
styles.xml
<style name="SpecialTextAppearance" parent="@android:style/TextAppearance"> <item name="android:textColor">@color/color1</item> <item name="android:textColorHighlight">@color/color2</item> <item name="android:textColorHint">@color/color3</item> <item name="android:textColorLink">@color/color4</item> <item name="android:textSize">28sp</item> <item name="android:textStyle">italic</item> </style>
AbsoluteSizeSpan
android.text.style.AbsoluteSizeSpan
The AbsoluteSizeSpan affects character-level text formatting. It allows you to set an absolute text size on a character.
/* public AbsoluteSizeSpan(int size, boolean dip) -size: a size -dip: false, size is in px; true, size is in dip (optionnal, default false) */ //set text size to 24dp span = new AbsoluteSizeSpan(24, true);
RelativeSizeSpan
android.text.style.RelativeSizeSpan
The RelativeSizeSpan affects character-level text formatting. It allows you to set an relative text size on a character.
/* public RelativeSizeSpan(float proportion) -proportion: a proportion of the actual text size */ //set text size 2 times bigger span = new RelativeSizeSpan(2.0f);
ScaleXSpan
The ScaleXSpan affects character-level text formatting. It allows you to scale on x a character.
/* public ScaleXSpan(float proportion) -proportion: a proportion of actual text scale x */ //scale x 3 times bigger span = new ScaleXSpan(3.0f);
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.
/* public MaskFilterSpan(MaskFilter filter) -filter: a filter to apply */ //Blur a character span = new MaskFilterSpan(new BlurMaskFilter(density*2, BlurMaskFilter.Blur.NORMAL)); //Emboss a character span = new MaskFilterSpan(new EmbossMaskFilter(new float[] { 1, 1, 1 }, 0.4f, 6, 3.5f));
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.
public class MutableForegroundColorSpan extends ForegroundColorSpan { private int mAlpha = 255; private int mForegroundColor; public MutableForegroundColorSpan(int alpha, int color) { super(color); mAlpha = alpha; mForegroundColor = color; } public MutableForegroundColorSpan(Parcel src) { super(src); mForegroundColor = src.readInt(); mAlpha = src.readInt(); } public void writeToParcel(Parcel dest, int flags) { super.writeToParcel(dest, flags); dest.writeInt(mForegroundColor); dest.writeFloat(mAlpha); } @Override public void updateDrawState(TextPaint ds) { ds.setColor(getForegroundColor()); } /** * @param alpha from 0 to 255 */ public void setAlpha(int alpha) { mAlpha = alpha; } public void setForegroundColor(int foregroundColor) { mForegroundColor = foregroundColor; } public float getAlpha() { return mAlpha; } @Override public int getForegroundColor() { return Color.argb(mAlpha, Color.red(mForegroundColor), Color.green(mForegroundColor), Color.blue(mForegroundColor)); } }
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.
MutableForegroundColorSpan span = new MutableForegroundColorSpan(255, Color.BLACK); spannableString.setSpan(span, 0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); textView.setText(spannableString); //here the text is black and fully opaque span.setAlpha(100); span.setForegroundColor(Color.RED); //here the text hasn't changed. textView.setText(spannableString); //finally, the text is red and translucent
Now, we want to animate the foreground color. We use a custom android.util.Property.
private static final Property<MutableForegroundColorSpan, Integer> MUTABLE_FOREGROUND_COLOR_SPAN_FC_PROPERTY = new Property<MutableForegroundColorSpan, Integer>(Integer.class, "MUTABLE_FOREGROUND_COLOR_SPAN_FC_PROPERTY") { @Override public void set(MutableForegroundColorSpan span, Integer value) { span.setForegroundColor(value); } @Override public Integer get(MutableForegroundColorSpan span) { return span.getForegroundColor(); } };
Finally, we animate the custom property with an ObjectAnimator. Don’t forget to refresh the View on update.
MutableForegroundColorSpan span = new MutableForegroundColorSpan(255, Color.BLACK); mSpannableString.setSpan(span, 0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); ObjectAnimator objectAnimator = ObjectAnimator.ofInt(span, MUTABLE_FOREGROUND_COLOR_SPAN_FC_PROPERTY, Color.BLACK, Color.RED); objectAnimator.setEvaluator(new ArgbEvaluator()); objectAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { //refresh mText.setText(mSpannableString); } }); objectAnimator.start();
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.
private static final class FireworksSpanGroup { private final float mAlpha; private final ArrayList<MutableForegroundColorSpan> mSpans; private FireworksSpanGroup(float alpha) { mAlpha = alpha; mSpans = new ArrayList<MutableForegroundColorSpan>(); } public void addSpan(MutableForegroundColorSpan span) { span.setAlpha((int) (mAlpha * 255)); mSpans.add(span); } public void init() { Collections.shuffle(mSpans); } public void setAlpha(float alpha) { int size = mSpans.size(); float total = 1.0f * size * alpha; for(int index = 0 ; index < size; index++) { MutableForegroundColorSpan span = mSpans.get(index); if(total >= 1.0f) { span.setAlpha(255); total -= 1.0f; } else { span.setAlpha((int) (total * 255)); total = 0.0f; } } } public float getAlpha() { return mAlpha; } }
We create a custom property to animate the alpha of a FireworksSpanGroup.
private static final Property<FireworksSpanGroup, Float> FIREWORKS_GROUP_PROGRESS_PROPERTY = new Property<FireworksSpanGroup, Float>(Float.class, "FIREWORKS_GROUP_PROGRESS_PROPERTY") { @Override public void set(FireworksSpanGroup spanGroup, Float value) { spanGroup.setProgress(value); } @Override public Float get(FireworksSpanGroup spanGroup) { return spanGroup.getProgress(); } };
Finally, we create the group and animate it with an ObjectAnimator.
final FireworksSpanGroup spanGroup = new FireworksSpanGroup(); //init the group with multiple spans //spanGroup.addSpan(span); //set spans on the ActionBar spannable title //mActionBarTitleSpannableString.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); spanGroup.init(); ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(spanGroup, FIREWORKS_GROUP_PROGRESS_PROPERTY, 0.0f, 1.0f); objectAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { //refresh the ActionBar title setTitle(mActionBarTitleSpannableString); } }); objectAnimator.start();
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.
@Override public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) { //return text with relative to the Paint mWidth = (int) paint.measureText(text, start, end); return mWidth; } @Override public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) { //draw the frame with custom Paint canvas.drawRect(x, top, x + mWidth, bottom, mPaint); }
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!