字符級Span解析

1 簡介

之前已經(jīng)講過TextView的基礎(chǔ)知識,和段落級別的Span,現(xiàn)在在這進(jìn)一步進(jìn)行講解,這篇文字主要講解如何給TextView設(shè)置字符級別的Span。如果一個(gè)Span想要影響段落層次的文本格式,則需要繼承CharacterStyle。


2 CharacterStyle

CharacterStyle是個(gè)抽象類,字符級別的Span都需要繼承這個(gè)類,這個(gè)類里面有一個(gè)抽象方法:

public abstract void updateDrawState(TextPaint tp)

通過改變TextPaint的屬性就可以得到不同的展現(xiàn)形式。在這個(gè)抽象類里面還有一個(gè)靜態(tài)方法:

public static CharacterStyle wrap(CharacterStyle cs)

一個(gè)CharacterStyle類型的Span只能給一個(gè)Spaned片段使用,如果想這個(gè)Span給多個(gè)片段使用可以使用wrap方法。wrap方法的具體代碼如下:

public static CharacterStyle wrap(CharacterStyle cs) {
    if (cs instanceof MetricAffectingSpan) {
        return new MetricAffectingSpan.Passthrough((MetricAffectingSpan) cs);
    } else {
        return new Passthrough(cs);
    }
}

再看Passthrough的代碼

private static class Passthrough extends CharacterStyle {
    private CharacterStyle mStyle;

    /**
     * Creates a new Passthrough of the specfied CharacterStyle.
     */
    public Passthrough(CharacterStyle cs) {
        mStyle = cs;
    }

    /**
     * Passes updateDrawState through to the underlying CharacterStyle.
     */
    @Override
    public void updateDrawState(TextPaint tp) {
        mStyle.updateDrawState(tp);
    }

    /**
     * Returns the CharacterStyle underlying this one, or the one
     * underlying it if it too is a Passthrough.
     */
    @Override
    public CharacterStyle getUnderlying() {
        return mStyle.getUnderlying();
    }
}

不難發(fā)現(xiàn)其實(shí)就是復(fù)制了一個(gè)CharacterStyle。


3 UpdateAppearance

如果一個(gè)Span修改字符級別的文本外觀,則實(shí)現(xiàn)UpdateAppearance。


UpdateAppearance

上面的Span都實(shí)現(xiàn)了UpdateAppearance接口,上面的諸多Span都是通過updateDrawState(TextPaint ds)方法來實(shí)現(xiàn)相應(yīng)的效果。

  1. BackgroundColorSpan:ds.bgColor = mColor;
  2. ForegroundColorSpan:ds.setColor(mColor);
  3. StrikethroughSpan:ds.setStrikeThruText(true);
  4. UnderlineSpan:ds.setUnderlineText(true);
  5. MaskFilterSpan:ds.setMaskFilter(mFilter);

BackgroundColorSpan和ForegroundColorSpan


BackAndFront

UnderlineSpan和StrikethroughSpan:


UnderAndStrike

MaskFilterSpan:
Mask

可以看一下ClickableSpan的源代碼

public abstract class ClickableSpan extends CharacterStyle implements UpdateAppearance {

    /**
     * Performs the click action associated with this span.
     */
    public abstract void onClick(View widget);
   
    /**
     * Makes the text underlined and in the link color.
     */
    @Override
    public void updateDrawState(TextPaint ds) {
        ds.setColor(ds.linkColor);
        ds.setUnderlineText(true);
    }
}

點(diǎn)擊后通過updateDrawState(TextPaint ds)方法改變字體外觀,onClick(View widget)則交給子類實(shí)現(xiàn)相應(yīng)的邏輯。
MaskFilterSpan中ds.setMaskFilter(mFilter)可以給字體設(shè)置模糊和浮雕效果。

span = new MaskFilterSpan(new BlurMaskFilter(density*2, BlurMaskFilter.Blur.NORMAL));
span = new MaskFilterSpan(new EmbossMaskFilter(new float[] { 1, 1, 1 }, 0.4f, 6, 3.5f));

4 UpdateLayout

