自定义Span

1 简介

之前已经讲过TextView的基础知识、段落级别的Span和字符级别的Span,分析了Android提供的一些Span的源码,这篇文字讲解如何自定义Span。
这篇文章中,由于段落级别的Span比较简单,在这不讲述这个类型的自定义Span。这篇着重讲述字符级的Span,并且结合Android提供动画机制制作出十分酷炫的动画Span。


2 FrameSpan

FrameSpan实现给相应的字符序列添加边框的效果,整体思路其实比较简单。

  1. 计算字符序列的宽度;
  2. 根据计算的宽度、上下坐标、起始坐标绘制矩形;
  3. 绘制文字

展现效果如下所示:


FrameSpan

再来看一下代码,其实代码十分简单。

public class FrameSpan extends ReplacementSpan {

    private final Paint mPaint;
    private int mWidth;

    public FrameSpan() {
        mPaint = new Paint();
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setColor(Color.BLUE);
        mPaint.setAntiAlias(true);
    }

    @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);
        canvas.drawText(text, start, end, x, y, paint);
    }
}

在这再次说明一下draw方法里面的参数的意义。

  1. canvas:用来绘制的画布;
  2. text:整个text;
  3. start:这个Span起始字符在text中的位置;
  4. end:这个Span结束字符在text中的位置;
  5. x:这个Span的其实水平坐标;
  6. y:这个Span的baseline的垂直坐标;
  7. top:这个Span的起始垂直坐标;
  8. bottom:这个Span的结束垂直坐标;
  9. paint:画笔

3 VerticalImageSpan

Google提供的ImageSpan和DynamicDrawableSpan只能实现图片和文字底部对齐或者是baseline对齐,现在VerticalImageSpan可以实现图片和文字居中对齐。


VerticalImageSpan

图中的图片保持了和文字居中对齐,现在来看看VerticalImageSpan的源码。

public class VerticalImageSpan extends ImageSpan {

    private Drawable drawable;
    public VerticalImageSpan(Drawable drawable) {
        super(drawable);
        this.drawable=drawable;
    }

    @Override
    public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fontMetricsInt) {
        Drawable drawable = getDrawable();
        if(drawable==null){
            drawable= this.drawable;
        }
        Rect rect = drawable.getBounds();
        if (fontMetricsInt != null) {
            Paint.FontMetricsInt fmPaint = paint.getFontMetricsInt();
            int fontHeight = fmPaint.bottom - fmPaint.top;
            int drHeight = rect.bottom - rect.top;

            int top = drHeight / 2 - fontHeight / 4;
            int bottom = drHeight / 2 + fontHeight / 4;

            fontMetricsInt.ascent = -bottom;
            fontMetricsInt.top = -bottom;
            fontMetricsInt.bottom = top;
            fontMetricsInt.descent = top;
        }
        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 drawable = getDrawable();
        canvas.save();
        int transY = ((bottom - top) - drawable.getBounds().bottom) / 2 + top;
        canvas.translate(x, transY);
        drawable.draw(canvas);
        canvas.restore();
    }
}

在geSize方法中通过fontMetricsInt设置从而实现图片和文字居中对齐,其实计算的根本为计算baseline的位置,因为TextView是按照baseline对齐的。
分析getSize方法可以知道这个图片的baseline为图片中央往下fontHeight / 2,这样也就实现了图片和文字的居中对齐。
draw方法用来绘制图片,绘制x坐标为span的其实坐标,绘制y坐标可以通过计算得到,具体计算请看上面的源码。


4 AnimateForegroundColorSpan

先讲述一个简单的动画Span的例子,这个动画是用来改变文字颜色的。


AnimateForegroundColorSpan

源代码如下:

private void animateColorSpan() {
    MutableForegroundColorSpan span = new MutableForegroundColorSpan(255, mTextColor);
    mSpans.add(span);

    WordPosition wordPosition = getWordPosition(mBaconIpsum);
    mBaconIpsumSpannableString.setSpan(span, wordPosition.start, wordPosition.end, 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(mBaconIpsumSpannableString);
        }
    });
    objectAnimator.setInterpolator(mSmoothInterpolator);
    objectAnimator.setDuration(600);
    objectAnimator.start();
}

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 alphaForegroundColorSpanGroup, Integer value) {
                alphaForegroundColorSpanGroup.setForegroundColor(value);
            }

            @Override
            public Integer get(MutableForegroundColorSpan span) {
                return span.getForegroundColor();
            }
        };

