【译】Spans,一个强大的概念
前言
最近,我写了一篇关于 NewStand app 和 app 上 ActionBar 的图标的翻转动效的文章。Cyril Mottier 建议我采用一个很优雅的方案,即使用 Spans 去淡入淡出 ActionBar 的标题。 此外,我一直想尝试所有可用的 Sapn 色的类型:ImageSpan、BackgroundColorSpan 等。他们非常简单易用但是(也)没有任何关于它们的文档和详细信息。 因此,在这篇文章中,我将探索在 Spans 的框架下什么是可以做的,然后,我将会告诉你怎么去进阶使用 Spans。 你可以下载和安装 demo 程序,查看源码。
框架
层次
主要规则:
- 如果一个 Span 影响字符级的文本格式,则继承 CharacterStyle。
- 如果一个 Span 影响段落层次的文本格式,则实现 ParagraphStyle
- 如果一个 Span 修改字符级别的文本外观,则实现 UpdateAppearance
- 如果一个 Span 修改字符级文本度量|大小,则实现 UpdateLayout
它为我们提供了下面这些类的关系图: 因为它有一点复杂所以我建议你使用像这样的可视化类图,以充分理解它的层次结构。
它是如何工作的?
布局(Layout)
当你给一个 TextView 设置文本时,它使用基类布局去管理文本的渲染。
布局类包含一个布尔mSpannedText
:真,当文本是一个 Spanned 的实例时(SpannableString实现 Spanned)。这个类只处理 ParagraphStyle Spans。
[draw](http://developer.android.com/reference/android/text/Layout.html#draw(android.graphics.Canvas,%20android.graphics.Path,%20android.graphics.Paint,%20int) 方法调用了其它两个方法:
- drawBackground 对于文本的每一行,如果有一个 LineBackgroundSpan 用于当前行,LineBackgroundSpan#drawBackground方法将被调用。
- drawText 对于文本的每一行,它计算 LeadingMarginSpan 和 LeadingMarginSpan2,并调用 LeadingMarginSpan#drawLeadingMargin 方法当它是必要的时候。这也用于确定文本对齐。最后,如果当前行是跨行的,布局将调用 TextLine#draw 方法(每一行都会创建一个 TextLine 对象)。
文本行(TextLine)
android.text.TextLine 的文档这么说:代表一行样式的文字,用于测量视觉顺序和为了渲染。 TextLine 类包含 3 个 Spans 的集合:
- MetricAffectingSpan set
- CharacterStyle set
- ReplacementSpan set
其中有趣的方法:TextLine#handleRun,这也是所有的 Spans 用来渲染文本的。相对于 Span 的类型,TextLine 调用:
- CharacterStyle#updateDrawState 方法更改 MetricAffectingSpan 和 CharacterStyle 两个 Spans 的 TextPaint 配置。
- TextLine#handleReplacement 方法处理 ReplacementSpan。它调用 Replacement#getSize 得到 replacement 的宽度,如果它需要更新字体规格最终会调用 [Replacement#draw](http://developer.android.com/reference/android/text/style/ReplacementSpan.html#draw(android.graphics.Canvas,%20java.lang.CharSequence,%20int,%20int,%20float,%20int,%20int,%20int,%20android.graphics.Paint)
字体规格(Font Metrics)
如果你想知道更多什么是字体规格,那么看下面的图解:
耍起来
BulletSpan
android.text.style.BulletSpan BulletSpan 影响段落层次的文本格式。它可以给段落的开始处加上项目符号。
/**
* gapWidth: 项目符号和文本之间的间隙
* color: 项目符号的颜色,默认为透明
*/
//创建一个黑色的 BulletSpan,间隙为 15px
span = new BulletSpan(15, Color.BLACK);
QuoteSpan
android.text.style.QuoteSpan QuoteSpan 影响段落层次的文本格式。它可以给一个段落加上垂直的引用线。
/**
* color: 垂直的引用线颜色,默认是蓝色
*/
//创建一个红色的引用
span = new QuoteSpan(Color.RED);
AlignmentSpan.Standard
android.text.style.AlignmentSpan.Standard AlignmentSpan.Standard 影响段落层次的文本格式。它可以把段落的每一行文本按正常、居中、相反的方式对齐。
/**
* align: 对齐方式
*/
//居中对齐的段落
span = new AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER);
UnderlineSpan
android.text.style.UnderlineSpan UnderlineSpan 影响字符级的文本格式。它可以为字符集加上下划线,归功于 Paint#setUnderlineText(true)。
//下划线
span = new UnderlineSpan();
StrikethroughSpan
android.text.style.StrikethroughSpan StrikethroughSpan 影响字符级的文本格式。它可以给字符集加上删除线,归功于 [Paint#setStrikeThruText(true))](http://developer.android.com/reference/android/graphics/Paint.html#setStrikeThruText(boolean)。
//删除线
span = new StrikethroughSpan();
SubscriptSpan
android.text.style.SubscriptSpan SubscriptSpan 影响字符级的文本格式,它可以通过减小 TextPaint#baselineShift 给字符集加下标。
//下标
span = new SubscriptSpan();
SuperscriptSpan
android.text.style.SuperscriptSpan SuperscriptSpan 影响字符级的文本格式。它可以通过增加 TextPaint#baselineShift 给字符集加上标。
//上标
span = new SuperscriptSpan();
BackgroundColorSpan
android.text.style.BackgroundColorSpan BackgroundColorSpan 影响字符级的文本格式。它可以给字符集加上背景颜色。
/**
* color: 背景颜色
*/
//设置字符背景颜色
span = new BackgroundColorSpan(Color.GREEN);
ForegroundColorSpan
android.text.style.ForegroundColorSpan ForegroundColorSpan 影响字符级的文本格式,它可以设置字符集的前景颜色也即文字颜色。
/**
* color: 前景颜色
*/
//设置红色的前景
span = new ForegroundColorSpan(Color.RED);
ImageSpan
android.text.style.ImageSpan ImageSpan 影响字符级的文本格式。它可以生成图像字符。这是为数不多的文档齐全的 Span 所以 enjoy it!
/**
* Context: 上下文
* resourceId: 图像资源id
*/
//用一个小图像代替字符
span = new ImageSpan(this, R.drawable.pic1_small);
StyleSpan
android.text.style.StyleSpan StyleSpan 影响字符级的文本格式,它可以给字符集设置样式(blod、italic、normal)。
//设置bold+italic的字符样式
span = new StyleSpan(Typeface.BOLD | Typeface.ITALIC);
TypefaceSpan
android.text.style.TypefaceSpan TypefaceSpan 影响字符级的文本格式。它可以给字符设置字体集(monospace、serif等)。
//设置serif family
span = new TypefaceSpan("serif");
TextAppearanceSpan
android.text.style.TextAppearanceSpan TextAppearanceSpan 影响字符级的文本格式。它可以给字符集设置外观(appearance)。
/**
* TextAppearanceSpan(Context context, int appearance, int colorList)
* context: 上下文
* appearance:appearance资源id(例如:android.R.style.TextAppearance_Small)
* colorList:文本的颜色资源id(例如:android.R.styleable.Theme_textColorPrimary)
*
* TextAppearanceSpan(String family, int style, int size, ColorStateList color, ColorStateList linkColor)
* family:字体family
* style:描述样式(例如:android.graphics.Typeface)
* size:文字大小
* color:文字颜色
* linkColor:连接文本的颜色
*/
//设置serif family
span = new TextAppearanceSpan(this/*a context*/, 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>
AbsoluteSizeSpan
android.text.style.AbsoluteSizeSpan AbsoluteSizeSpan 影响字符级的文本格式。它可以设置一个字符集的绝对文字大小。
/**
* size: 大小
* dip: false,size单位为px,true,size单位为dip(默认为false)。
*/
//设置文字大小为24dp
span = new AbsoluteSizeSpan(24, true);
RelativeSizeSpan
android.text.style.RelativeSizeSpan RelativeSizeSpan 影响字符水平的文本格式。它可以设置字符集的文本大小。
//设置文字大小为大2倍
span = new RelativeSizeSpan(2.0f);
ScaleXSpan
android.text.style.ScaleXSpan ScaleXSpan 影响字符集的文本格式。它可以在x轴方向上缩放字符集。
//设置水平方向上放大3倍
span = new ScaleXSpan(3.0f);
MaskFilterSpan
android.text.style.MaskFilterSpan MaskFilterSpan 影响字符集文本格式。它可以给字符集设置 android.graphics.MaskFilter。 警告:BlurMaskFilter不支持硬件加速
//模糊字符集
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));
Spans进阶
前景色(文字颜色)动画
ForegroundColorSpan 为只读。这意味实例化之后着你不能改变你不能改变前景色。所以,要做的第一件事就是编写一个 MutableForegroundColorSpan。 MutableForegroundColorSpan.java
public class MutableForegroundColorSpan extends ForegroundColorSpan
{
private int mAlpha = 255;
private int mForegroundColor;
public MutableForegroundColorSpan(int alpha, int color)
{
super(color);
mAlpha = alpha;
mForegroundColor = color;
}
public MutableForegroundColorSpan(Parcel src)
{
super(src);
mForegroundColor = src.readInt();
mAlpha = src.readInt();
}
public void writeToParcel(Parcel dest, int flags)
{
super.writeToParcel(dest, flags);
dest.writeInt(mForegroundColor);
dest.writeFloat(mAlpha);
}
@Override
public void updateDrawState(TextPaint ds)
{
ds.setColor(getForegroundColor());
}
/**
* @param alpha from 0 to 255
*/
public void setAlpha(int alpha)
{
mAlpha = alpha;
}
public void setForegroundColor(int foregroundColor)
{
mForegroundColor = foregroundColor;
}
public float getAlpha()
{
return mAlpha;
}
@Override
public int getForegroundColor()
{
return Color.argb(mAlpha, Color.red(mForegroundColor), Color.green(mForegroundColor), Color.blue(mForegroundColor));
}
}
现在,我们可以在同一个实例改变透明度和前景色了。但是,当你设置这些属性,它并不会刷新视图,你必须通过重新设置 SpannableString 才能刷新视图。
MutableForegroundColorSpan span = new MutableForegroundColorSpan(255, Color.BLACK);
spannableString.setSpan(span, 0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
textView.setText(spannableString);
//黑色完全不透明(译者注:上面代码的效果)
span.setAlpha(100);
span.setForegroundColor(Color.RED);
//到这一步文字没有变化
textView.setText(spannableString);
//最后,文字变为红色和透明
现在我们要前景色的动画。我们可以自定义 android.util.Property。
private static final Property<MutableForegroundColorSpan, Integer> MUTABLE_FOREGROUND_COLOR_SPAN_FC_PROPERTY =
new Property<MutableForegroundColorSpan, Integer>(Integer.class, "MUTABLE_FOREGROUND_COLOR_SPAN_FC_PROPERTY") {
@Override
public void set(MutableForegroundColorSpan span, Integer value) {
span.setForegroundColor(value);
}
@Override
public Integer get(MutableForegroundColorSpan span) {
return span.getForegroundColor();
}
};
最后,我们使用属性动画(ObjectAnimator)让自定义属性动起来。不要忘记更新视图。
MutableForegroundColorSpan span = new MutableForegroundColorSpan(255, Color.BLACK);
mSpannableString.setSpan(span, 0, text.length(), 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(mSpannableString);
}
});
objectAnimator.start();
ActionBar"烟火"
“烟火"动画是让文字随机淡入。首先,把文字切断成多个 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.setProgress(value);
}
@Override
public Float get(FireworksSpanGroup spanGroup) {
return spanGroup.getProgress();
}
};
最后,我们创建 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();
使用自定义的span
在本节中,我们将看到使用自定义 span 来绘制的方式。这是文本定制很好的方式。 首先,我们要创建一个继承ReplacementSpan抽象类的自定义 Span。 如果你想画一个自定义的背景,你可以实现LineBackgroundSpan ,这是影响段落级的文本格式。 我们必须实现 2 个方法:
- getSize:这个方法返回新的你更换后的 size。 text:Span 管理的文本 start:文本开始处 end:文本结尾处 fm:字体规格,可以为空
- draw:可以使用 Canvas 绘制。 x:绘制文本的x坐标 top:线(line)的顶部(译者注:line 的定义参看前面字体规格这一节) y:基线 bottom:线的底部、
让我们看一个例子,画一个包围文本的蓝色矩形。 FrameSpan.java
@Override
public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm)
{
//将返回相对于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)
{
//使用自定义的画笔绘制在画布上
canvas.drawRect(x, top, x + mWidth, bottom, mPaint);
}
附加
Sample app 包含了一些 Spans 进阶的例子,如下:
总结
在编写这篇文章的过程中,我意识到 Spans 是真的如同 Drawable 那般强大的,我认为它们还没有被充分运用。文本是一个应用程序的主要内容,它无处不在,所以不要忘记,通过 Spans 让它变得更具活力和吸引力!