如果一個(gè)Span修改字符級文本度量|大小,則實(shí)現(xiàn)UpdateLayout。在Android源碼中,只有MetricAffectingSpan實(shí)現(xiàn)了UpdateLayout接口。


UpdateLayout

UpdateLayout

接下來看一下MetricAffectingSpan的源碼。

public abstract class MetricAffectingSpan
extends CharacterStyle
implements UpdateLayout {

    public abstract void updateMeasureState(TextPaint p);

    /**
     * Returns "this" for most MetricAffectingSpans, but for 
     * MetricAffectingSpans that were generated by {@link #wrap},
     * returns the underlying MetricAffectingSpan.
     */
    @Override
    public MetricAffectingSpan getUnderlying() {
        return this;
    }

    /**
     * A Passthrough MetricAffectingSpan is one that
     * passes {@link #updateDrawState} and {@link #updateMeasureState}
     * calls through to the specified MetricAffectingSpan 
     * while still being a distinct object,
     * and is therefore able to be attached to the same Spannable
     * to which the specified MetricAffectingSpan is already attached.
     */
    /* package */ static class Passthrough extends MetricAffectingSpan {
        private MetricAffectingSpan mStyle;
        
        /**
         * Creates a new Passthrough of the specfied MetricAffectingSpan.
         */
        public Passthrough(MetricAffectingSpan cs) {
            mStyle = cs;
        }

        /**
         * Passes updateDrawState through to the underlying MetricAffectingSpan.
         */
        @Override
        public void updateDrawState(TextPaint tp) {
            mStyle.updateDrawState(tp);
        }

        /**
         * Passes updateMeasureState through to the underlying MetricAffectingSpan.
         */
        @Override
        public void updateMeasureState(TextPaint tp) {
            mStyle.updateMeasureState(tp);
        }
    
        /**
         * Returns the MetricAffectingSpan underlying this one, or the one
         * underlying it if it too is a Passthrough.
         */
        @Override
        public MetricAffectingSpan getUnderlying() {
            return mStyle.getUnderlying();
        }
    }
}

可以看見MetricAffectingSpan同樣繼承了CharacterStyle,因此同樣繼承了抽象方法updateDrawState(TextPaint tp),這個(gè)方法可以交給子類實(shí)現(xiàn),從而實(shí)現(xiàn)字體外觀的改變。在MetricAffectingSpan類中定義了一個(gè)抽象方法updateMeasureState(TextPaint p),繼承MetricAffectingSpan類的子類可以實(shí)現(xiàn)這個(gè)抽象方法,從而實(shí)現(xiàn)對字體大小的改變。在MetricAffectingSpan中同樣也提供了一個(gè)Passthrough的類,從而完成CharacterStyle中定義的wrap方法。
接下來分別對MetricAffectingSpan的實(shí)現(xiàn)類進(jìn)行講述。


4.1 SubscriptSpan和SuperscriptSpan

SubscriptSpan和SuperscriptSpan實(shí)現(xiàn)字體的上下標(biāo)展示,效果如下面的圖片所示:


SubscriptSpan

SuperscriptSpan

其實(shí)這兩個(gè)Span的實(shí)現(xiàn)特別簡單,通過查看這兩個(gè)類的實(shí)現(xiàn),能夠幫助我們對Android的字體有著更深入的理解。
SuperscriptSpan:

    @Override
    public void updateDrawState(TextPaint tp) {
        tp.baselineShift += (int) (tp.ascent() / 2);
    }

    @Override
    public void updateMeasureState(TextPaint tp) {
        tp.baselineShift += (int) (tp.ascent() / 2);
    }

SubscriptSpan:

    @Override
    public void updateDrawState(TextPaint tp) {
        tp.baselineShift -= (int) (tp.ascent() / 2);
    }

    @Override
    public void updateMeasureState(TextPaint tp) {
        tp.baselineShift -= (int) (tp.ascent() / 2);
    }

4.2 AbsoluteSizeSpan和RelativeSizeSpan

AbsoluteSizeSpan和RelativeSizeSpan用來改變相應(yīng)字符的字體大小。

