字符级Span解析

作者: 风从影 | 来源:发表于2016-12-09 18:21 被阅读2739次

    1 简介

    之前已经讲过TextView的基础知识,和段落级别的Span,现在在这进一步进行讲解,这篇文字主要讲解如何给TextView设置字符级别的Span。如果一个Span想要影响段落层次的文本格式,则需要继承CharacterStyle。


    2 CharacterStyle

    CharacterStyle是个抽象类,字符级别的Span都需要继承这个类,这个类里面有一个抽象方法:

    public abstract void updateDrawState(TextPaint tp)
    

    通过改变TextPaint的属性就可以得到不同的展现形式。在这个抽象类里面还有一个静态方法:

    public static CharacterStyle wrap(CharacterStyle cs)
    

    一个CharacterStyle类型的Span只能给一个Spaned片段使用,如果想这个Span给多个片段使用可以使用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();
        }
    }
    

    不难发现其实就是复制了一个CharacterStyle。


    3 UpdateAppearance

    如果一个Span修改字符级别的文本外观,则实现UpdateAppearance。


    UpdateAppearance

    上面的Span都实现了UpdateAppearance接口,上面的诸多Span都是通过updateDrawState(TextPaint ds)方法来实现相应的效果。

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

    点击后通过updateDrawState(TextPaint ds)方法改变字体外观,onClick(View widget)则交给子类实现相应的逻辑。
    MaskFilterSpan中ds.setMaskFilter(mFilter)可以给字体设置模糊和浮雕效果。

    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

    如果一个Span修改字符级文本度量|大小,则实现UpdateLayout。在Android源码中,只有MetricAffectingSpan实现了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),这个方法可以交给子类实现,从而实现字体外观的改变。在MetricAffectingSpan类中定义了一个抽象方法updateMeasureState(TextPaint p),继承MetricAffectingSpan类的子类可以实现这个抽象方法,从而实现对字体大小的改变。在MetricAffectingSpan中同样也提供了一个Passthrough的类,从而完成CharacterStyle中定义的wrap方法。
    接下来分别对MetricAffectingSpan的实现类进行讲述。


    4.1 SubscriptSpan和SuperscriptSpan

    SubscriptSpan和SuperscriptSpan实现字体的上下标展示,效果如下面的图片所示:


    SubscriptSpan
    SuperscriptSpan

    其实这两个Span的实现特别简单,通过查看这两个类的实现,能够帮助我们对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用来改变相应字符的字体大小。

    /**
    * size: 大小
    * dip: false,size单位为px,true,size单位为dip(默认为false)。
    */
    //设置文字大小为24dp
    span = new AbsoluteSizeSpan(24, true);
    
    AbsoluteSizeSpan
    //设置文字大小为大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轴方向上缩放字符集。

    //设置水平方向上放大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都可以字体的样式进行改变,StyleSpan可以对字体设置bold或者italic的字符样式,TypefaceSpan可以对字体设置其他的样式,TextAppearanceSpan通过xml文件从而对字体进行设置。

    //设置bold+italic的字符样式
    span = new StyleSpan(Typeface.BOLD | Typeface.ITALIC);
    
    StyleSpan
    //设置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用来对字体设置不同的地区,由于不同地区的字体会导致字体大小的变化,因此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比较复杂因此在这单独讲解。在ReplacementSpan里新增加了两个抽象方法,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)返回所占的宽度。其实根据getSize方法的参数我们能够计算原本那些字符所占用的宽度,计算方法如下:

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

    通过这个宽度我们可以给文字制作相应的效果。
    抽象方法draw,可以让我们在合适的区域绘制相应的图形,start和end分别为span作用的起始和结束字符的index,x为起始横坐标,y为baseline对应的坐标,top为起始高度,bottom为结束高度。
    在Android提供的源码里面提供了一个抽象类DynamicDrawableSpan来继承ReplacementSpan,而DynamicDrawableSpan又有一个子类ImageSpan。


    5.1 DynamicDrawableSpan

    DynamicDrawableSpan是一个抽象类,DynamicDrawableSpan可以做到使用Drawable替代相对应的字符序列,展现效果如下所示:


    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()告诉子类需要提供一个Drawable用来绘制;
    2. getSize方法中,通过设置FontMetricsInt,从而使得替代字符序列的baseline和图片的尾部对齐,而替代字符序列的垂直高度就为图片的高度;
    3. draw方法中,需要绘制图片的其实x坐标很明确就是x,y坐标可以通过多种方式获取,在baseline对齐的情况下可以等于top,也可以等于y-b.getBounds().bottom,还可以等于bottom-b.getBounds().bottom-descent,各种方法都可以。

    在Android系统中,提供了一个ImageSpan继承了DynamicDrawableSpan,实现了通过多种方式生成Drawable。

    6 相关链接

    Textview图文基础
    段落级span
    字符级span
    自定义span

    相关文章

      网友评论

      • Jsonjia:你好,你这个假如字符串中有个是数字,就不好使了,比如String s =“哈哈哈哈12344”,这样就不行了,这样怎么解决
      • hubsul_nvtag:请教一下 那个竖着的textview怎么做呢?可不可以做个竖着控件的教程呢😀😀
        hubsul_nvtag: @风从影 嗯嗯 太感谢您了 我在网上搜 发现这个控件的需求还是挺大的😬😬
        风从影:@nvtag 我研究下~,有成果告诉你,之前没研究这个~
      • 46c32675c38b:😁请教一个问题,最近项目中有一个需求:textview中的每一行文字都需要有单独的背景,是一种过渡色的背景,backgroundcolorspan只能设置单一的颜色,您有什么思路吗,希望能指点一下
        风从影:@JXHIUUI 也可以啊,你先查看段落级span文章,里面有讲如何设置每一行字的颜色,关于渐变色你可以参看自定义span里面的彩虹色的实现。
      • 46c32675c38b:真是用心了😀
        风从影:@JXHIUUI :yum:

      本文标题:字符级Span解析

      本文链接:https://www.haomeiwen.com/subject/ipqymttx.html