TextView图文混排

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文件就可以实现了。

"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图文混排_第1张图片

图片动起来

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


<animated-rotate
  android:pivotX="50%"
  android:pivotY="50%"
  android:drawable="@drawable/ic_loading"
  android:duration="500" />

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

<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图文混排_第2张图片

Html Content

关键词:![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>
1/font>人签到,日榜单排在第1名]]>
Data>string>

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

不同颜色文字内容

图片和链接

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

Html代码:

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

android字符串:

<string name="from_html_text">
Hello World
Here is an
[站外图片上传中……(14)]octopus.
And here is a
link. ]]> 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图文混排_第3张图片

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

textView.setMovementMethod(LinkMovementMethod.getInstance());

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

Span方式

整体机理

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

setText(CharSequence text)

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

CharSequence
TextView图文混排_第4张图片

CharSequence方法
TextView图文混排_第5张图片

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

spannable

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

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

0

当调用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);

设置的是前置包括,所以插入前面样式有效
1

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

spannableString.insert(2,"a");

效果如下:
2

spanned

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

TextView图文混排_第7张图片

  • 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图文混排_第8张图片

由图中可以看出,他们都实现了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图文混排_第9张图片

ParagraphStyle:

TextView图文混排_第10张图片

UpdateAppearance:

TextView图文混排_第11张图片

UpdateLayout:
TextView图文混排_第12张图片

实例

TextView图文混排_第13张图片

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图文混排_第14张图片

  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图文混排_第15张图片

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

具体来看代码,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图文混排_第16张图片

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图文混排_第17张图片

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图文混排_第18张图片

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

Layout.Alignment getAlignment();

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

LineBackgroundSpan

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

LineBackgroundSpan
TextView图文混排_第20张图片

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

具体代码如下:

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图文混排_第21张图片

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

LineHeightDemo

上面图片的一行文字打印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图文混排_第22张图片

这个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图文混排_第23张图片

/**
 * 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图文混排_第24张图片

上面的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图文混排_第25张图片

UnderlineSpanStrikethroughSpan
TextView图文混排_第26张图片

MaskFilterSpan
TextView图文混排_第27张图片

可以看一下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图文混排_第28张图片

TextView图文混排_第29张图片

接下来看一下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图文混排_第30张图片

SuperscriptSpan:
TextView图文混排_第31张图片

其实这两个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图文混排_第32张图片

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

TextView图文混排_第33张图片

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图文混排_第34张图片

源码:

    @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图文混排_第35张图片

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

TextView图文混排_第36张图片

TextAppearanceSpan
span = new TextAppearanceSpan(this, R.style.SpecialTextAppearance);
<-- style.xml -->

TextView图文混排_第37张图片

源码:

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。

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);
    }
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图文混排_第38张图片

下面我们来分析一下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 wr = mDrawableRef;
        Drawable d = null;
        if (wr != null)
            d = wr.get();
        if (d == null) {
            d = getDrawable();
            mDrawableRef = new WeakReference(d);
        }
        return d;
    }

    private WeakReference 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图文混排_第39张图片

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

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图文混排_第40张图片

图中的图片保持了和文字居中对齐,现在来看看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图文混排_第41张图片

源代码如下:

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 MUTABLE_FOREGROUND_COLOR_SPAN_FC_PROPERTY =
        new Property(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图文混排_第42张图片

源代码如下所示:

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

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 ANIMATED_COLOR_SPAN_FLOAT_PROPERTY
      = new Property(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

FireworksSpan

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

private static final class FireworksSpanGroup {
    private final float mAlpha;
    private final ArrayList mSpans;
    private FireworksSpanGroup(float alpha) {
        mAlpha = alpha;
        mSpans = new ArrayList();
    }
    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 FIREWORKS_GROUP_PROGRESS_PROPERTY =
new Property(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图文混排_第43张图片

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

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

    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 TYPE_WRITER_GROUP_ALPHA_PROPERTY =
        new Property(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

你可能感兴趣的:(笔记)