/**
* size: 大小
* dip: false,size單位為px,true,size單位為dip(默認(rèn)為false)。
*/
//設(shè)置文字大小為24dp
span = new AbsoluteSizeSpan(24, true);
AbsoluteSizeSpan
//設(shè)置文字大小為大2倍
span = new RelativeSizeSpan(2.0f);
RelativeSizeSpan

AbsoluteSizeSpan:

    @Override
    public void updateDrawState(TextPaint ds) {
        if (mDip) {
            ds.setTextSize(mSize * ds.density);
        } else {
            ds.setTextSize(mSize);
        }
    }

    @Override
    public void updateMeasureState(TextPaint ds) {
        if (mDip) {
            ds.setTextSize(mSize * ds.density);
        } else {
            ds.setTextSize(mSize);
        }
    }

RelativeSizeSpan:

    @Override
    public void updateDrawState(TextPaint ds) {
        ds.setTextSize(ds.getTextSize() * mProportion);
    }

    @Override
    public void updateMeasureState(TextPaint ds) {
        ds.setTextSize(ds.getTextSize() * mProportion);
    }

4.3 ScaleXSpan

ScaleXSpan影響字符集的文本格式。它可以在x軸方向上縮放字符集。

//設(shè)置水平方向上放大3倍
span = new ScaleXSpan(3.0f);
ScaleXSpan

源碼:

    @Override
    public void updateDrawState(TextPaint ds) {
        ds.setTextScaleX(ds.getTextScaleX() * mProportion);
    }

    @Override
    public void updateMeasureState(TextPaint ds) {
        ds.setTextScaleX(ds.getTextScaleX() * mProportion);
    }

4.4 StyleSpan、TypefaceSpan和TextAppearanceSpan

StyleSpan、TypefaceSpan和TextAppearanceSpan都可以字體的樣式進(jìn)行改變,StyleSpan可以對字體設(shè)置bold或者italic的字符樣式,TypefaceSpan可以對字體設(shè)置其他的樣式,TextAppearanceSpan通過xml文件從而對字體進(jìn)行設(shè)置。

//設(shè)置bold+italic的字符樣式
span = new StyleSpan(Typeface.BOLD | Typeface.ITALIC);
StyleSpan
//設(shè)置serif family
span = new TypefaceSpan("serif");
TypefaceSpan
span = new TextAppearanceSpan(this, R.style.SpecialTextAppearance);
<-- style.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>
TextAppearanceSpan

StyleSpan:

    @Override
    public void updateDrawState(TextPaint ds) {
        apply(ds, mStyle);
    }

    @Override
    public void updateMeasureState(TextPaint paint) {
        apply(paint, mStyle);
    }

    private static void apply(Paint paint, int style) {
        int oldStyle;

        Typeface old = paint.getTypeface();
        if (old == null) {
            oldStyle = 0;
        } else {
            oldStyle = old.getStyle();
        }

        int want = oldStyle | style;

        Typeface tf;
        if (old == null) {
            tf = Typeface.defaultFromStyle(want);
        } else {
            tf = Typeface.create(old, want);
        }

        int fake = want & ~tf.getStyle();

        if ((fake & Typeface.BOLD) != 0) {
            paint.setFakeBoldText(true);
        }

        if ((fake & Typeface.ITALIC) != 0) {
            paint.setTextSkewX(-0.25f);
        }

        paint.setTypeface(tf);
    }

TypefaceSpan:

@Override
    public void updateDrawState(TextPaint ds) {
        apply(ds, mFamily);
    }

    @Override
    public void updateMeasureState(TextPaint paint) {
        apply(paint, mFamily);
    }

    private static void apply(Paint paint, String family) {
        int oldStyle;

        Typeface old = paint.getTypeface();
        if (old == null) {
            oldStyle = 0;
        } else {
            oldStyle = old.getStyle();
        }

        Typeface tf = Typeface.create(family, oldStyle);
        int fake = oldStyle & ~tf.getStyle();

        if ((fake & Typeface.BOLD) != 0) {
            paint.setFakeBoldText(true);
        }

        if ((fake & Typeface.ITALIC) != 0) {
            paint.setTextSkewX(-0.25f);
        }

        paint.setTypeface(tf);
    }

