android 自定义span_自定义Span

本文详细介绍了如何在Android中自定义Span,包括字符级的FrameSpan实现文字边框,VerticalImageSpan实现图片与文字居中对齐,以及各种动画Span如AnimateForegroundColorSpan、RainbowSpan、AnimatedRainbowSpan、FireworksSpan和TypeWriterSpan的实现,展示了丰富的文字动画效果。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1 简介

之前已经讲过TextView的基础知识、段落级别的Span和字符级别的Span,分析了Android提供的一些Span的源码,这篇文字讲解如何自定义Span。

这篇文章中,由于段落级别的Span比较简单,在这不讲述这个类型的自定义Span。这篇着重讲述字符级的Span,并且结合Android提供动画机制制作出十分酷炫的动画Span。

2 FrameSpan

FrameSpan实现给相应的字符序列添加边框的效果,整体思路其实比较简单。

计算字符序列的宽度;

根据计算的宽度、上下坐标、起始坐标绘制矩形;

绘制文字

展现效果如下所示:

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:画笔

3 VerticalImageSpan

Google提供的ImageSpan和DynamicDrawableSpan只能实现图片和文字底部对齐或者是baseline对齐,现在VerticalImageSpan可以实现图片和文字居中对齐。

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坐标可以通过计算得到,具体计算请看上面的源码。

4 AnimateForegroundColorSpan

先讲述一个简单的动画Span的例子,这个动画是用来改变文字颜色的。

AnimateForegroundColorSpan

源代码如下:

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 MUTABLE_FOREGROUND_COLOR_SPAN_FC_PROPERTY =

new Property(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重新设置Span。

5 RainbowSpan

彩虹样的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度。

6 AnimatedRainbowSpan

AnimatedRainbowSpan

如果要实现一个动画的彩虹样式,那么该如何实现呢?

其实结合上面的RainbowSpan和AnimateForegroundColorSpan的例子便可以实现AnimatedRainbowSpan。

实现思路:通过ObjectAnimator动画调整RainbowSpan中矩阵的平移,从而实现动画彩虹的效果。

代码如下所示:

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 ANIMATED_COLOR_SPAN_FLOAT_PROPERTY

= new Property(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

FireworksSpan

“烟火”动画是让文字随机淡入。首先,把文字切断成多个spans(例如,一个character的span),淡入spans后再淡入其它的spans。用 前面介绍的MutableForegroundColorSpan,我们将创建一组特殊的span对象。在span组调用对应的setAlpha方法,我 们随机设置每个span的透明度。

private static final class FireworksSpanGroup {

private final float mAlpha;

private final ArrayList mSpans;

private FireworksSpanGroup(float alpha) {

mAlpha = alpha;

mSpans = new ArrayList();

}

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 FIREWORKS_GROUP_PROGRESS_PROPERTY =

new Property(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();

}

};

最后,我们创建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();

8 TypeWriterSpan

TypeWriterSpan

有了上面的例子,写TypeWriterSpan就变得十分简单了。

先创建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 mSpans;

private TypeWriterSpanGroup(float alpha) {

mAlpha = alpha;

mSpans = new ArrayList();

}

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 TYPE_WRITER_GROUP_ALPHA_PROPERTY =

new Property(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();

}

};

9 相关链接

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值