Span简单使用
在Android中,Span用来定义文本的样式。通过Span可以改变几个文字的颜色,让它们可点击,缩放文字大小甚至绘制自定义的项目符号点。
Span的价值是,可以将这些样式作用在字符级别或者段落级别。
Span是专门用来增强TextView样式的,Span通过改变TextPaint属性,在Canvas上绘制,甚至是改变文本的布局和影响像行高这样的元素,来改变文本样式。它可以被应用到部分或整段的文本中。
通常使用的套路是样式属性和Span组合使用,可以考虑将设置给TextView的样式属性作为一种“基本”样式,而 Span样式是应用在基本样式“之上”并且会覆盖基本样式的样式。例如,当给一个 TextView 设置了 textColor=”@color.blue” 属性且设置开头4个字符应用了 ForegroundColorSpan(Color.PINK),则开头4个字符会使用 span 设置的粉色,而其他文本使用 TextView 属性设置的颜色。
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/blue"/>
SpannableString spannable = new SpannableString(“Text styling”);
spannable.setSpan(
new ForegroundColorSpan(Color.PINK),
0, 4,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
myTextView.setText(spannable);
当使用 span 时,需要使用SpannedString, SpannableString 或 SpannableStringBuilder之一。 它们之间的区别在于text内容或markup是可改变的还是不可改变的,以及它们使用的内部结构:SpannedString 和 SpannableString 使用线性数组记录已添加的 span,而 SpannableStringBuilder 使用 区间树。
如何决定使用哪一个类:
- 创建后 文本 和 span 不可变 –> SpannedString
- 创建后文本不可变,仅需设置 少量的 span (<~ 10)? –> SpannableString
- 创建后需设置 文本 和 span –> SpannableStringBuilder
- 创建后需设置 大量的 span (>~ 10)? –> SpannableStringBuilder
Android framework在android.text.style包提供了20+的Span样式,通过2个维度可以对Span进行分类:
- 基于Span是否改变text的外形还是改变text的尺寸或布局
- 基于Span的作用范围是字符级别还是或段落级别
Span的实现原理是,Android framework定义了几个接口和抽象类,这些接口和抽象类有允许Span访问TextPaint或Canvas对象的方法,它们会在测量和渲染时被检查,达到改变文本样式的效果。
影响text外观的Span
这些Span可以影响text外观:文本或背景颜色、下划线、删除线等等,如下UML类图所示,类名所见即所得。
这些Span会触发文本重新绘制,而不会触发重新布局。这些 span 实现了 UpdateAppearance 且继承自 CharacterStyle。CharacterStyle的子类通过提供更新 TextPaint 的访问方法,定义了怎样绘制文本。
影响text尺寸或布局的Span
这些Span可以影响text的尺寸和布局,如文本绝对尺寸、相对尺寸、插入图片、上标、下标、字体、字体风格等,如下UML类图所示,类名所见即所得。这些Span都继承自MetricAffectingSpan。
影响文本字体大小的Span可能会使得text字符宽高变化,甚至多出来一行,其实现是通过监听,触发重新测量、进而重新计算布局,进而重新绘制。这些Span继承自MetricAffectingSpan类,这个抽象类通过提供对 TextPaint的访问,来影响文本测量,而 MetricAffectingSpan 继承自CharacterSpan,其子类在字符级别影响文本的外形。
字符级Span
抽象类CharacterStyle对文本产生的影响在字符级别,更新元素,如背景颜色、样式或大小,上面的影响text外观、影响text尺寸或布局的Span都是字符级的Span。 CharacterStyle主要就是一个抽象方法updateDrawState,影响绘制属性,总结下来就是,一支画笔走天下,什么效果都能渲染。 MetricAffectingSpan主要就是一个抽象方法updateMeasureState,影响测量,进而重新布局。
段落级Span
段落级别Span都实现了接口ParagraphStyle(空接口),这些Span可以更改整个文本块的对齐方式或者边距。继承自ParagraphStyle的Span必须作用于text整体,从第一个字符附加到单个段落的最后一个字符,否则Span不会被显示。 在 Android 中,段落是基于换行符 (\n) 定义的。
Framework中段落级的Span,如下UML类图所示,类名所见即所得。可以看到很多接口没有实现,系统是预留了很多能力的,方便自定义。
自定义Span
系统提供的Span样式虽多,但是未必有一款合你心意,自定义Span总是在所难免。在实现你自己的Span时,需要确定你的Span是会影响字符级别还是影响段落级别的文本,以及它是影响文本的布局还是影响文本的外观,据此选择需要扩展的基类和实现的接口。相应选择如下:
举个例子,你需要Span样式可以改变文本的大小和颜色。你可以扩展RelativeSizeSpan,由于 RelativeSizeSpan已经提供了updateDrawState和updateMeasureState回调,我们可以复写绘制状态回调并设置 TextPaint 的颜色。这只是一个自定义Span的例子而已,同样的效果你可以通过组合使用RelativeSizeSpan和ForegroundColorSpan来达成。
public class RelativeSizeColorSpan extends RelativeSizeSpan {
private int color;
public RelativeSizeColorSpan(float spanSize, int spanColor) {
super(spanSize);
color = spanColor;
}
@Override
public void updateDrawState(TextPaint textPaint) {
super.updateDrawState(textPaint);
textPaint.setColor(color);
}
}
Span使用最佳实践
基于使用场景,TextView#setText()方法有几种优化内存的方式。原理是,setText方法会copy一份text实例,在某些场景可以规避创建copy text实例。
text不变增加或移除Span
TextView#setText()因处理不同的Span有多个重载,例如,设置一个Spannable text:
textView.setText(spannableObject);
当调用setText()方法,TextView会copy Spannable作为SpannableString,并在内存中以CharSequence形态保存。这意味着text和Span是不可变的,当需要更新text和Span时,需要创建新的Spannable,并且调用setText()。 如果Span是可变的,使用setText(CharSequence text, TextView.BufferType type)更佳, 如下:
textView.setText(spannable, BufferType.SPANNABLE);
Spannable spannableText = (Spannable) textView.getText();
spannableText.setSpan(
new ForegroundColorSpan(color),
8, spannableText.getLength(),
SPAN_INCLUSIVE_INCLUSIVE);
上例中,由于BufferType.SPANNABLE参数,setText方法创建了SpannableString(可变markup,不可变文本),再次更新Span时,可以获取TextView中的Spannable引用,而非再次创建新的Spannable实例,优化内存使用。 需要注意的是,此时需要主动调用invalidate() 或者requestLayout(),根据更新的Span是影响外观的,还是影响尺寸和布局的而定。
TextView多次设置text
一些场景,比如RecyclerView.ViewHolder,存在TextView复用,导致多次设置text。 通常不使用BufferType参数的情况下,每次设置文本,TextView都会copy一份实例,以CharSequence的形态存在内存中。也就是,每次设置新的文本,TextView都会创建新的实例。 通过实现自己的Spannable#Factory并重写newSpannable()可以控制这个过程,并避免多余实例的创建。范例如下:
Spannable.Factory spannableFactory = new Spannable.Factory(){
@Override
public Spannable newSpannable(CharSequence source) {
return (Spannable) source;
}
};
需要注意的是,必须使用textView.setText(spannableObject, BufferType.SPANNABLE)这种方式设置文本,否则就会抛出ClassCastException。 需要告诉TextView使用自定义的Spannable#Factory,如下:
textView.setSpannableFactory(spannableFactory);
在获得TextView引用之后需要立刻设置,如果在使用RecyclerView,应该在view第一次被inflate出来之后立刻设置Factory,避免绑定数据时TextView#setText()出现多余的实例创建。
改变Span属性
如果需要改变一个可变Span的内部属性,比如改变BulletSpan的颜色,避免多次重头调用setText()方法,最佳实现方式是,保存Span的引用,再需要更新Span属性时,通过引用改变属性,然后调用invalidate() 或者 requestLayout()方法。 BulletSpan颜色改变的范例如下:
public class MainActivity extends AppCompatActivity {
private BulletPointSpan bulletSpan = new BulletPointSpan(Color.RED);
@Override
protected void onCreate(Bundle savedInstanceState) {
...
SpannableString spannable = new SpannableString("Text is spantastic");
// setting the span to the bulletSpan field
spannable.setSpan(bulletSpan, 0, 4, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
styledText.setText(spannable);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// change the color of our mutable span
bulletSpan.setColor(Color.GRAY);
// color won’t be changed until invalidate is called
styledText.invalidate();
}
});
}
}
Span支持绘制View
通过继承 ReplacementSpan 在 TextView 控件区域内画自己想画的东西。ReplacementSpan只有两个抽象方法需要我们重写:
/**
* Returns the width of the span
*/
public abstract int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm);
/**
* Draws the span into the canvas.
*/
public abstract void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint);
第一个方法getSize(),返回值就是Span替换文字后所占的宽度
第二个方法draw(),在TextView绘制时被调用,与此同时,会把canvas,text,paint以及一堆坐标传给我们,我们覆盖这个方法,就可以在特定位置画一些我们想画的东西了。
其实可以将TextView和ImageView的绘制在TextView中,可以更加方便地设置图片和文字的样式。也就是,可以整一个ViewSpan,传入外部的View,把这个View绘制在想要的位置。既然可以放一个View,那么为什么不直接放一个LinearLayout,绘制在TextView中,在LinearLayout中再放入View,这样的话,不是既可以设置View的样式,又可以控制View的位置。
public class MarkerViewSpan extends ReplacementSpan {
protected View view;
public MarkerViewSpan(View view ) {
super();
this.view = view;
this.view.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
}
@Override
public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, @Nullable Paint.FontMetricsInt fm) {
int widthSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
view.measure(widthSpec, heightSpec);
view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
if (fm != null) {
int height = view.getMeasuredHeight();
fm.ascent = fm.top = -height / 2;
fm.descent = fm.bottom = height / 2;
}
return view.getRight();
}
@Override
public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint) {
Paint.FontMetricsInt fm = paint.getFontMetricsInt();
int transY = (y + fm.descent + y + fm.ascent) / 2 - view.getMeasuredHeight() / 2;//计算y方向的位移
canvas.save();
canvas.translate(x, transY);
view.draw(canvas);
canvas.restore();
}
}
上面代码中,我们同样继承了 ReplacementSpan ,通过构造方法传入了一个View,在 getSize() 方法中测量View的尺寸,设置 fm(Paint.FontMetricsInt)的 ascent 和 descent;在 draw() 方法中定位位置,调用 view.draw(canvas),绘制出 View 的视图。
下面看一下Android是如何绘制文字的。
Android中绘制文字是根据基线定位的。
draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint)
方法中的 x 和 y 就是图中绿点的 x和y的坐标,y就是基线位置。ascent 和 descent 的含义上面图片中有文字提示。
在 getSize() 方法中我们已经设置了 ascent 和 descent。我们需要定位View的位置。下面是通过中线(centerLine)位置计算基线 (baseLine),现在我们知道了基线位置 y,和 ascent 和 descent。可以计算出 centerLine 位置。
①centerLine作为ascentLine和descentLine的中间线
centerLineY = (ascentLineY + descentLineY)/2
<=> centerLineY = (ascent + baselineY + descent + baselineY)/2
<=> centerLineY = baselineY + (ascent + descent)/2
<=>baselineY = centerLineY - (ascent + descent)/2
∵ ascent = fontMetrics.ascent, descent = fontMetrics.descent
∴ baseLineY = centerLineY - (fontMetrics.ascent + fontMetrics.descent)/2
得出centerLinerY的位置:centerLineY = (ascent + baselineY + descent + baselineY)/2 = (fm.ascent + y + fm.descent + y ) / 2。
由于画笔在基线位置开始绘制,所以需要找到 View 的开始绘制位置,将画笔移到那个位置绘制View,再恢复。而 View 需要与文字居中对齐,所以,需要让中线位置再减去 view 高度的一半。这样就得出了画笔需要移动的位置:
int transY = (y + fm.descent + y + fm.ascent) / 2 - view.getMeasuredHeight() / 2;
Span转发手势
需要让Span支持点击事件处理,需要给TextView设置setMovementMethod。TextView的源码中:
@Override
public boolean onTouchEvent(MotionEvent event) {
// 略去无关代码
if ((mMovement != null || onCheckIsTextEditor()) && isEnabled()
&& mText instanceof Spannable && mLayout != null) {
boolean handled = false;
if (mMovement != null) {
handled |= mMovement.onTouchEvent(this, (Spannable) mText, event);
}
final boolean textIsSelectable = isTextSelectable();
if (touchIsFinished && mLinksClickable && mAutoLinkMask != 0 && textIsSelectable) {
// The LinkMovementMethod which should handle taps on links has not been installed
// on non editable text that support text selection.
// We reproduce its behavior here to open links for these.
ClickableSpan[] links = ((Spannable) mText).getSpans(getSelectionStart(),
getSelectionEnd(), ClickableSpan.class);
if (links.length > 0) {
links[0].onClick(this);
handled = true;
}
}
}
// 略去无关代码
}
需要实现自定义点击事件处理机制,我们需要继承LinkMovementMethod,重写onTouchEvent方法。
private TouchableReplacementSpan touchedTarget;
@Override
public boolean onTouchEvent(final TextView textView, final Spannable spannable, MotionEvent event) {
int action = event.getActionMasked();
TouchableReplacementSpan touchableReplacementSpan = getTouchedSpan(textView, spannable, event);
if(null!=touchableReplacementSpan){
if (action == MotionEvent.ACTION_DOWN) {
touchedTarget=touchableReplacementSpan;
} else if(null!=touchedTarget&&touchedTarget!=touchableReplacementSpan){
//When the finger keep moving. It may be changing the span.
MotionEvent newEvent = MotionEvent.obtain(event);
newEvent.setAction(MotionEvent.ACTION_CANCEL);
touchedTarget.onTouchEvent(textView,newEvent);
touchedTarget=touchableReplacementSpan;
}
touchableReplacementSpan.onTouchEvent(textView,event);
} else if(action==MotionEvent.ACTION_UP||action==MotionEvent.ACTION_CANCEL){
if(null!=touchedTarget){
MotionEvent newEvent = MotionEvent.obtain(event);
newEvent.setAction(MotionEvent.ACTION_CANCEL);
touchedTarget.onTouchEvent(textView,newEvent);
touchedTarget=null;
}
}
return true;
}
/**
* Return the touch offset position.
* @param textView
* @param event
* @return
*/
private int getTouchedOffset(TextView textView, MotionEvent event){
int x = (int) event.getX();
int y = (int) event.getY();
x -= textView.getTotalPaddingLeft();
y -= textView.getTotalPaddingTop();
x += textView.getScrollX();
y += textView.getScrollY();
final Layout layout = textView.getLayout();
int line = layout.getLineForVertical(y);
int off=0;
try {
off=layout.getOffsetForHorizontal(line, x*1f);
} catch (IndexOutOfBoundsException e) {
}
return off;
}
/**
* Getting the touched span object from the touch event.
* @param widget
* @param spannable
* @param event
* @return
*/
private TouchableReplacementSpan getTouchedSpan(TextView widget, Spannable spannable, MotionEvent event){
int touchedOffset = getTouchedOffset(widget, event);
TouchableReplacementSpan[] spans = spannable.getSpans(touchedOffset, touchedOffset, TouchableReplacementSpan.class);
if (0 < spans.length){
return spans[0];
}
return null;
}
然后在自定义的ReplacementSpan中包含的View,进行事件的分发给对应的View。
Span扩展文本动画
将所有字符转换为 replacement span 来支持动画。然后,我们将文本行为抽象为文本控制器,来完全操纵所有动画 span。
需要获得所有的字符我们需要覆盖AppCompatTextView的setText方法,然后在方法中创建新的字符串,并将它设置给AppCompatTextView。
@Override
public void setText(CharSequence text, BufferType type) {
SpannableString newText = new SpannableString(text);
for(int i=0;i<newText.length();i++){
AnimationTextSpan animationTextSpan = new AnimationTextSpan(this,text,i,i+1);
animationTextSpan.setAlpha(0f);
newText.setSpan(animationTextSpan,i,i+1, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
}
//Invoke The setText method.
CharSequence oldText = getText();
this.oldText = oldText;
if(null!=oldText&&null!=newText){
pendingFlag|=FLAG_TRANSITION;
}
//Setting the new text.
super.setText(newText, BufferType.NORMAL);
//This method will be invoked from the superclass.
//So no matter if we initialize this field in this class. It will throw a null pointer exception.
if(null==textController){
textController=new DefaultTextController();
}
this.textController.attachToTextView(this);
this.prepareAnimator();
}
然后覆盖AppCompatTextView的onDraw方法绘制对应的动画属性,因为再给AppCompatTextView设置新字符串时保留了旧的字符串,所以现在我们有了字符动画的初始和最后的状态。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
AbsTextController textController = getTextController();
//Prepare the text controller
if(0!=(pendingFlag&FLAG_PREPARE)&&null!=textController){
pendingFlag^=FLAG_PREPARE;
textController.prepare();
}
//Start the the text controller
if(0!=(pendingFlag&FLAG_ANIMATOR)&&null!=textController){
pendingFlag^=FLAG_ANIMATOR;
textController.start();
}
//Start the text transform.
if(0!=(pendingFlag&FLAG_TRANSITION)){
pendingFlag^=FLAG_TRANSITION;
if(null!=textTransition && null!= oldText){
CharSequence newText = getText();
textTransition.transform(this, (Spanned) newText, (Spanned) oldText);
}
}
//Drawing the text transition.
drawTransformText(canvas,textTransition);
//Drawing the text controller.
drawTextController(canvas, textController);
}
/**
* Drawing the text transition.
* @param canvas
* @param textTransform
*/
private void drawTransformText(Canvas canvas,TextTransition textTransform) {
if(null!=textTransform){
canvas.save();
int totalPaddingLeft = getTotalPaddingLeft();
int totalPaddingTop = getTotalPaddingTop();
canvas.translate(totalPaddingLeft,totalPaddingTop);
TextPaint paint = getPaint();
textTransform.onDraw(this,canvas,paint);
canvas.restore();
}
}
/**
* Drawing the text controller.
* @param canvas
* @param textController
*/
private void drawTextController(Canvas canvas, AbsTextController textController) {
if(null!=textController){
canvas.save();
canvas.translate(getPaddingLeft(),getPaddingTop());
textController.onDraw(canvas);
canvas.restore();
}
}
AbsTextController是用于控制字符的属性变化,例如透明、位移、缩放、旋转等。TextTransition是用于将字符在新旧两种状态中做变化。
例如倒计时变化。
public interface TextTransition {
/**
* Transform the text form old text to new text.
* @param textView the text view.
* @param newText the new text spannable.
* @param oldText the old text spannable.
*/
void transform(TextView textView, Spanned newText, Spanned oldText);
/**
* Drawing something when the text transform from old text to new text.
* @param textView
* @param canvas
* @param paint
*/
void onDraw(@NonNull TextView textView, @NonNull Canvas canvas, @NonNull Paint paint);
}
public class CountDownTextTransform implements TextTransition {
private AnimationTextSpan[] oldTextSpanArray;
@Override
public void transform(TextView textView, Spanned newText, Spanned oldText) {
oldTextSpanArray = oldText.getSpans(0, oldText.length(), AnimationTextSpan.class);
AnimationTextSpan[] newTextSpanArray = newText.getSpans(0, newText.length(), AnimationTextSpan.class);
int length = Math.max(oldTextSpanArray.length, newTextSpanArray.length);
for(int i=0;i<length;i++){
AnimationTextSpan oldAnimationTextSpan=null;
if(i < oldTextSpanArray.length){
oldAnimationTextSpan = oldTextSpanArray[i];
}
AnimationTextSpan newAnimationTextSpan=null;
if(i < newTextSpanArray.length){
newAnimationTextSpan = newTextSpanArray[i];
}
if(null!=oldAnimationTextSpan&&null!=newAnimationTextSpan){
if(oldAnimationTextSpan.getWord()!=newAnimationTextSpan.getWord()){
transformOldSpan(oldAnimationTextSpan);
transformNewSpan(newAnimationTextSpan);
}
} else if(null!=oldAnimationTextSpan){
transformOldSpan(oldAnimationTextSpan);
} else if(null!=newAnimationTextSpan){
transformNewSpan(newAnimationTextSpan);
}
}
}
private void transformNewSpan(AnimationTextSpan newAnimationTextSpan) {
RectF newBounds = newAnimationTextSpan.getBounds();
newAnimationTextSpan.setTranslationY(-newBounds.height());
newAnimationTextSpan.setClipRect(newBounds, Region.Op.INTERSECT);
TextSpanPropertyAnimator textSpanPropertyAnimator = newAnimationTextSpan.propertyAnimator();
textSpanPropertyAnimator.translationY(0);
}
private void transformOldSpan(AnimationTextSpan oldAnimationTextSpan) {
RectF oldBounds = oldAnimationTextSpan.getBounds();
oldAnimationTextSpan.setClipRect(oldBounds, Region.Op.INTERSECT);
oldAnimationTextSpan.setTranslationY(0);
TextSpanPropertyAnimator textSpanPropertyAnimator = oldAnimationTextSpan.propertyAnimator();
textSpanPropertyAnimator.translationY(oldBounds.height());
}
@Override
public void onDraw(@NonNull TextView textView,@NonNull Canvas canvas, @NonNull Paint paint) {
if(null!=oldTextSpanArray){
for(AnimationTextSpan animationTextSpan:oldTextSpanArray){
animationTextSpan.drawText(canvas,paint);
}
}
}
}
在transform方法的变换中只是获取我们在AppCompatTextView的setText方法中生成的AnimationTextSpan,然后根据需要变化的属性进行赋值。然后在onDraw方法中将AnimationTextSpan进行绘制,从而实现动画。
AnimationTextSpan其实是继承了ReplacementSpan的支持动画的Span。支持属性动画和对象动画。
1、使用了自定义TextPropertyHolder类来封装属性动画的各种属性,包括透明、位移、缩放、旋转。然后使用ValueAnimator来进行各种属性的变化处理并传递给AnimationTextSpan,最后AnimationTextSpan使用Canvas和Paint进行文字的绘制。
/**
* Drawing the text information use all the fields inside.
* This function actually support us to drawing the text outside by using the boundary of the span.
* So no matter the span in the text view or not. we could draw this span anywhere.
* This is for text transform or something like that.
* @param canvas
* @param paint
*/
public void drawText(@NonNull Canvas canvas, @NonNull Paint paint){
if(!isWillNotDraw()) {
canvas.save();
//All the animation properties.
float alpha = propertyHolder.getAlpha();
float rotate = propertyHolder.getRotate();
float scaleX = propertyHolder.getScaleX();
float scaleY = propertyHolder.getScaleY();
float translationX = propertyHolder.getTranslationX();
float translationY = propertyHolder.getTranslationY();
RectF bounds = propertyHolder.getBounds();
//We are using canvas behavior to support all of the animation features.
canvas.rotate(rotate,bounds.centerX(),bounds.centerY());
canvas.scale(scaleX,scaleY,bounds.centerX(),bounds.centerY());
paint.setAlpha(Math.round(0xFF*alpha));
if(null!=clipRect&&null!=op){
canvas.clipRect(clipRect, op);
}
Rect textBounds = getTextBounds();
float offsetX=bounds.left;
float offsetY=bounds.top-textBounds.top;
canvas.drawText(text,start,end,translationX+offsetX,translationY+offsetY,paint);
canvas.restore();
}
}
2、使用ObjectAnimator来进行对象属性的变化处理,最后AnimationTextSpan使用Canvas和Paint进行文字的绘制。
override fun startAnimator(animationTextSpanList:List<AnimationTextSpan>) {
animationTextSpanList.forEachIndexed{ index,animationTextSpan->
val bounds = animationTextSpan.bounds
val left=bounds.left
val frame1= Keyframe.ofFloat(0f,-bounds.width())
val frame2= Keyframe.ofFloat(0.2f,left-60)
val frame3= Keyframe.ofFloat(0.8f,left+60)
val frame4= Keyframe.ofFloat(1.0f, left+width)
val objectAnimator = animationTextSpan.objectAnimator()
objectAnimator.setValues(PropertyValuesHolder.ofKeyframe("x", frame1, frame2, frame3,frame4))
objectAnimator.interpolator= LinearInterpolator()
objectAnimator.duration=4000
objectAnimator.startDelay=(animationTextSpanList.size-index)*100L
objectAnimator.repeatCount=ValueAnimator.INFINITE
objectAnimator.repeatMode= ValueAnimator.RESTART
objectAnimator.start()
}
}