TextAppearanceSpan:

    @Override
    public void updateDrawState(TextPaint ds) {
        updateMeasureState(ds);

        if (mTextColor != null) {
            ds.setColor(mTextColor.getColorForState(ds.drawableState, 0));
        }

        if (mTextColorLink != null) {
            ds.linkColor = mTextColorLink.getColorForState(ds.drawableState, 0);
        }
    }

    @Override
    public void updateMeasureState(TextPaint ds) {
        if (mTypeface != null || mStyle != 0) {
            Typeface tf = ds.getTypeface();
            int style = 0;

            if (tf != null) {
                style = tf.getStyle();
            }

            style |= mStyle;

            if (mTypeface != null) {
                tf = Typeface.create(mTypeface, style);
            } else if (tf == null) {
                tf = Typeface.defaultFromStyle(style);
            } else {
                tf = Typeface.create(tf, style);
            }

            int fake = style & ~tf.getStyle();

            if ((fake & Typeface.BOLD) != 0) {
                ds.setFakeBoldText(true);
            }

            if ((fake & Typeface.ITALIC) != 0) {
                ds.setTextSkewX(-0.25f);
            }

            ds.setTypeface(tf);
        }

        if (mTextSize > 0) {
            ds.setTextSize(mTextSize);
        }
    }

4.5 LocaleSpan

LocaleSpan用來對字體設(shè)置不同的地區(qū),由于不同地區(qū)的字體會(huì)導(dǎo)致字體大小的變化,因此LocaleSpan也需要繼承MetricAffectingSpan。


LineHeightDemo

源碼:

    @Override
    public void updateDrawState(TextPaint ds) {
        apply(ds, mLocale);
    }

    @Override
    public void updateMeasureState(TextPaint paint) {
        apply(paint, mLocale);
    }

    private static void apply(Paint paint, Locale locale) {
        paint.setTextLocale(locale);
    }

5 ReplacementSpan

ReplacementSpan繼承了MetricAffectingSpan,但是ReplacementSpan比較復(fù)雜因此在這單獨(dú)講解。在ReplacementSpan里新增加了兩個(gè)抽象方法,ReplacementSpan源碼如下:

public abstract class ReplacementSpan extends MetricAffectingSpan {

    public abstract int getSize(Paint paint, CharSequence text,
                         int start, int end,
                         Paint.FontMetricsInt fm);
    public abstract void draw(Canvas canvas, CharSequence text,
                     int start, int end, float x,
                     int top, int y, int bottom, Paint paint);

    /**
     * This method does nothing, since ReplacementSpans are measured
     * explicitly instead of affecting Paint properties.
     */
    public void updateMeasureState(TextPaint p) { }

    /**
     * This method does nothing, since ReplacementSpans are drawn
     * explicitly instead of affecting Paint properties.
     */
    public void updateDrawState(TextPaint ds) { }
}

抽象方法getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm)返回所占的寬度。其實(shí)根據(jù)getSize方法的參數(shù)我們能夠計(jì)算原本那些字符所占用的寬度,計(jì)算方法如下:

    @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;
    }

通過這個(gè)寬度我們可以給文字制作相應(yīng)的效果。
抽象方法draw,可以讓我們在合適的區(qū)域繪制相應(yīng)的圖形,start和end分別為span作用的起始和結(jié)束字符的index,x為起始橫坐標(biāo),y為baseline對應(yīng)的坐標(biāo),top為起始高度,bottom為結(jié)束高度。
在Android提供的源碼里面提供了一個(gè)抽象類DynamicDrawableSpan來繼承ReplacementSpan,而DynamicDrawableSpan又有一個(gè)子類ImageSpan。


5.1 DynamicDrawableSpan

DynamicDrawableSpan是一個(gè)抽象類,DynamicDrawableSpan可以做到使用Drawable替代相對應(yīng)的字符序列,展現(xiàn)效果如下所示:


ImageSpan

下面我們來分析一下DynamicDrawableSpan的源碼。

public abstract class DynamicDrawableSpan extends ReplacementSpan {
    private static final String TAG = "DynamicDrawableSpan";
    
    /**
     * A constant indicating that the bottom of this span should be aligned
     * with the bottom of the surrounding text, i.e., at the same level as the
     * lowest descender in the text.
     */
    public static final int ALIGN_BOTTOM = 0;
    
