字符级Span解析
1 简介
之前已经讲过TextView的基础知识,和段落级别的Span,现在在这进一步进行讲解,这篇文字主要讲解如何给TextView设置字符级别的Span。如果一个Span想要影响段落层次的文本格式,则需要继承CharacterStyle。
2 CharacterStyle
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。
3 UpdateAppearance
如果一个Span修改字符级别的文本外观,则实现UpdateAppearance。
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
BackAndFront
UnderlineSpan和StrikethroughSpan:
UnderAndStrike
MaskFilterSpan:
Mask
可以看一下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));
4 UpdateLayout
如果一个Span修改字符级文本度量|大小,则实现UpdateLayout。在Android源码中,只有MetricAffectingSpan实现了UpdateLayout接口。
UpdateLayout
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的实现类进行讲述。
4.1 SubscriptSpan和SuperscriptSpan
SubscriptSpan和SuperscriptSpan实现字体的上下标展示,效果如下面的图片所示:
SubscriptSpan
SuperscriptSpan
其实这两个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);
}
4.2 AbsoluteSizeSpan和RelativeSizeSpan
AbsoluteSizeSpan和RelativeSizeSpan用来改变相应字符的字体大小。
/**
* size: 大小
* dip: false,size单位为px,true,size单位为dip(默认为false)。
*/
//设置文字大小为24dp
span = new AbsoluteSizeSpan(24, true);
AbsoluteSizeSpan
//设置文字大小为大2倍
span = new RelativeSizeSpan(2.0f);
RelativeSizeSpan
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);
}
4.3 ScaleXSpan
ScaleXSpan影响字符集的文本格式。它可以在x轴方向上缩放字符集。
//设置水平方向上放大3倍
span = new ScaleXSpan(3.0f);
ScaleXSpan
源码:
@Override
public void updateDrawState(TextPaint ds) {
ds.setTextScaleX(ds.getTextScaleX() * mProportion);
}
@Override
public void updateMeasureState(TextPaint ds) {
ds.setTextScaleX(ds.getTextScaleX() * mProportion);
}
4.4 StyleSpan、TypefaceSpan和TextAppearanceSpan
StyleSpan、TypefaceSpan和TextAppearanceSpan都可以字体的样式进行改变,StyleSpan可以对字体设置bold或者italic的字符样式,TypefaceSpan可以对字体设置其他的样式,TextAppearanceSpan通过xml文件从而对字体进行设置。
//设置bold+italic的字符样式
span = new StyleSpan(Typeface.BOLD | Typeface.ITALIC);
StyleSpan
//设置serif family
span = new TypefaceSpan("serif");
TypefaceSpan
span = new TextAppearanceSpan(this, R.style.SpecialTextAppearance);
<-- style.xml -->
<style name="SpecialTextAppearance" parent="@android:style/TextAppearance">
<item name="android:textColor">@color/color1</item>
<item name="android:textColorHighlight">@color/color2</item>
<item name="android:textColorHint">@color/color3</item>
<item name="android:textColorLink">@color/color4</item>
<item name="android:textSize">28sp</item>
<item name="android:textStyle">italic</item>
</style>
TextAppearanceSpan
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);
}
}
4.5 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);
}
5 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。
5.1 DynamicDrawableSpan
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<Drawable> wr = mDrawableRef;
Drawable d = null;
if (wr != null)
d = wr.get();
if (d == null) {
d = getDrawable();
mDrawableRef = new WeakReference<Drawable>(d);
}
return d;
}
private WeakReference<Drawable> mDrawableRef;
}
- 抽象方法getDrawable()告诉子类需要提供一个Drawable用来绘制;
- getSize方法中,通过设置FontMetricsInt,从而使得替代字符序列的baseline和图片的尾部对齐,而替代字符序列的垂直高度就为图片的高度;
- draw方法中,需要绘制图片的其实x坐标很明确就是x,y坐标可以通过多种方式获取,在baseline对齐的情况下可以等于top,也可以等于y-b.getBounds().bottom,还可以等于bottom-b.getBounds().bottom-descent,各种方法都可以。
在Android系统中,提供了一个ImageSpan继承了DynamicDrawableSpan,实现了通过多种方式生成Drawable。