欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

TextView图文混排

程序员文章站 2022-04-25 07:50:04
...

TextView图文混排

简介

在使用TextView的时候,我们经常需要在TextView中进行图文混排,比如在QQ中聊天的消息中就会展现表情,比如在微博中,用户发出的微博里面经常会带有各种小图标和链接。

Android官方对TextView的图文混排提供了支持,我们可以从以下三种方式实现TextView的图文混排:

  1. 在TextView的XML布局文件中添加Compound Drawable属性;

  2. 在对TextView设置字符串时,可以设置Html类型的字符串。Html.fromHtml()方法可以对Html的字符串进行处理,从而使得Html类型的内容满足TextView的要求。在给TextView设置Html类型的内容时,还可以传入一个ImageGetter,从而对Html类型内容中的图片进行处理;

  3. 对TextView设置内容的时候,可以传入CharSequence类型,而一些CharSequence类型可以利用CharacterStyle进行修饰,从而展现出丰富多彩的内容。CharacterStyle拥有很多子类(BackgroundColorSpan,ClickableSpan,ImageSpan,TypefaceSpan等),可以产生出各种各样的效果。

对于以上三种形式有着不同的使用场景:

  • 一般情况下我们希望在字符串的上、下、左、右方向添加图片,这种需求简单明确,使用第1种方式(Compound Drawable)就可以了。

  • 有时候我们希望TextView中含有不同颜色的字体,这时候可以使用第二种方式(Html.fromHtml()),只需要在不同颜色的字体上设置相应的颜色即可。第二种方式也可以处理TextView中的链接情况,第2中方式还可以在TextView中显示图片

  • 第3种方式可以对TextView中的显示内容进行各种变换,可以对字体背景进行设置,可以对字体颜色进行设置,可以在内容中加入图片,可以进行的操作非常多,但是同时相应的处理也较为复杂。

下面将会对以上的三种方式分别进行讲述,希望能够让大家更好地掌握TextView的使用。

Compound Drawable

一般情况

关键词:android:drawableLeft、android:drawableRight、android:drawableBottom
一般情况下,我们只需要对TextView的上下左右设置固定的图片,这时候只需要像下面一样编写XML文件就可以实现了。

<TextView
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="@string/animation"
  android:drawableLeft="@drawable/rotating_loading"
  android:drawableRight="@drawable/animated_wifi"
  android:drawableBottom="@drawable/animated_clock"/>

TextView图文混排

图片动起来

将2.1中的左、右、下三个方向的drawable转为动画drawable,则可以实现在TextView中显示动画的效果。首先我们需要得到TextView四周的drawable,判断drawable是否实现Animatable,如果实现了则启动相应的动画效果。

private void startAnimation(TextView textView) {
  Drawable[] drawables = textView.getCompoundDrawables();
  for (Drawable drawable : drawables) {
    if (drawable != null && drawable instanceof Animatable) {
      ((Animatable) drawable).start();
    }
  }
}

三个动画的drawable xml:

<!-- res/drawable/rotating_loading.xml -->
<animated-rotate
  android:pivotX="50%"
  android:pivotY="50%"
  android:drawable="@drawable/ic_loading"
  android:duration="500" />
<!-- res/drawable/animated_wifi.xml -->
<animation-list>
  <item android:drawable="@drawable/ic_wifi_0" android:duration="250" />
  <item android:drawable="@drawable/ic_wifi_1" android:duration="250" />
  <item android:drawable="@drawable/ic_wifi_2" android:duration="250" />
  <item android:drawable="@drawable/ic_wifi_3" android:duration="250" />
</animation-list>
<!-- res/drawable/animated_clock.xml -->
<animated-vector android:drawable="@drawable/clock">
  <target android:name="hours" android:animation="@anim/hours_rotation" />
  <target android:name="minutes" android:animation="@anim/minutes_rotation" />
</animated-vector>

TextView图文混排

Html Content

