1 簡介
之前已經(jīng)講過TextView的基礎(chǔ)知識、段落級別的Span和字符級別的Span捧颅,分析了Android提供的一些Span的源碼景图,這篇文字講解如何自定義Span。
這篇文章中碉哑,由于段落級別的Span比較簡單挚币,在這不講述這個類型的自定義Span。這篇著重講述字符級的Span扣典,并且結(jié)合Android提供動畫機制制作出十分酷炫的動畫Span妆毕。
2 FrameSpan
FrameSpan實現(xiàn)給相應(yīng)的字符序列添加邊框的效果,整體思路其實比較簡單贮尖。
- 計算字符序列的寬度笛粘;
- 根據(jù)計算的寬度、上下坐標(biāo)湿硝、起始坐標(biāo)繪制矩形薪前;
- 繪制文字
展現(xiàn)效果如下所示:
再來看一下代碼,其實代碼十分簡單关斜。
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方法里面的參數(shù)的意義示括。
- canvas:用來繪制的畫布;
- text:整個text痢畜;
- start:這個Span起始字符在text中的位置垛膝;
- end:這個Span結(jié)束字符在text中的位置鳍侣;
- x:這個Span的其實水平坐標(biāo);
- y:這個Span的baseline的垂直坐標(biāo)吼拥;
- top:這個Span的起始垂直坐標(biāo)倚聚;
- bottom:這個Span的結(jié)束垂直坐標(biāo);
- paint:畫筆
3 VerticalImageSpan
Google提供的ImageSpan和DynamicDrawableSpan只能實現(xiàn)圖片和文字底部對齊或者是baseline對齊凿可,現(xiàn)在VerticalImageSpan可以實現(xiàn)圖片和文字居中對齊惑折。
圖中的圖片保持了和文字居中對齊,現(xiàn)在來看看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設(shè)置從而實現(xiàn)圖片和文字居中對齊唬复,其實計算的根本為計算baseline的位置,因為TextView是按照baseline對齊的全肮。
分析getSize方法可以知道這個圖片的baseline為圖片中央往下fontHeight / 2敞咧,這樣也就實現(xiàn)了圖片和文字的居中對齊。
draw方法用來繪制圖片辜腺,繪制x坐標(biāo)為span的其實坐標(biāo)休建,繪制y坐標(biāo)可以通過計算得到,具體計算請看上面的源碼评疗。
4 AnimateForegroundColorSpan
先講述一個簡單的動畫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<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 alphaForegroundColorSpanGroup, Integer value) {
alphaForegroundColorSpanGroup.setForegroundColor(value);
}
@Override
public Integer get(MutableForegroundColorSpan span) {
return span.getForegroundColor();
}
};
其實整個邏輯比較簡單百匆,通過Property不斷給span更換顏色砌些,然后動畫update的時候給TextView重新設(shè)置Span。
5 RainbowSpan
彩虹樣的Span加匈,其實實現(xiàn)起來也是很簡單的存璃,主要是用到了Paint的Shader技術(shù),效果如下所示:
源代碼如下所示:
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是從上到下進行繪制雕拼,因此這里需要用到矩陣纵东,然后將矩陣旋轉(zhuǎn)90度。
6 AnimatedRainbowSpan
如果要實現(xiàn)一個動畫的彩虹樣式啥寇,那么該如何實現(xiàn)呢偎球?
其實結(jié)合上面的RainbowSpan和AnimateForegroundColorSpan的例子便可以實現(xiàn)AnimatedRainbowSpan。
實現(xiàn)思路:通過ObjectAnimator動畫調(diào)整RainbowSpan中矩陣的平移辑甜,從而實現(xiàn)動畫彩虹的效果衰絮。
代碼如下所示:
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<AnimatedColorSpan, Float> ANIMATED_COLOR_SPAN_FLOAT_PROPERTY
= new Property<AnimatedColorSpan, Float>(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);
}
}
}
7 FireworksSpan
“煙火”動畫是讓文字隨機淡入。首先磷醋,把文字切斷成多個spans(例如猫牡,一個character的span),淡入spans后再淡入其它的spans子檀。用 前面介紹的MutableForegroundColorSpan镊掖,我們將創(chuàng)建一組特殊的span對象。在span組調(diào)用對應(yīng)的setAlpha方法褂痰,我 們隨機設(shè)置每個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; }
}
我們創(chuàng)建一個自定義屬性動畫的屬性去更改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.setAlpha(value);
}
@Override
public Float get(FireworksSpanGroup spanGroup) {
return spanGroup.getAlpha();
}
};
最后,我們創(chuàng)建span組并使用一個ObjectAnimator給其加上動畫缩歪。
final FireworksSpanGroup spanGroup = new FireworksSpanGroup();
//初始化包含多個spans的grop
//spanGroup.addSpan(span);
//給ActionBar的標(biāo)題設(shè)置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)
{
//更新標(biāo)題
setTitle(mActionBarTitleSpannableString);
}
});
objectAnimator.start();
8 TypeWriterSpan
有了上面的例子归薛,寫TypeWriterSpan就變得十分簡單了。
先創(chuàng)建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<MutableForegroundColorSpan> mSpans;
private TypeWriterSpanGroup(float alpha) {
mAlpha = alpha;
mSpans = new ArrayList<MutableForegroundColorSpan>();
}
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<TypeWriterSpanGroup, Float> TYPE_WRITER_GROUP_ALPHA_PROPERTY =
new Property<TypeWriterSpanGroup, Float>(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();
}
};