    /**
     * A constant indicating that the bottom of this span should be aligned
     * with the baseline of the surrounding text.
     */
    public static final int ALIGN_BASELINE = 1;
    
    protected final int mVerticalAlignment;
    
    public DynamicDrawableSpan() {
        mVerticalAlignment = ALIGN_BOTTOM;
    }

    /**
     * @param verticalAlignment one of {@link #ALIGN_BOTTOM} or {@link #ALIGN_BASELINE}.
     */
    protected DynamicDrawableSpan(int verticalAlignment) {
        mVerticalAlignment = verticalAlignment;
    }

    /**
     * Returns the vertical alignment of this span, one of {@link #ALIGN_BOTTOM} or
     * {@link #ALIGN_BASELINE}.
     */
    public int getVerticalAlignment() {
        return mVerticalAlignment;
    }

    /**
     * Your subclass must implement this method to provide the bitmap   
     * to be drawn.  The dimensions of the bitmap must be the same
     * from each call to the next.
     */
    public abstract Drawable getDrawable();

    @Override
    public int getSize(Paint paint, CharSequence text,
                         int start, int end,
                         Paint.FontMetricsInt fm) {
        Drawable d = getCachedDrawable();
        Rect rect = d.getBounds();
        if (fm != null) {
            fm.ascent = -rect.bottom; 
            fm.descent = 0; 

            fm.top = fm.ascent;
            fm.bottom = 0;
        }
        return rect.right;
    }

    @Override
    public void draw(Canvas canvas, CharSequence text,
                     int start, int end, float x, 
                     int top, int y, int bottom, Paint paint) {
        Drawable b = getCachedDrawable();
        canvas.save();
        int transY = bottom - b.getBounds().bottom;
        if (mVerticalAlignment == ALIGN_BASELINE) {
            transY -= paint.getFontMetricsInt().descent;
        }
        canvas.translate(x, transY);
        b.draw(canvas);
        canvas.restore();
    }

    private Drawable getCachedDrawable() {
        WeakReference<Drawable> wr = mDrawableRef;
        Drawable d = null;
        if (wr != null)
            d = wr.get();
        if (d == null) {
            d = getDrawable();
            mDrawableRef = new WeakReference<Drawable>(d);
        }
        return d;
    }

    private WeakReference<Drawable> mDrawableRef;
}
  1. 抽象方法getDrawable()告訴子類需要提供一個(gè)Drawable用來繪制;
  2. getSize方法中,通過設(shè)置FontMetricsInt,從而使得替代字符序列的baseline和圖片的尾部對齊,而替代字符序列的垂直高度就為圖片的高度;
  3. draw方法中,需要繪制圖片的其實(shí)x坐標(biāo)很明確就是x,y坐標(biāo)可以通過多種方式獲取,在baseline對齊的情況下可以等于top,也可以等于y-b.getBounds().bottom,還可以等于bottom-b.getBounds().bottom-descent,各種方法都可以。

在Android系統(tǒng)中,提供了一個(gè)ImageSpan繼承了DynamicDrawableSpan,實(shí)現(xiàn)了通過多種方式生成Drawable。

6 相關(guān)鏈接

Textview圖文基礎(chǔ)
段落級span
字符級span
自定義span

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,319評論 25 708
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,688評論 19 139
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法,內(nèi)部類的語法,繼承相關(guān)的語法,異常的語法,線程的語...
    子非魚_t_閱讀 34,823評論 18 399
  • 東風(fēng)睡獅渾然醒, 一聲低吼世界驚。 主導(dǎo)全球新秩序, 扼制霸權(quán)斬幽靈。 中華巨輪已揚(yáng)帆, 乘風(fēng)破浪萬里行。 十三億...
    5988閱讀 2,559評論 0 0
  • 今天已經(jīng)是第三周的第一天,在經(jīng)歷了十幾天的一個(gè)跟進(jìn)后,基本上慢慢有了一些進(jìn)展。 今天又講到了所有關(guān)于成交的所有邏輯...
    馬春穎閱讀 333評論 0 0

友情鏈接更多精彩內(nèi)容