参考博客原链

四个主要接口

  • 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

CharacterStyle

Spans Hierachy梳理 - 图1

ParagraphStyle

Spans Hierachy梳理 - 图2

UpdateAppearance/UpdateLayout

Spans Hierachy梳理 - 图3

工作机制

LAYOUT

当在 TextView中设置文字时,其使用 Layout 管理整个文字的渲染
Layout 类包含了一个 boolean参数 mSpannedText : true 当text是 Spanned (SpannableString implements Spanned)的子类。该类值处理ParagraphStyle类型的Spans

draw)方法相关内部调用了两个其他方法:

  • drawBackground

对每一行的text,如果有对每一行设置设置 LineBackgroundSpan,就会调用LineBackgroundSpan#drawBackground)

  • drawText

对每一行的text,计算 LeadingMarginSpan and LeadingMarginSpan2 并且会在需要的时候调用LeadingMarginSpan#drawLeadingMargin) . 同样会在使用AlignmentSpan 决定text的alignment是调用. 最后,如果当前的行是 spanned, Layout 将会调用 TextLine#draw 方法(每一行对应创建一个TextLine)

TEXTLINE

android.text.TextLine 的文档描述:代表一行有样式的text,负责按实际视觉顺序测绘和渲染

TextLine类包含了3个Spans集合:

  • MetricAffectingSpan set

  • CharacterStyle set

  • ReplacementSpan set

核心的方法是 TextLine#handleRun . 在此是所有Spans被用于渲染text的地方,根据 Span的类型,TextLine会调用:

FONTMETRICS

关于文字的度量,有如下图:
Spans Hierachy梳理 - 图4
ascent: 基准线和上边距之间部分
descent: 基准线之下部分
leading: 两行基准线之间的距离

PLAYGROUND

无序列表

  1. //15是中间间距
  2. span = new BulletSpan(15, Color.BLACK);

引用Span

  1. //默认只能设置颜色,不能设置gap
  2. span = new QuoteSpan(Color.RED);

        ALIGN_NORMAL,
        ALIGN_OPPOSITE,
        ALIGN_CENTER,
        /** @hide */
        ALIGN_LEFT,
        /** @hide */
        ALIGN_RIGHT,
span = new AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER);

下和上标注,通过 TextPaint#baselineShift 来调整标注的位置

<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>

对文字设置绝对的大小

span = new AbsoluteSizeSpan(24, true);

设置相对于其他文字的大小比例

span = new RelativeSizeSpan(2.0f);

运行设置一个android.graphics.MaskFilter 在文字上

//高斯模糊某块文字
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));

FLAG误解

Span对flag的设置分四类

  • Spanned.SPAN_INCLUSIVE_EXCLUSIVE

  • Spanned.SPAN_INCLUSIVE_INCLUSIVE

  • Spanned.SPAN_EXCLUSIVE_EXCLUSIVE

  • Spanned.SPAN_EXCLUSIVE_INCLUSIVE

之前的理解是指对是否包含start,end的index位置,但实际表达的意思是 在设置此flag后,后输入的文字是否会受到影响. 后输入的文字可以指start前,或者end后,一般都是使用Spanned.SPAN_EXCLUSIVE_EXCLUSIVE稳妥

�高阶用法

ANIMATE COLOR

ForegroundColorSpan是 只读的,因此在初始化完成后就不能对其更改。因此继承自实现ForegroundColorSpan去改变属性

public class MutableForegroundColorSpan extends ForegroundColorSpan {

    private int mAlpha = 255;
    private int mForegroundColor;

    public MutableForegroundColorSpan(int alpha,int color) {
        super(color);
        this.mAlpha = alpha;
        this.mForegroundColor = color;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        super.writeToParcel(dest, flags);
        dest.writeInt(mAlpha);
        dest.writeInt(mForegroundColor);
    }

    @Override
    public void updateDrawState(TextPaint ds) {
        super.updateDrawState(ds);
        ds.setColor(getForegroundColor());
    }

    @Override
    public int getForegroundColor() {
        return Color.argb(mAlpha,Color.red(mForegroundColor), Color.green(mForegroundColor), Color.blue(mForegroundColor));
    }

    public void setAlpha(@IntRange(from = 0,to = 255) int alpha){
        this.mAlpha = alpha;
    }

    public void setForegroundColor(int foregroundColor){
        this.mForegroundColor = foregroundColor;
    }
}

通过利用ObjectAnimator以及自定义属性(Property),由 SpannableString 去操作设置在 TextView的文字,也就是动态改变 MutableForegroundColorSpan 的颜色,再通过 TextView 去强制刷新

  1. 设置携带Span的 SpannableString 到 TextView
MutableForegroundColorSpan span = new MutableForegroundColorSpan(255, mTextColor);
SpannableString ss = new SpannableString("text");
ss.setSpan(span, 0, "text".length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
mText.setText(ss);

�2. 创建 MutableForegroundColorSpan 自定义属性(值为Int型),通过 set/get 方法来控制其alpha值

此触发的调用链
1.ValueAnimator#start()
2.ValueAnimator#addAnimationCallback
3.AnimationHandler#doAnimationFrame
4.ValueAnimator#doAnimationFrame
5.ValueAnimator#animateBasedOnTime
6.ObjectAnimator#animateValue -> ValueAnimator#animateValue
7.onAnimationUpdate
8.PropertyValueHolder#setAnimatedValue(Object target) #960L
9.mProperty.set(target, getAnimatedValue()) (如果property不为null,为null则用放射的方式去invoke)

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();
            }
        };
  1. 创建ObjectAnimator 去控制 MutableForegroundColorSpan 属性的变更
//颜色由Color.BLACK -> Color.RED变化
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) {
        //在此会调前,MutableForegroundColorSpan的属性已经由第二步属性了,因此这里需要重新调用onDraw方法绘制
        mText.invalidate();
    }
});
objectAnimator.setInterpolator(mSmoothInterpolator);
objectAnimator.setDuration(600);
objectAnimator.start();

�TYPE WRITER

�类似挨着输入字的动画
Spans Hierachy梳理 - 图5

主要思路就是将每个字符看成一个span, 用 progress 来控制span的 alpha值. 比如当progress = 0.5f时,则将前1/2的span设置alpha为255,后 1/2的span设置alpha为0.

具体实现的代码见

public void setAlpha(float progress) {
    int size = mSpans.size();//字符个数
    float total = 1.0f * size * progress; // 字符进度

    if(DEBUG) Log.d(TAG, "progress " + progress + " * 1.0f * size => " + total);

    for(int index = 0 ; index < size; index++) {
        MutableForegroundColorSpan span = mSpans.get(index);
        if(index > total){
            span.setAlpha(0);
        }else {
            span.setAlpha(255);
        }
    }
}