其实整个逻辑比较简单,通过Property不断给span更换颜色,然后动画update的时候给TextView重新设置Span。


5 RainbowSpan

彩虹样的Span,其实实现起来也是很简单的,主要是用到了Paint的Shader技术,效果如下所示:



源代码如下所示:

private static class RainbowSpan extends CharacterStyle implements UpdateAppearance {
private final int[] colors;

public RainbowSpan(Context context) {
  colors = context.getResources().getIntArray(R.array.rainbow);
}

@Override
public void updateDrawState(TextPaint paint) {
  paint.setStyle(Paint.Style.FILL);
  Shader shader = new LinearGradient(0, 0, 0, paint.getTextSize() * colors.length, colors, null,
      Shader.TileMode.MIRROR);
  Matrix matrix = new Matrix();
  matrix.setRotate(90);
  shader.setLocalMatrix(matrix);
  paint.setShader(shader);
}
}

由于paint使用shader是从上到下进行绘制,因此这里需要用到矩阵,然后将矩阵旋转90度。


6 AnimatedRainbowSpan

AnimatedRainbowSpan

如果要实现一个动画的彩虹样式,那么该如何实现呢?
其实结合上面的RainbowSpan和AnimateForegroundColorSpan的例子便可以实现AnimatedRainbowSpan。
实现思路:通过ObjectAnimator动画调整RainbowSpan中矩阵的平移,从而实现动画彩虹的效果。
代码如下所示:

public class AnimatedRainbowSpanActivity extends Activity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_animated_rainbow_span);

    final TextView textView = (TextView) findViewById(R.id.text);
    String text = textView.getText().toString();

    AnimatedColorSpan span = new AnimatedColorSpan(this);

    final SpannableString spannableString = new SpannableString(text);
    String substring = getString(R.string.animated_rainbow_span).toLowerCase();
    int start = text.toLowerCase().indexOf(substring);
    int end = start + substring.length();
    spannableString.setSpan(span, start, end, 0);

    ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(
        span, ANIMATED_COLOR_SPAN_FLOAT_PROPERTY, 0, 100);
    objectAnimator.setEvaluator(new FloatEvaluator());
    objectAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
      @Override
      public void onAnimationUpdate(ValueAnimator animation) {
        textView.setText(spannableString);
      }
    });
    objectAnimator.setInterpolator(new LinearInterpolator());
    objectAnimator.setDuration(DateUtils.MINUTE_IN_MILLIS * 3);
    objectAnimator.setRepeatCount(ValueAnimator.INFINITE);
    objectAnimator.start();
  }

  private static final Property<AnimatedColorSpan, Float> ANIMATED_COLOR_SPAN_FLOAT_PROPERTY
      = new Property<AnimatedColorSpan, Float>(Float.class, "ANIMATED_COLOR_SPAN_FLOAT_PROPERTY") {
    @Override
    public void set(AnimatedColorSpan span, Float value) {
      span.setTranslateXPercentage(value);
    }
    @Override
    public Float get(AnimatedColorSpan span) {
      return span.getTranslateXPercentage();
    }
  };

  private static class AnimatedColorSpan extends CharacterStyle implements UpdateAppearance {
    private final int[] colors;
    private Shader shader = null;
    private Matrix matrix = new Matrix();
    private float translateXPercentage = 0;

    public AnimatedColorSpan(Context context) {
      colors = context.getResources().getIntArray(R.array.rainbow);
    }

    public void setTranslateXPercentage(float percentage) {
      translateXPercentage = percentage;
    }

    public float getTranslateXPercentage() {
      return translateXPercentage;
    }

    @Override
    public void updateDrawState(TextPaint paint) {
      paint.setStyle(Paint.Style.FILL);
      float width = paint.getTextSize() * colors.length;
      if (shader == null) {
        shader = new LinearGradient(0, 0, 0, width, colors, null,
            Shader.TileMode.MIRROR);
      }
      matrix.reset();
      matrix.setRotate(90);
      matrix.postTranslate(width * translateXPercentage, 0);
      shader.setLocalMatrix(matrix);
      paint.setShader(shader);
    }
  }
}

