自定义Span

作者: 风从影 | 来源:发表于2016-12-09 18:23 被阅读2748次

    1 简介

    之前已经讲过TextView的基础知识、段落级别的Span和字符级别的Span,分析了Android提供的一些Span的源码,这篇文字讲解如何自定义Span。
    这篇文章中,由于段落级别的Span比较简单,在这不讲述这个类型的自定义Span。这篇着重讲述字符级的Span,并且结合Android提供动画机制制作出十分酷炫的动画Span。


    2 FrameSpan

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

    1. 计算字符序列的宽度;
    2. 根据计算的宽度、上下坐标、起始坐标绘制矩形;
    3. 绘制文字

    展现效果如下所示:


    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方法里面的参数的意义。

    1. canvas:用来绘制的画布;
    2. text:整个text;
    3. start:这个Span起始字符在text中的位置;
    4. end:这个Span结束字符在text中的位置;
    5. x:这个Span的其实水平坐标;
    6. y:这个Span的baseline的垂直坐标;
    7. top:这个Span的起始垂直坐标;
    8. bottom:这个Span的结束垂直坐标;
    9. 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<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重新设置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<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

    FireworksSpan

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

    我们创建一个自定义属性动画的属性去更改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();
        }
    };
    

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

    9 相关链接

    Textview图文基础
    段落级span
    字符级span
    自定义span

    相关文章

      网友评论

      • woitaylor:挺有意思的,能不能定义一个垂直滚动的span?
      • 爱吃鱼的外星人:你好,请问下设置图文混排时候怎么设置文字与图片上下的间距,我用div标签设置,貌似没有效果
        <div style=" text-align: center; width:10px; height:20px;border:1px solid #ffffff"> </div> 能提供下思路么?
      • c1b405d8ae82:你好
        VerticalImageSpan 能支持点击图片替换成另外一个图片吗!!!!!!!!!或者给个思路,谢谢
      • 4640f78109a4:仿佛打开了一扇门

      本文标题:自定义Span

      本文链接:https://www.haomeiwen.com/subject/evqymttx.html