[转] Spans, a Powerful Concept.

转载自: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:

FontMetrics

If you want to know more about what is font metrics, just look at the following schema:

Playground

BulletSpan

android.text.style.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

android.text.style.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

android.text.style.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

android.text.style.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

android.text.style.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!

Leave a Reply

Your email address will not be published. Required fields are marked *