关键词:<Data>![CDATA[

tv.setText(Html.fromHtml(
        days + "<font color='#555555'></font>" + 
        hours + "<font color='#555555'></font>" + 
        minutes + "<font color='#555555'></font>" + 
        second + "<font color='#555555'></font>"));

不同字体颜色

一些情况下,TextView中可能不同的文字有着不同的颜色,这个时候处理方式2是非常适用的。

<string name="different_color_text"><Data>
<![CDATA[今日已有<font color="#f0717e">1/font>人签到,日榜单排在第<font color="#f0717e">1</font>名]]>
</Data></string>

这个时候只需要直接对TextView设置上面的内容即可,展现效果如下所示:

TextView图文混排

图片和链接

在一些情况下,TextView中含有图片和链接,这时候使用处理方式2也是个不错的选择。

Html代码:

<h1>Hello World</h1>
Here is an
[站外图片上传中……(13)]<i>octopus</i>.<br>
And here is a
<a href="http://d.android.com">link</a>

android字符串:
<a href=”

<string name="from_html_text">
<![CDATA[
<h1>Hello World</h1>
Here is an
[站外图片上传中……(14)]<i>octopus</i>.<br>
And here is a
<a href="http://d.android.com">link</a>.
]]>
</string>

给TextView设置内容:

String html = getString(R.string.from_html_text);
/*让链接可点击*/
textView.setMovementMethod(LinkMovementMethod.getInstance());
/*ResouroceImageGetter用来处理TextView中的图片*/
textView.setText(Html.fromHtml(html, new ResouroceImageGetter(this), null));

ResouroceImageGetter的作用就是根据传过来的src返回drawable,继承Html.ImageGetter,它的代码如下:

private static class ResouroceImageGetter implements Html.ImageGetter {
  // Constructor takes a Context  
  public Drawable getDrawable(String source) {
    int path = context.getResources().getIdentifier(source, "drawable", BuildConfig.APPLICATION_ID);
    Drawable drawable = context.getResources().getDrawable(path);
    drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
    return drawable;
  }
}

通过上面的代码就完成了TextView中链接和图片的设置,展示效果如下面所示:

TextView图文混排

需要注意的是:要让TextView里面的链接生效,需要对TextView进行设置。setMovementMethod

textView.setMovementMethod(LinkMovementMethod.getInstance());

但是上面的代码会造成当TextView设置最大行数失败,当超过最大行数的时候会造成TextView里面的内容可以滑动,在下面的内容里面会讲解如何解决这个问题。

Span方式

整体机理

TextView可以通过下面的方法设置内容,一般情况下我们会给TextView设置String类型的内容,String类型是实现了CharSequence接口的。

setText(CharSequence text)

在Google的android官方网站上我们可以得到CharSequence接口的相关内容。

CharSequence
TextView图文混排

CharSequence方法
TextView图文混排

在这里我们需要了解spanned和spannable,其实这两个都是接口,而且spannable是继承spanned。为了方便理解,这里先讲解spannable接口。

spannable

在spannable接口里面定义了下面两个抽象方法:
TextView图文混排

  • setSpan(Object what, int start, int end, int flags),在这个方法中what通常指各种类型的span(ImageSpan、URLSpan、ClickableSpan等),该方法可以将spannable里面从start到end的内容替换为指定的span类型的内容。其中flags是指设定start和end的方式,在下面的内容中会讲到。

  • removeSpan(Object what),在这个方法中what也是指各种类型的span,这个方法是在spannable中移除特定的span。

flags

关于上面提到的flags通常使用的是以下4种:

  1. Spanned.SPAN_EXCLUSIVE_EXCLUSIVE(前后都不包括);

  2. Spanned.SPAN_INCLUSIVE_EXCLUSIVE(前面包括,后面不包括);

  3. Spanned.SPAN_EXCLUSIVE_INCLUSIVE(前面不包括,后面包括);

  4. Spanned.SPAN_INCLUSIVE_INCLUSIVE(前后都包括)。

一般来说通常使用的是Spanned.SPAN_EXCLUSIVE_EXCLUSIVE,以免影响前后插入的文本样式。

实际上如下操作,以上四种没有区别:

SpannableStringBuilder spannableString = new SpannableStringBuilder();
spannableString.append("0123456");
spannableString.setSpan(new ForegroundColorSpan(Color.parseColor("#FF0000")), 1, 2,
        Spannable.SPAN_INCLUSIVE_EXCLUSIVE);//不管是哪一种都不会影响0跟2的颜色
textView.setText(spannableString);

TextView图文混排

当调用builder.insert()方法时,Spannable标识就起作用了。

SpannableStringBuilder spannableString = new SpannableStringBuilder();
spannableString.append("0123456");
spannableString.setSpan(new ForegroundColorSpan(Color.parseColor("#FF0000")), 1, 2,
        Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
spannableString.insert(1,"a");//插入文本位置要紧挨span样式 如果不是,那么设置的样式不适用a,后置包括同理。  
textView.setText(spannableString);

设置的是前置包括,所以插入前面样式有效
TextView图文混排

插入位置改变。因为设置的是后置不包括,即使插入文本位置紧挨span样式,样式也无效。

spannableString.insert(2,"a");

效果如下:
TextView图文混排

spanned

在spanned里面提供了下面5个抽象方法:

TextView图文混排

  • getSpanEnd(Object tag),这个方法用来获取一个span的结束位置。

  • getSpanFlags(Object tag),这个方法用来获取这个span设置的flag。

  • getSpanStart(Object tag),这个方法用来获取一个span开始的位置。

  • getSpans(int start, int end, Class type),这个方法用来获取从start到end的位置上所有的特定类型的span,比如说我么希望找到某一段里面所有的ClickableSpan就可以使用这个方法。

  • nextSpanTransition(int start, int limit, Class type),这个方法会在你指定的文本范围内,返回下一个你指定的Span类型的开始位置,依照这个方法,我们就可以逐层扫描指定的 Span ,而不用同时考虑其他类型的Span的影响,十分有用。

SpannableString、SpannableStringBuilder

接下来讲述的是SpannableString和SpannableStringBuilder两个类,这两个类实现了Spannable接口,实现了接口里面定义的方法。SpannableString和SpannableStringBuilder的关系类似于String和StringBuilder的关系。SpannableStringBuilder和StringBuilder一样实现了Appendable接口,从而可以往里面不断append内容。在使用Span实现TextView图文混排的过程中,一般来说我们都会使用SpannableString和SpannableStringBuilder中的一个。

流程

所以对于使用Span方式实现TextView图文混排的整体流程是:

  1. 创建一个SpannableString或者SpannableStringBuilder对象;

  2. 利用setSpan(Object what, int start, int end, int flags)方法,将SpannableString或者SpannableStringBuilder对象的某些位置的内容替换为具体类型的Span;

  3. 利用TextView的setText(CharSequence text)方法将SpannableString或者SpannableStringBuilder对象进行展示。

SpannableStringBuilder

Google官方的介绍:

This is the class for text whose content and markup can both be changed.
(这是一个内容和标记都可以更改的文本类)

SpannableStringBuilder有个亲兄弟——SpannableString。SpannableStringBuilder和SpannableString的区别类似与StringBuilder、String,就是SpannableStringBuilder可以拼接,而SpannableString不可拼接。
TextView图文混排

由图中可以看出,他们都实现了CharSequence,因此,他们可以直接在TextViewsetText中使用

不同类型的Span

Span 效果
BackgroundColorSpan 文本背景色
ForegroundColorSpan 文本颜色
MaskFilterSpan 修饰效果,如模糊(BlurMaskFilter)浮雕
RasterizerSpan 光栅效果
StrikethroughSpan 删除线
SuggestionSpan 相当于占位符
UnderlineSpan 下划线
AbsoluteSizeSpan 文本字体(绝对大小)
DynamicDrawableSpan 设置图片,基于文本基线或底部对齐。
ImageSpan 图片
RelativeSizeSpan 相对大小(文本字体)
ScaleXSpan 基于x轴缩放
StyleSpan 字体样式:粗体、斜体等
SubscriptSpan 下标(数学公式会用到)
SuperscriptSpan 上标(数学公式会用到)
TextAppearanceSpan 文本外貌(包括字体、大小、样式和颜色)
TypefaceSpan 文本字体
URLSpan 文本超链接
ClickableSpan 点击事件

以下是Span的一些规则:

  • 如果一个Span影响字符级的文本格式,则继承CharacterStyle

  • 如果一个Span影响段落层次的文本格式,则实现ParagraphStyle

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

  • 如果一个Span修改字符级文本度量|大小,则实现UpdateLayout

CharacterStyle:

TextView图文混排

ParagraphStyle:

TextView图文混排

UpdateAppearance:

TextView图文混排

UpdateLayout:
TextView图文混排

实例

TextView图文混排

int falg = Spannable.SPAN_EXCLUSIVE_EXCLUSIVE;
SpannableStringBuilder spanStr = new SpannableStringBuilder();

String s1 = "前景";//ForegroundColorSpan
spanStr.append(s1);
spanStr.setSpan(new ForegroundColorSpan(Color.RED), spanStr.length() - s1.length(), spanStr.length(), falg);

String s2 = "背景";//BackgroundColorSpan
spanStr.append(s2);
spanStr.setSpan(new BackgroundColorSpan(Color.parseColor("#009ad6")), spanStr.length() - s2.length(), spanStr.length(), falg);

String s3 = "大小";//AbsoluteSizeSpan
spanStr.append(s3);
spanStr.setSpan(new AbsoluteSizeSpan(100), spanStr.length() - s3.length(), spanStr.length(), falg);

String s4 = "粗体";//StyleSpan
spanStr.append(s4);
spanStr.setSpan(new StyleSpan(Typeface.BOLD), spanStr.length() - s4.length(), spanStr.length(), falg);

String s5 = "斜体";//StyleSpan
spanStr.append(s5);
spanStr.setSpan(new StyleSpan(Typeface.ITALIC), spanStr.length() - s5.length(), spanStr.length(), falg);

String s6 = "删除线";//StrikethroughSpan
spanStr.append(s6);
spanStr.setSpan(new StrikethroughSpan(), spanStr.length() - s6.length(), spanStr.length(), falg);

String s7 = "下划线";//UnderlineSpan
spanStr.append(s7);
spanStr.setSpan(new UnderlineSpan(), spanStr.length() - s7.length(), spanStr.length(), falg);

String s8 = "图片";//ImageSpan
spanStr.append(s8);
spanStr.setSpan(new ImageSpan(context, R.mipmap.ic_launcher), spanStr.length() - s8.length(), spanStr.length(), falg);

String s9 = "点击";//ClickableSpan 
spanStr.append(s9);
ClickableSpan clickableSpan = new ClickableSpan() {
    @Override
    public void onClick(View view) {
        Toast.makeText(context, "请不要点我", Toast.LENGTH_SHORT).show();
    }
};
spanStr.setSpan(clickableSpan, spanStr.length() - s9.length(), spanStr.length(), falg);
textView.setMovementMethod(LinkMovementMethod.getInstance());

String s10 = "URL";//URLSpan
spanStr.append(s10);
spanStr.setSpan(new URLSpan("https://www.baidu.com/"), spanStr.length() - s10.length(), spanStr.length(), falg);

textView.setText(spanStr);

ParagraphStyle(段落级Span)

ParagraphStyle是一个接口,通过查看Android源码,我们发现这个接口里面什么方法也没有定义,因此,我们可以认为,这个接口无非是标识实现这个接口的Span为段落级别的Span。

在Android源码中又继续定义了几个接口实现了ParagraphStyle接口。
TextView图文混排

  1. LeadingMarginSpan:用来处理像word中项目符号一样的接口;

  2. AlignmentSpan:用来处理整个段落对其的接口;

  3. LineBackgroundSpan:用来处理一行的背景的接口;

  4. LineHeightSpan:用来处理一行高度的接口;

  5. TabStopSpan:用来将字符串中的”\t”替换成相应的空行;

LeadingMarginSpan

LeadingMarginSpan用来控制整个段落左边或者右边显示某些特定效果,里面有两个接口方法。

/**
 * Returns the amount by which to adjust the leading margin. Positive values
 * move away from the leading edge of the paragraph, negative values move
 * towards it.
 * 
 * @param first true if the request is for the first line of a paragraph,
 * false for subsequent lines
 * @return the offset for the margin.
 */
//first为是否为第一行,返回值为整个段落偏移的距离
public int getLeadingMargin(boolean first);
/**
 * Renders the leading margin.  This is called before the margin has been
 * adjusted by the value returned by {@link #getLeadingMargin(boolean)}.
 * 
 * @param c the canvas
 * @param p the paint. The this should be left unchanged on exit.
 * @param x the current position of the margin
 * @param dir the base direction of the paragraph; if negative, the margin
 * is to the right of the text, otherwise it is to the left.
 * @param top the top of the line
 * @param baseline the baseline of the line
 * @param bottom the bottom of the line
 * @param text the text
 * @param start the start of the line
 * @param end the end of the line
 * @param first true if this is the first line of its paragraph
 * @param layout the layout containing this line
 */
//在偏移的位置里面进行各种效果绘制
public void drawLeadingMargin(Canvas c, Paint p,
                                  int x, int dir,
                                  int top, int baseline, int bottom,
                                  CharSequence text, int start, int end,
                                  boolean first, Layout layout);

LeadingMarginSpan2还多规定了一个方法。

/**
 * Returns the number of lines of the paragraph to which this object is
 * attached that the "first line" margin will apply to.
 */
//控制影响的行数
public int getLeadingMarginLineCount();

下面通过三个LeadingMarginSpan的实现来具体说明。

BulletSpan

先来看BulletSpan实现的效果,效果如下图所示:

TextView图文混排

通过上面的图片可以看见整个段落右移了一段距离,然后在移动留下的空间处绘制了一个小圆点。

具体来看代码,BulletSpan代码如下所示:

public int getLeadingMargin(boolean first) {
    return 2 * BULLET_RADIUS + mGapWidth;
}

public void drawLeadingMargin(Canvas c, Paint p, int x, int dir,
                              int top, int baseline, int bottom,
                              CharSequence text, int start, int end,
                              boolean first, Layout l) {
    if (((Spanned) text).getSpanStart(this) == start) {
        Paint.Style style = p.getStyle();
        int oldcolor = 0;

        if (mWantColor) {
            oldcolor = p.getColor();
            p.setColor(mColor);
        }

        p.setStyle(Paint.Style.FILL);

        if (c.isHardwareAccelerated()) {
            if (sBulletPath == null) {
                sBulletPath = new Path();
                // Bullet is slightly better to avoid aliasing artifacts on mdpi devices.
                sBulletPath.addCircle(0.0f, 0.0f, 1.2f * BULLET_RADIUS, Direction.CW);
            }

            c.save();
            c.translate(x + dir * BULLET_RADIUS, (top + bottom) / 2.0f);
            c.drawPath(sBulletPath, p);
            c.restore();
        } else {
            c.drawCircle(x + dir * BULLET_RADIUS, (top + bottom) / 2.0f, BULLET_RADIUS, p);
        }

        if (mWantColor) {
            p.setColor(oldcolor);
        }

        p.setStyle(style);
    }
}

第一个方法无论是否是第一行都返回了偏移距离为2 * BULLET_RADIUS + mGapWidth,因此整个段落都移动了相应的距离。

第二个方法绘制了一个圆形,((Spanned) text).getSpanStart(this) == start判断了这一行的起始位置是否是整个Span的起始位置,如果是则绘制圆形,如果把这个判断去掉,那么每一行都将绘制小圆形。


QuoteSpan

先看实现的效果,实现的效果如下所示:
TextView图文混排

QuoteSpan代码如下所示:

public int getLeadingMargin(boolean first) {
    return STRIPE_WIDTH + GAP_WIDTH;
}

public void drawLeadingMargin(Canvas c, Paint p, int x, int dir,
                              int top, int baseline, int bottom,
                              CharSequence text, int start, int end,
                              boolean first, Layout layout) {
    Paint.Style style = p.getStyle();
    int color = p.getColor();

    p.setStyle(Paint.Style.FILL);
    p.setColor(mColor);

    c.drawRect(x, top, x + dir * STRIPE_WIDTH, bottom, p);

    p.setStyle(style);
    p.setColor(color);
}

上面的代码就十分清晰了,每行都偏移相应距离,然后每行都绘制矩形,就连成了一条竖线。


TextRoundSpan

如果希望做到两端文字环绕图片的效果,其实可以考虑编写Span实现LeadingMarginSpan2。具体做法其实比较简单,相对布局中放置ImageView和TextView,然后根据ImageView的大小计算TextView需要偏移的距离和行数,整个效果就可以实现,实现的效果如下所示:

TextView图文混排

float fontSpacing=mTextView.getPaint().getFontSpacing();
lines = (int) (finalHeight/fontSpacing);
/**
 * Build the layout with LeadingMarginSpan2
 */
TextRoundSpan span = new TextRoundSpan(lines, finalWidth +10 );
class TextRoundSpan implements LeadingMarginSpan.LeadingMarginSpan2 {
  private int margin;
  private int lines;

  TextRoundSpan(int lines, int margin) {
      this.margin = margin;
      this.lines = lines;
  }

  /**
   * Apply the margin
   *
   * @param first
   * @return
   */
  @Override
  public int getLeadingMargin(boolean first) {
      if (first) {
          return margin;
      } else {
          return 0;
      }
  }

  @Override
  public void drawLeadingMargin(Canvas c, Paint p, int x, int dir,
              int top, int baseline, int bottom, CharSequence text,
              int start, int end, boolean first, Layout layout) {}

  @Override
  public int getLeadingMarginLineCount() {
      return lines;
  }
};

其实分析上面可以得出当当前行数小于等于getLeadingMarginLineCount(),getLeadingMargin(boolean first)中first的值为true。

AlignmentSpan

AlignmentSpan处理整个段落文字排列,当设置不同的排列方式,显示的效果不同。

TextView图文混排

AlignmentSpan接口中定义了一个接口方法,里面还有个Standard实现。

Layout.Alignment getAlignment();

AlignmentSpan比较简单,不多做讲述。

LineBackgroundSpan

LineBackgroundSpan用来设置每一行的背景颜色,这个和对字体设置颜色不同,具体区别如下:
TextBackgroundSpan
TextView图文混排

LineBackgroundSpan
TextView图文混排

可以看见下面图片中背景颜色是整行的。

具体代码如下:

public class MainActivity extends Activity {

    private static class MySpan implements LineBackgroundSpan {
        private final int color;

        public MySpan(int color) {
            this.color = color;
        }

        @Override
        public void drawBackground(Canvas c, Paint p, int left, int right, int top, int baseline, int bottom, CharSequence text, int start, int end, int lnum) {
            final int paintColor = p.getColor();
            p.setColor(color);
            c.drawRect(new Rect(left, top, right, bottom), p);
            p.setColor(paintColor);
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        final TextView tv = new TextView(this);
        setContentView(tv);

        tv.setText("Lines:\n", TextView.BufferType.EDITABLE);
        appendLine(tv.getEditableText(), "123456 123 12345678\n", Color.BLACK);
        appendLine(tv.getEditableText(), "123456 123 12345678\n", Color.RED);
        appendLine(tv.getEditableText(), "123456 123 12345678\n", Color.BLACK);
    }

    private void appendLine(Editable text, String string, int color) {
        final int start = text.length();
        text.append(string);
        final int end = text.length();
        text.setSpan(new MySpan(color), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    }
}

LineHeightSpan

要想熟练使用这个Span,需要对字体的高度设置有着较好的理解。
TextView图文混排

Top和Ascent之间存在的距离是考虑到了类似读音符号。Android依然会在绘制文本的时候在文本外层留出一定的边距,这就是为什么top和bottom总会比ascent和descent大一点的原因。而在TextView中我们可以通过xml设置其属性android:includeFontPadding=”false”去掉一定的边距值但是不能完全去掉。

TextView图文混排

上面图片的一行文字打印FontMetrics相应的值,如下所示:

  1. ascent:-46.38672

  2. top:-52.807617

  3. leading:0.0

  4. descent:12.207031

  5. bottom:13.549805

下面我们来看一下Android提供的DrawableMarginSpan的源码。

public class DrawableMarginSpan
implements LeadingMarginSpan, LineHeightSpan
{
    public DrawableMarginSpan(Drawable b) {
        mDrawable = b;
    }

    public DrawableMarginSpan(Drawable b, int pad) {
        mDrawable = b;
        mPad = pad;
    }

    public int getLeadingMargin(boolean first) {
        return mDrawable.getIntrinsicWidth() + mPad;
    }

    public void drawLeadingMargin(Canvas c, Paint p, int x, int dir,
                                  int top, int baseline, int bottom,
                                  CharSequence text, int start, int end,
                                  boolean first, Layout layout) {
        int st = ((Spanned) text).getSpanStart(this);
        int ix = (int)x;
        int itop = (int)layout.getLineTop(layout.getLineForOffset(st));

        int dw = mDrawable.getIntrinsicWidth();
        int dh = mDrawable.getIntrinsicHeight();

        // XXX What to do about Paint?
        mDrawable.setBounds(ix, itop, ix+dw, itop+dh);
        mDrawable.draw(c);
    }

    public void chooseHeight(CharSequence text, int start, int end,
                             int istartv, int v,
                             Paint.FontMetricsInt fm) {
        if (end == ((Spanned) text).getSpanEnd(this)) {
            int ht = mDrawable.getIntrinsicHeight();

            int need = ht - (v + fm.descent - fm.ascent - istartv);
            if (need > 0)
                fm.descent += need;

            need = ht - (v + fm.bottom - fm.top - istartv);
            if (need > 0)
                fm.bottom += need;
        }
    }

    private Drawable mDrawable;
    private int mPad;
}

DrawableMarginSpan
TextView图文混排

这个Span实现了LeadingMarginSpan和LineHeightSpan接口,实现了LeadingMarginSpan接口是为了实现段落便宜的效果,不过这里的代码存在一定的问题,因为会多次调用Drawable的绘制。实现LineHeightSpan是为了解决TextView高度的问题,设置最后一行的高度从而来保证整个TextView的高度大于或者等于Drawable的高度。

int need = ht - (v + fm.descent - fm.ascent - istartv);

上面v为这一行的起始垂直坐标,descent为正数,ascent为负数,istartv为整个Span的起始垂直坐标,上面表达式减去的就是整个TextView到这一行的高度,然后将这个高度和Drawable的高度进行对比,从而进行相应设置。

TabStopSpan

TabStopSpan用来将字符串中的”\t”替换成相应的空行,普通情况下”\t”不会进行显示,当使用TabStopSpan可以将”\t”替换成相应长度的空白区域。

TextView图文混排

/**
 * Returns the offset of the tab stop from the leading margin of the
 * line.
 * @return the offset
 */
public int getTabStop();

这个接口方法返回空白的长度。

CharacterStyle(字符级Span)

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。

UpdateAppearance

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

上面的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);

BackgroundColorSpanForegroundColorSpan
TextView图文混排

UnderlineSpanStrikethroughSpan
TextView图文混排

MaskFilterSpan
TextView图文混排

可以看一下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));

UpdateLayout

如果一个Span修改字符级文本度量|大小,则实现UpdateLayout。在Android源码中,只有MetricAffectingSpan实现了UpdateLayout接口。

TextView图文混排

TextView图文混排

接下来看一下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的实现类进行讲述。

SubscriptSpanSuperscriptSpan

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

SuperscriptSpan:
TextView图文混排

其实这两个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);
    }
