在使用TextView的时候,我们经常需要在TextView中进行图文混排,比如在QQ中聊天的消息中就会展现表情,比如在微博中,用户发出的微博里面经常会带有各种小图标和链接。
Android官方对TextView的图文混排提供了支持,我们可以从以下三种方式实现TextView的图文混排:
在TextView的XML布局文件中添加Compound Drawable属性;
在对TextView设置字符串时,可以设置Html类型的字符串。Html.fromHtml()方法可以对Html的字符串进行处理,从而使得Html类型的内容满足TextView的要求。在给TextView设置Html类型的内容时,还可以传入一个ImageGetter,从而对Html类型内容中的图片进行处理;
对TextView设置内容的时候,可以传入CharSequence类型,而一些CharSequence类型可以利用CharacterStyle进行修饰,从而展现出丰富多彩的内容。CharacterStyle拥有很多子类(BackgroundColorSpan,ClickableSpan,ImageSpan,TypefaceSpan等),可以产生出各种各样的效果。
对于以上三种形式有着不同的使用场景:
一般情况下我们希望在字符串的上、下、左、右方向添加图片,这种需求简单明确,使用第1种方式(Compound Drawable)就可以了。
有时候我们希望TextView中含有不同颜色的字体,这时候可以使用第二种方式(Html.fromHtml()),只需要在不同颜色的字体上设置相应的颜色即可。第二种方式也可以处理TextView中的链接情况,第2中方式还可以在TextView中显示图片。
第3种方式可以对TextView中的显示内容进行各种变换,可以对字体背景进行设置,可以对字体颜色进行设置,可以在内容中加入图片,可以进行的操作非常多,但是同时相应的处理也较为复杂。
下面将会对以上的三种方式分别进行讲述,希望能够让大家更好地掌握TextView的使用。
关键词: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"/>
将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>
关键词:、![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>
<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里面的链接生效,需要对TextView进行设置。setMovementMethod
textView.setMovementMethod(LinkMovementMethod.getInstance());
但是上面的代码会造成当TextView设置最大行数失败,当超过最大行数的时候会造成TextView里面的内容可以滑动,在下面的内容里面会讲解如何解决这个问题。
TextView可以通过下面的方法设置内容,一般情况下我们会给TextView设置String类型的内容,String类型是实现了CharSequence接口的。
setText(CharSequence text)
在Google的android官方网站上我们可以得到CharSequence接口的相关内容。
在这里我们需要了解spanned和spannable,其实这两个都是接口,而且spannable是继承spanned。为了方便理解,这里先讲解spannable接口。
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通常使用的是以下4种:
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE(前后都不包括);
Spanned.SPAN_INCLUSIVE_EXCLUSIVE(前面包括,后面不包括);
Spanned.SPAN_EXCLUSIVE_INCLUSIVE(前面不包括,后面包括);
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);
当调用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);
插入位置改变。因为设置的是后置不包括,即使插入文本位置紧挨span样式,样式也无效。
spannableString.insert(2,"a");
在spanned里面提供了下面5个抽象方法:
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两个类,这两个类实现了Spannable接口,实现了接口里面定义的方法。SpannableString和SpannableStringBuilder的关系类似于String和StringBuilder的关系。SpannableStringBuilder和StringBuilder一样实现了Appendable接口,从而可以往里面不断append内容。在使用Span实现TextView图文混排的过程中,一般来说我们都会使用SpannableString和SpannableStringBuilder中的一个。
所以对于使用Span方式实现TextView图文混排的整体流程是:
创建一个SpannableString或者SpannableStringBuilder对象;
利用setSpan(Object what, int start, int end, int flags)方法,将SpannableString或者SpannableStringBuilder对象的某些位置的内容替换为具体类型的Span;
利用TextView的setText(CharSequence text)方法将SpannableString或者SpannableStringBuilder对象进行展示。
Google官方的介绍:
This is the class for text whose content and markup can both be changed.
(这是一个内容和标记都可以更改的文本类)
SpannableStringBuilder有个亲兄弟——SpannableString。SpannableStringBuilder和SpannableString的区别类似与StringBuilder、String,就是SpannableStringBuilder可以拼接,而SpannableString不可拼接。
由图中可以看出,他们都实现了CharSequence,因此,他们可以直接在TextView的setText中使用
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:
ParagraphStyle:
UpdateAppearance:
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是一个接口,通过查看Android源码,我们发现这个接口里面什么方法也没有定义,因此,我们可以认为,这个接口无非是标识实现这个接口的Span为段落级别的Span。
在Android源码中又继续定义了几个接口实现了ParagraphStyle接口。
LeadingMarginSpan:用来处理像word中项目符号一样的接口;
AlignmentSpan:用来处理整个段落对其的接口;
LineBackgroundSpan:用来处理一行的背景的接口;
LineHeightSpan:用来处理一行高度的接口;
TabStopSpan:用来将字符串中的”\t”替换成相应的空行;
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代码如下所示:
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代码如下所示:
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);
}
上面的代码就十分清晰了,每行都偏移相应距离,然后每行都绘制矩形,就连成了一条竖线。
如果希望做到两端文字环绕图片的效果,其实可以考虑编写Span实现LeadingMarginSpan2。具体做法其实比较简单,相对布局中放置ImageView和TextView,然后根据ImageView的大小计算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接口中定义了一个接口方法,里面还有个Standard实现。
Layout.Alignment getAlignment();
AlignmentSpan比较简单,不多做讲述。
LineBackgroundSpan用来设置每一行的背景颜色,这个和对字体设置颜色不同,具体区别如下:
TextBackgroundSpan
可以看见下面图片中背景颜色是整行的。
具体代码如下:
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);
}
}
要想熟练使用这个Span,需要对字体的高度设置有着较好的理解。
Top和Ascent之间存在的距离是考虑到了类似读音符号。Android依然会在绘制文本的时候在文本外层留出一定的边距,这就是为什么top和bottom总会比ascent和descent大一点的原因。而在TextView中我们可以通过xml设置其属性android:includeFontPadding=”false”去掉一定的边距值但是不能完全去掉。
上面图片的一行文字打印FontMetrics相应的值,如下所示:
ascent:-46.38672
top:-52.807617
leading:0.0
descent:12.207031
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;
}
这个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用来将字符串中的”\t”替换成相应的空行,普通情况下”\t”不会进行显示,当使用TabStopSpan可以将”\t”替换成相应长度的空白区域。
/**
* Returns the offset of the tab stop from the leading margin of the
* line.
* @return the offset
*/
public int getTabStop();
这个接口方法返回空白的长度。
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。
如果一个Span修改字符级别的文本外观,则实现UpdateAppearance。
上面的Span都实现了UpdateAppearance接口,上面的诸多Span都是通过updateDrawState(TextPaint ds)方法来实现相应的效果。
BackgroundColorSpan:ds.bgColor = mColor;
ForegroundColorSpan:ds.setColor(mColor);
StrikethroughSpan:ds.setStrikeThruText(true);
UnderlineSpan:ds.setUnderlineText(true);
MaskFilterSpan:ds.setMaskFilter(mFilter);
BackgroundColorSpan和ForegroundColorSpan:
UnderlineSpan和StrikethroughSpan:
可以看一下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));
如果一个Span修改字符级文本度量|大小,则实现UpdateLayout。在Android源码中,只有MetricAffectingSpan实现了UpdateLayout接口。
接下来看一下MetricAffectingSpan的源码。
public abstract class MetricAffectingSpan
extends CharacterStyle
implements UpdateLayout {
public abstract void updateMeasureState(TextPaint p);
/**
* Returns "this" for most MetricAffectingSpans, but for
* MetricAffectingSpans that were generated by {@link #wrap},
* returns the underlying MetricAffectingSpan.
*/
@Override
public MetricAffectingSpan getUnderlying() {
return this;
}
/**
* A Passthrough MetricAffectingSpan is one that
* passes {@link #updateDrawState} and {@link #updateMeasureState}
* calls through to the specified MetricAffectingSpan
* while still being a distinct object,
* and is therefore able to be attached to the same Spannable
* to which the specified MetricAffectingSpan is already attached.
*/
/* package */ static class Passthrough extends MetricAffectingSpan {
private MetricAffectingSpan mStyle;
/**
* Creates a new Passthrough of the specfied MetricAffectingSpan.
*/
public Passthrough(MetricAffectingSpan cs) {
mStyle = cs;
}
/**
* Passes updateDrawState through to the underlying MetricAffectingSpan.
*/
@Override
public void updateDrawState(TextPaint tp) {
mStyle.updateDrawState(tp);
}
/**
* Passes updateMeasureState through to the underlying MetricAffectingSpan.
*/
@Override
public void updateMeasureState(TextPaint tp) {
mStyle.updateMeasureState(tp);
}
/**
* Returns the MetricAffectingSpan underlying this one, or the one
* underlying it if it too is a Passthrough.
*/
@Override
public MetricAffectingSpan getUnderlying() {
return mStyle.getUnderlying();
}
}
}
可以看见MetricAffectingSpan同样继承了CharacterStyle,因此同样继承了抽象方法updateDrawState(TextPaint tp),这个方法可以交给子类实现,从而实现字体外观的改变。在MetricAffectingSpan类中定义了一个抽象方法updateMeasureState(TextPaint p),继承MetricAffectingSpan类的子类可以实现这个抽象方法,从而实现对字体大小的改变。在MetricAffectingSpan中同样也提供了一个Passthrough的类,从而完成CharacterStyle中定义的wrap方法。
接下来分别对MetricAffectingSpan的实现类进行讲述。
SubscriptSpan和SuperscriptSpan实现字体的上下标展示,效果如下面的图片所示:
SubscriptSpan:
其实这两个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);
}
AbsoluteSizeSpan和RelativeSizeSpan用来改变相应字符的字体大小。
/**
* size: 大小
* dip: false,size单位为px,true,size单位为dip(默认为false)。
*/
//设置文字大小为24dp
span = new AbsoluteSizeSpan(24, true);
//设置文字大小为大2倍
span = new RelativeSizeSpan(2.0f);
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影响字符集的文本格式。它可以在x轴方向上缩放字符集。
//设置水平方向上放大3倍
span = new ScaleXSpan(3.0f);
源码:
@Override
public void updateDrawState(TextPaint ds) {
ds.setTextScaleX(ds.getTextScaleX() * mProportion);
}
@Override
public void updateMeasureState(TextPaint ds) {
ds.setTextScaleX(ds.getTextScaleX() * mProportion);
}
StyleSpan、TypefaceSpan和TextAppearanceSpan都可以字体的样式进行改变,
StyleSpan可以对字体设置bold或者italic的字符样式,
TypefaceSpan可以对字体设置其他的样式,
TextAppearanceSpan通过xml文件从而对字体进行设置。
//设置bold+italic的字符样式
span = new StyleSpan(Typeface.BOLD | Typeface.ITALIC);
//设置serif family
span = new TypefaceSpan("serif");
span = new TextAppearanceSpan(this, R.style.SpecialTextAppearance);
<-- style.xml -->
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也需要继承MetricAffectingSpan。
源码:
@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继承了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可以做到使用Drawable替代相对应的字符序列,展现效果如下所示:
ImageSpan:
下面我们来分析一下DynamicDrawableSpan的源码。
public abstract class DynamicDrawableSpan extends ReplacementSpan {
private static final String TAG = "DynamicDrawableSpan";
/**
* A constant indicating that the bottom of this span should be aligned
* with the bottom of the surrounding text, i.e., at the same level as the
* lowest descender in the text.
*/
public static final int ALIGN_BOTTOM = 0;
/**
* A constant indicating that the bottom of this span should be aligned
* with the baseline of the surrounding text.
*/
public static final int ALIGN_BASELINE = 1;
protected final int mVerticalAlignment;
public DynamicDrawableSpan() {
mVerticalAlignment = ALIGN_BOTTOM;
}
/**
* @param verticalAlignment one of {@link #ALIGN_BOTTOM} or {@link #ALIGN_BASELINE}.
*/
protected DynamicDrawableSpan(int verticalAlignment) {
mVerticalAlignment = verticalAlignment;
}
/**
* Returns the vertical alignment of this span, one of {@link #ALIGN_BOTTOM} or
* {@link #ALIGN_BASELINE}.
*/
public int getVerticalAlignment() {
return mVerticalAlignment;
}
/**
* Your subclass must implement this method to provide the bitmap
* to be drawn. The dimensions of the bitmap must be the same
* from each call to the next.
*/
public abstract Drawable getDrawable();
@Override
public int getSize(Paint paint, CharSequence text,
int start, int end,
Paint.FontMetricsInt fm) {
Drawable d = getCachedDrawable();
Rect rect = d.getBounds();
if (fm != null) {
fm.ascent = -rect.bottom;
fm.descent = 0;
fm.top = fm.ascent;
fm.bottom = 0;
}
return rect.right;
}
@Override
public void draw(Canvas canvas, CharSequence text,
int start, int end, float x,
int top, int y, int bottom, Paint paint) {
Drawable b = getCachedDrawable();
canvas.save();
int transY = bottom - b.getBounds().bottom;
if (mVerticalAlignment == ALIGN_BASELINE) {
transY -= paint.getFontMetricsInt().descent;
}
canvas.translate(x, transY);
b.draw(canvas);
canvas.restore();
}
private Drawable getCachedDrawable() {
WeakReference 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;
}
抽象方法getDrawable()告诉子类需要提供一个Drawable用来绘制;
getSize方法中,通过设置FontMetricsInt,从而使得替代字符序列的baseline和图片的尾部对齐,而替代字符序列的垂直高度就为图片的高度;
draw方法中,需要绘制图片的其实x坐标很明确就是x,y坐标可以通过多种方式获取,在baseline对齐的情况下可以等于top,也可以等于y-b.getBounds().bottom,还可以等于bottom-b.getBounds().bottom-descent,各种方法都可以。
在Android系统中,提供了一个ImageSpan继承了DynamicDrawableSpan,实现了通过多种方式生成Drawable。
FrameSpan实现给相应的字符序列添加边框的效果,整体思路其实比较简单。
计算字符序列的宽度;
根据计算的宽度、上下坐标、起始坐标绘制矩形;
绘制文字
展现效果如下所示:
再来看一下代码,其实代码十分简单。
public class FrameSpan extends ReplacementSpan {
private final Paint mPaint;
private int mWidth;
public FrameSpan() {
mPaint = new Paint();
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(Color.BLUE);
mPaint.setAntiAlias(true);
}
@Override
public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
//return text with relative to the Paint
mWidth = (int) paint.measureText(text, start, end);
return mWidth;
}
@Override
public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
//draw the frame with custom Paint
canvas.drawRect(x, top, x + mWidth, bottom, mPaint);
canvas.drawText(text, start, end, x, y, paint);
}
}
在这再次说明一下draw方法里面的参数的意义。
canvas:用来绘制的画布;
text:整个text;
start:这个Span起始字符在text中的位置;
end:这个Span结束字符在text中的位置;
x:这个Span的其实水平坐标;
y:这个Span的baseline的垂直坐标;
top:这个Span的起始垂直坐标;
bottom:这个Span的结束垂直坐标;
paint:画笔
Google提供的ImageSpan和DynamicDrawableSpan只能实现图片和文字底部对齐或者是baseline对齐,现在VerticalImageSpan可以实现图片和文字居中对齐。
图中的图片保持了和文字居中对齐,现在来看看VerticalImageSpan的源码。
public class VerticalImageSpan extends ImageSpan {
private Drawable drawable;
public VerticalImageSpan(Drawable drawable) {
super(drawable);
this.drawable=drawable;
}
@Override
public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fontMetricsInt) {
Drawable drawable = getDrawable();
if(drawable==null){
drawable= this.drawable;
}
Rect rect = drawable.getBounds();
if (fontMetricsInt != null) {
Paint.FontMetricsInt fmPaint = paint.getFontMetricsInt();
int fontHeight = fmPaint.bottom - fmPaint.top;
int drHeight = rect.bottom - rect.top;
int top = drHeight / 2 - fontHeight / 4;
int bottom = drHeight / 2 + fontHeight / 4;
fontMetricsInt.ascent = -bottom;
fontMetricsInt.top = -bottom;
fontMetricsInt.bottom = top;
fontMetricsInt.descent = top;
}
return rect.right;
}
@Override
public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
Drawable drawable = getDrawable();
canvas.save();
int transY = ((bottom - top) - drawable.getBounds().bottom) / 2 + top;
canvas.translate(x, transY);
drawable.draw(canvas);
canvas.restore();
}
}
在geSize方法中通过fontMetricsInt设置从而实现图片和文字居中对齐,其实计算的根本为计算baseline的位置,因为TextView是按照baseline对齐的。
分析getSize方法可以知道这个图片的baseline为图片中央往下fontHeight / 2,这样也就实现了图片和文字的居中对齐。
draw方法用来绘制图片,绘制x坐标为span的其实坐标,绘制y坐标可以通过计算得到,具体计算请看上面的源码。
先讲述一个简单的动画Span的例子,这个动画是用来改变文字颜色的。
源代码如下:
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。
彩虹样的Span,其实实现起来也是很简单的,主要是用到了Paint的Shader技术,效果如下所示:
源代码如下所示:
private static class RainbowSpan extends CharacterStyle implements UpdateAppearance {
private final int[] colors;
public RainbowSpan(Context context) {
colors = context.getResources().getIntArray(R.array.rainbow);
}
@Override
public void updateDrawState(TextPaint paint) {
paint.setStyle(Paint.Style.FILL);
Shader shader = new LinearGradient(0, 0, 0, paint.getTextSize() * colors.length, colors, null,
Shader.TileMode.MIRROR);
Matrix matrix = new Matrix();
matrix.setRotate(90);
shader.setLocalMatrix(matrix);
paint.setShader(shader);
}
}
由于paint使用shader是从上到下进行绘制,因此这里需要用到矩阵,然后将矩阵旋转90度。
如果要实现一个动画的彩虹样式,那么该如何实现呢?
其实结合上面的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);
}
}
}
“烟火”动画是让文字随机淡入。首先,把文字切断成多个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就变得十分简单了。
先创建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