本文作者
作者:风从影
链接:
http://www.jianshu.com/p/deb28c22852a
本文由作者授权发布。
如果你对Span不了解,建议先阅读TextView 图文混排 & 炫酷的段落级Span解析。
注:在编辑过程中,我将本文中所有的效果都跑了一遍。由于作者文中缺少示例,为了便于理解,我在文中添加了一些示例代码;其次,发现原文中部分类缺失,也对文中缺少的类也做了补充。
1 简介
之前已经讲过TextView的基础知识、段落级别的Span和字符级别的Span,分析了Android提供的一些Span的源码,这篇文字讲解如何自定义Span。
这篇文章中,由于段落级别的Span比较简单,在这不讲述这个类型的自定义Span。这篇着重讲述字符级的Span,并且结合Android提供动画机制制作出十分酷炫的动画Span。
2 给字符添加边框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:画笔
用法示例:
@Overrideprotected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); TextView tv = findViewById(R.id.id_tv_framespan); final SpannableString spannableString = new SpannableString( "helloworld,helloworld!helloworld,helloworld!helloworld," + "helloworld!helloworld,helloworld!"); spannableString.setSpan(new FrameSpan(), 0, 30, Spanned.SPAN_INCLUSIVE_INCLUSIVE); tv.setText(spannableString);}
3 图文垂直居中
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坐标可以通过计算得到,具体计算请看上面的源码。
用法示例:
@Overrideprotected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); TextView tv = findViewById(R.id.id_tv_framespan); Drawable drawable = getResources().getDrawable(R.drawable.ic_launcher); drawable.setBounds(0, 0, 50, 50); final SpannableString spannableString = new SpannableString( "helloworld,helloworld!helloworld,helloworld!helloworld," + "helloworld!helloworld,helloworld!"); spannableString.setSpan(new VerticalImageSpan(drawable), 0, 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE); tv.setText(spannableString);}
4 字体多色渐变
彩虹样的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度。
用法示例:
@Overrideprotected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); TextView tv = findViewById(R.id.id_tv_framespan); final SpannableString spannableString = new SpannableString( "helloworld,helloworld!helloworld,helloworld!helloworld," + "helloworld!helloworld,helloworld!"); spannableString.setSpan(new RainbowSpan(this), 0, 40, Spanned.SPAN_INCLUSIVE_INCLUSIVE); tv.setText(spannableString);}
5 字体多色渐变动画效果
如果要实现一个动画的彩虹样式,那么该如何实现呢?
其实结合上面的RainbowSpan和AnimateForegroundColorSpan的例子便可以实现AnimatedRainbowSpan。
实现思路:通过ObjectAnimator动画调整RainbowSpan中矩阵的平移,从而实现动画彩虹的效果。
代码如下所示:
首先是AnimatedColorSpan
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); } }}
配合属性动画:
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(); }};
6
打字效果
有了上面的例子,写TypeWriterSpan就变得十分简单了。
先创建TypeWriterSpanGroup
public class TypeWriterSpanGroup { private final float mAlpha; private final ArrayList<MutableForegroundColorSpan> mSpans; public 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; 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; }}
然后添加Span与添加动画,整体使用示例如下:
@Overrideprotected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); final TextView tv = findViewById(R.id.id_tv_framespan); String val = "helloworld,helloworld!helloworld,helloworld!helloworld"; final SpannableString spannableString = new SpannableString(val); // 添加Span final TypeWriterSpanGroup group = new TypeWriterSpanGroup(0); for(int index = 0 ; index <= val.length()-1 ; index++) { MutableForegroundColorSpan span = new MutableForegroundColorSpan(); group.addSpan(span); spannableString.setSpan(span, index, index + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } // 添加动画 ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(group, TYPE_WRITER_GROUP_ALPHA_PROPERTY, 0.0f, 1.0f); objectAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { //refresh tv.setText(spannableString); } }); 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(); } };
涉及到的类:
public class MutableForegroundColorSpan extends CharacterStyle implements UpdateAppearance { public static final String TAG = "MutableForegroundColorSpan"; private int mColor = Color.BLACK; private int mAlpha = 0 ; @Override public void updateDrawState(TextPaint tp) { tp.setColor(mColor); tp.setAlpha(mAlpha); } public int getColor() { return mColor; } public void setColor(int color) { this.mColor = color; } public void setAlpha(int alpha) { mAlpha = alpha; }}
7
还能更炫酷一点?
本节为我个人添加
上文我们已经学习了如何让字体变色已经打字效果,如果我将两个效果合到一起呢?
在上面的“//添加Span”上方添加如下两行代码即可:
spannableString.setSpan(new RainbowSpan(this), 0, val.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);tv.setText(spannableString);
相关链接:
字符级span
http://www.jianshu.com/p/be0d79b9d5e6
推荐阅读:
- 欢迎投稿 -
赞助商Google、滴滴 与 Udacity 联合开发的 Android 课程,有来自硅谷的实战项目,并提供一对一代码审阅和技术辅导,现在部分课程能免费体验,感兴趣的朋友可以扫下面的二维码。