文章目录
- 1. 前言
- 2. 框架
- 2.0.1. 层次
3. 它是如何工作的?
- 3.0.1. 布局(Layout)
- 3.0.2. 文本行(TextLine)
- 3.0.3. 字体规格(Font Metrics)
4. 耍起来
- 4.0.1. BulletSpan
- 4.0.2. QuoteSpan
- 4.0.3. AlignmentSpan.Standard
- 4.0.4. UnderlineSpan
- 4.0.5. StrikethroughSpan
- 4.0.6. SubscriptSpan
- 4.0.7. SuperscriptSpan
- 4.0.8. BackgroundColorSpan
- 4.0.9. ForegroundColorSpan
- 4.0.10. ImageSpan
- 4.0.11. StyleSpan
- 4.0.12. TypefaceSpan
- 4.0.13. TextAppearanceSpan
- 4.0.14. AbsoluteSizeSpan
- 4.0.15. RelativeSizeSpan
- 4.0.16. ScaleXSpan
- 4.0.17. MaskFilterSpan
5. Spans进阶
- 5.0.1. 前景色(文字颜色)动画
- 5.0.2. ActionBar”烟火”
- 5.0.3. 使用自定义的span
- 5.0.4. 附加
6. 总结
前言
原文:Spans, a Powerful Concept.
最近,我写了一篇关于NewStand app和app上ActionBar的图标的翻转动效的文章。Cyril Mottier建议我采用一个很优雅的方案,即使用Spans去淡入淡出ActionBar的标题。
此外,我一直想尝试所有可用的Sapn色的类型:ImageSpan、BackgroundColorSpan等。他们非常简单易用但是(也)没有任何关于它们的文档和详细信息。
因此,在这篇文章中,我将探索在Spans的框架下什么是可以做的,然后,我将会告诉你怎么去进阶使用Spans。
你可以下载和安装demo程序,查看源码。
框架
层次
主要规则:
- 如果一个Span影响字符级的文本格式,则继承CharacterStyle。
- 如果一个Span影响段落层次的文本格式,则实现ParagraphStyle
- 如果一个Span修改字符级别的文本外观,则实现UpdateAppearance
- 如果一个Span修改字符级文本度量|大小,则实现UpdateLayout
它为我们提供了下面这些美丽的类图:
characterstyle
paragraphstyle
updateappearance
updatelayout
因为它有一点复杂所以我建议你使用像这样的可视化类图,以充分理解它的层次结构。
它是如何工作的?
布局(Layout)
当你给一个TextView设置文本时,它使用基类布局去管理文本的渲染。
布局类包含一个布尔mSpannedText
:真,当文本是一个Spanned的实例时(SpannableString实现Spanned)。这个类只处理ParagraphStyleSpans。
draw方法调用了其它两个方法:
- 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
字体规格(Font Metrics)
如果你想知道更多什么是字体规格,那么看下面的图解:
fontmetrics
耍起来
BulletSpan
android.text.style.BulletSpan
BulletSpan影响段落层次的文本格式。它可以给段落的开始处加上项目符号。
1
2
3
4
5
6
7
|
/**
* gapWidth:项目符号和文本之间的间隙
* color: 项目符号的颜色,默认为透明
*/
span =
new BulletSpan(
15, Color.BLACK);
|
BulletSpan的效果
QuoteSpan
android.text.style.QuoteSpan
QuoteSpan影响段落层次的文本格式。它可以给一个段落加上垂直的引用线。
1
2
3
4
5
6
|
/**
* color: 垂直的引用线颜色,默认是蓝色
*/
span =
new QuoteSpan(Color.RED);
|
QuoteSpan的效果
AlignmentSpan.Standard
android.text.style.AlignmentSpan.Standard
AlignmentSpan.Standard影响段落层次的文本格式。它可以把段落的每一行文本按正常、居中、相反的方式对齐。
1
2
3
4
5
6
|
/**
* align: 对齐方式
*/
span =
new AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER);
|
AlignmentSpan.Standard的效果
UnderlineSpan
android.text.style.UnderlineSpan
UnderlineSpan影响字符级的文本格式。它可以为字符集加上下划线,归功于Paint#setUnderlineText(true))。
1
2
|
span =
new UnderlineSpan();
|
UnderlineSpan效果图
StrikethroughSpan
android.text.style.StrikethroughSpan
StrikethroughSpan影响字符级的文本格式。它可以给字符集加上删除线,归功于Paint#setStrikeThruText(true))。
1
2
|
span =
new StrikethroughSpan();
|
StrikethroughSpan的效果图
SubscriptSpan
android.text.style.SubscriptSpan
SubscriptSpan影响字符级的文本格式,它可以通过减小TextPaint#baselineShift给字符集加下标。
1
2
|
span =
new SubscriptSpan();
|
SubscriptSpan的效果图
SuperscriptSpan
android.text.style.SuperscriptSpan
SuperscriptSpan影响字符级的文本格式。它可以通过增加TextPaint#baselineShift 给字符集加上标。
1
2
|
span =
new SuperscriptSpan();
|
SuperscriptSpan效果图
BackgroundColorSpan
android.text.style.BackgroundColorSpan
BackgroundColorSpan影响字符级的文本格式。它可以给字符集加上背景颜色。
1
2
3
4
5
6
|
/**
* color: 背景颜色
*/
span =
new BackgroundColorSpan(Color.GREEN);
|
BackgroundColorSpan的效果图
ForegroundColorSpan
android.text.style.ForegroundColorSpan
ForegroundColorSpan影响字符级的文本格式,它可以设置字符集的前景颜色也即文字颜色。
1
2
3
4
5
6
|
/**
* color: 前景颜色
*/
span =
new ForegroundColorSpan(Color.RED);
|
ForegroundColorSpan的效果图
ImageSpan
android.text.style.ImageSpan
ImageSpan影响字符级的文本格式。它可以生成图像字符。这是为数不多的文档齐全的Span所以enjoy it!
1
2
3
4
5
6
7
|
/**
* Context: 上下文
* resourceId: 图像资源id
*/
span =
new ImageSpan(
this, R.drawable.pic1_small);
|
ImageSpan的效果图
StyleSpan
android.text.style.StyleSpan
StyleSpan影响字符级的文本格式,它可以给字符集设置样式(blod、italic、normal)。
1
2
|
span =
new StyleSpan(Typeface.BOLD | Typeface.ITALIC);
|
StyleSpan的效果图
TypefaceSpan
android.text.style.TypefaceSpan
TypefaceSpan影响字符级的文本格式。它可以给字符设置字体集(monospace、serif等)。
1
2
|
span =
new TypefaceSpan(
"serif");
|
TypefaceSpan的效果图
TextAppearanceSpan
android.text.style.TextAppearanceSpan
TextAppearanceSpan影响字符级的文本格式。它可以给字符集设置外观(appearance)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
/**
* 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:连接文本的颜色
*/
span =
new TextAppearanceSpan(
this
, R.style.SpecialTextAppearance);
|
1
2
3
4
5
6
7
8
9
|
<-- 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>
|
TextAppearanceSpan效果图
AbsoluteSizeSpan
android.text.style.AbsoluteSizeSpan
AbsoluteSizeSpan影响字符级的文本格式。它可以设置一个字符集的绝对文字大小。
1
2
3
4
5
6
7
|
/**
* size: 大小
* dip: false,size单位为px,true,size单位为dip(默认为false)。
*/
span =
new AbsoluteSizeSpan(
24,
true);
|
AbsoluteSizeSpan的效果图
RelativeSizeSpan
android.text.style.RelativeSizeSpan
RelativeSizeSpan影响字符水平的文本格式。它可以设置字符集的文本大小。
1
2
|
span =
new RelativeSizeSpan(
2.0f);
|
RelativeSizeSpan的效果图
ScaleXSpan
android.text.style.ScaleXSpan
ScaleXSpan印象字符集的文本格式。它可以在x轴方向上缩放字符集。
1
2
|
span =
new ScaleXSpan(
3.0f);
|
ScaleXSpan的效果图
MaskFilterSpan
android.text.style.MaskFilterSpan
MaskFilterSpan影响字符集文本格式。它可以给字符集设置android.graphics.MaskFilter。
警告:BlurMaskFilter不支持硬件加速
1
2
3
4
|
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));
|
MaskFilterSpan的效果图: BlurMaskFilter
MaskFilterSpan的效果图: EmbossMaskFilter
Spans进阶
前景色(文字颜色)动画
前景色(文字颜色)动画
ForegroundColorSpan为只读。这意味实例化之后着你不能改变你不能改变前景色。所以,要做的第一件事就是编写一个MutableForegroundColorSpan。
MutableForegroundColorSpan.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
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才能刷新视图。
1
2
3
4
5
6
7
8
9
|
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。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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)让自定义属性动起来。不要忘记更新视图。
1
2
3
4
5
6
7
8
9
10
11
12
|
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) {
mText.setText(mSpannableString);
}
});
objectAnimator.start();
|
ActionBar”烟火”
ActionBar"烟火"
“烟火”动画是让文字随机淡入。首先,把文字切断成多个spans(例如,一个character的span),淡入spans后再淡入其它的spans。用前面介绍的MutableForegroundColorSpan,我们将创建一组特殊的span对象。在span组调用对应的setAlpha方法,我们随机设置每个span的透明度。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
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的透明度
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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给其加上动画。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
final FireworksSpanGroup spanGroup =
new FireworksSpanGroup();
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
@Override
public
int
getSize(Paint paint, CharSequence text,
int start,
int end, Paint.FontMetricsInt fm)
{
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);
}
|
自定义Span的效果
附加
Sample app包含了一些Spans进阶的例子,如下:
Progressive blur
Typewriter
总结
在编写这篇文章的过程中,我意识到Spans是真的像Drawable那样强大的,我认为它们还没有被充分运用。文本是一个应用程序的主要内容,它无处不在,所以不要忘记,通过Spans让它变得更具活力和吸引力!