7 FireworksSpan

FireworksSpan

“烟火”动画是让文字随机淡入。首先,把文字切断成多个spans(例如,一个character的span),淡入spans后再淡入其它的spans。用 前面介绍的MutableForegroundColorSpan,我们将创建一组特殊的span对象。在span组调用对应的setAlpha方法,我 们随机设置每个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; }
}

我们创建一个自定义属性动画的属性去更改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.setAlpha(value);
    }
    @Override
    public Float get(FireworksSpanGroup spanGroup) {
        return spanGroup.getAlpha();
    }
};

最后,我们创建span组并使用一个ObjectAnimator给其加上动画。

final FireworksSpanGroup spanGroup = new FireworksSpanGroup();
//初始化包含多个spans的grop
//spanGroup.addSpan(span);
//给ActionBar的标题设置spans
//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)
    {
        //更新标题
        setTitle(mActionBarTitleSpannableString);
    }
});
objectAnimator.start();

8 TypeWriterSpan

TypeWriterSpan

有了上面的例子,写TypeWriterSpan就变得十分简单了。
先创建TypeWriterSpanGroup

private static final class TypeWriterSpanGroup {

    private static final boolean DEBUG = false;
    private static final String TAG = "TypeWriterSpanGroup";

    private final float mAlpha;
    private final ArrayList<MutableForegroundColorSpan> mSpans;

    private TypeWriterSpanGroup(float alpha) {
        mAlpha = alpha;
        mSpans = new ArrayList<MutableForegroundColorSpan>();
    }

    public void addSpan(MutableForegroundColorSpan span) {
        span.setAlpha((int) (mAlpha * 255));
        mSpans.add(span);
    }

    public void setAlpha(float alpha) {
        int size = mSpans.size();
        float total = 1.0f * size * alpha;

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

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

            if(DEBUG) Log.d(TAG, "alpha span(" + index + ") => " + alpha);
        }
    }

    public float getAlpha() {
        return mAlpha;
    }
}

添加Span

private TypeWriterSpanGroup buildTypeWriterSpanGroup(int start, int end) {
    final TypeWriterSpanGroup group = new TypeWriterSpanGroup(0);
    for(int index = start ; index <= end ; index++) {
        MutableForegroundColorSpan span = new MutableForegroundColorSpan(0, Color.BLACK);
        mSpans.add(span);
        group.addSpan(span);
        mBaconIpsumSpannableString.setSpan(span, index, index + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    }
    return group;
}

添加动画

private void animateTypeWriter() {
    TypeWriterSpanGroup spanGroup = buildTypeWriterSpanGroup(0, mBaconIpsum.length() - 1);
    ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(spanGroup, TYPE_WRITER_GROUP_ALPHA_PROPERTY, 0.0f, 1.0f);
    objectAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            //refresh
            mText.setText(mBaconIpsumSpannableString);
        }
    });
    objectAnimator.setInterpolator(mTypeWriterInterpolator);
    objectAnimator.setDuration(5000);
    objectAnimator.start();
}

添加动画属性变化器

 private static final Property<TypeWriterSpanGroup, Float> TYPE_WRITER_GROUP_ALPHA_PROPERTY =
        new Property<TypeWriterSpanGroup, Float>(Float.class, "TYPE_WRITER_GROUP_ALPHA_PROPERTY") {

            @Override
            public void set(TypeWriterSpanGroup spanGroup, Float value) {
                spanGroup.setAlpha(value);
            }

            @Override
            public Float get(TypeWriterSpanGroup spanGroup) {
                return spanGroup.getAlpha();
            }
        };

9 相关链接

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,859评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,772评论 2 374
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,956评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,729评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,611评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,441评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,850评论 3 388
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,485评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,770评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,803评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,598评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,434评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,856评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,065评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,365评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,852评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,065评论 2 338

推荐阅读更多精彩内容