AbsoluteSizeSpanRelativeSizeSpan

AbsoluteSizeSpan和RelativeSizeSpan用来改变相应字符的字体大小。

/**
* size: 大小
* dip: false,size单位为px,true,size单位为dip(默认为false)。
*/
//设置文字大小为24dp
span = new AbsoluteSizeSpan(24, true);

TextView图文混排

//设置文字大小为大2倍
span = new RelativeSizeSpan(2.0f);

TextView图文混排

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

ScaleXSpan影响字符集的文本格式。它可以在x轴方向上缩放字符集。

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

TextView图文混排

源码:

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

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

StyleSpan、TypefaceSpan和TextAppearanceSpan都可以字体的样式进行改变,
StyleSpan可以对字体设置bold或者italic的字符样式,
TypefaceSpan可以对字体设置其他的样式,
TextAppearanceSpan通过xml文件从而对字体进行设置。

StyleSpan
//设置bold+italic的字符样式
span = new StyleSpan(Typeface.BOLD | Typeface.ITALIC);

TextView图文混排

TypefaceSpan
//设置serif family
span = new TypefaceSpan("serif");

TextView图文混排

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

TextView图文混排

源码:

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

LocaleSpan用来对字体设置不同的地区,由于不同地区的字体会导致字体大小的变化,因此LocaleSpan也需要继承MetricAffectingSpan。

TextView图文混排

源码:

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

DynamicDrawableSpan

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

下面我们来分析一下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。

自定义Span

FrameSpan

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

  1. 计算字符序列的宽度;

  2. 根据计算的宽度、上下坐标、起始坐标绘制矩形;

  3. 绘制文字

展现效果如下所示:

TextView图文混排

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

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:画笔


VerticalImageSpan

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

TextView图文混排

图中的图片保持了和文字居中对齐,现在来看看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坐标可以通过计算得到,具体计算请看上面的源码。

AnimateForegroundColorSpan

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

TextView图文混排

源代码如下:

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。

RainbowSpan

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

TextView图文混排

源代码如下所示:

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

AnimatedRainbowSpan

TextView图文混排

如果要实现一个动画的彩虹样式,那么该如何实现呢?

其实结合上面的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);
    }
  }
}

FireworksSpan

TextView图文混排

“烟火”动画是让文字随机淡入。首先,把文字切断成多个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();

TypeWriterSpan

TextView图文混排

有了上面的例子,写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();
            }
        };

引用:
TextView图文混排基础
段落级span
字符级span
自定义span
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE与Spannable.SPAN_INCLUSIVE_EXCLUSIVE