自定义 ImageSpan 实现简单的文字图片背景效果

4,740 阅读4分钟

要实现这样一个效果

效果

最节省时间的办法就是一个水平的LinearLayout中嵌套三个textview来分别设置background和text 但这太low了 为了节省TextView来减少布局的层级

我们可以通过 使用SpannableStringBuilder来实现用一个TextView来实现这个功能


并且封装一个工具类来使用,只用传入TextView对象和背景图就可以了。

实现起来比较简单,但是踩了不少坑,我觉得值得记录一下。

首先要知道如何使用图文混排

通过SpannableStringBuilder这个类可以构造一个可以用来设置不同样式(Span)的SpannableStringBuilder对象, 方法如下(还有一个SpannableString,这两个类的关系就像String 与 StringBuilder一样,具体区别 我也没用过0_0):


            //可以通过构造方法来初始化
            SpannableStringBuilder ssb = new SpannableStringBuilder("这是一个字符串");
            //也同样可以进行拼接
            SpannableStringBuilder ssb1 = new SpannableStringBuilder();
            ssb1.append("这是另一个字符串");
  • 然后就是进行样式的修改 通过setSpan方法可以设置不同的样式,系统预设了很多样式的实现可以选择
    Span

  • 下面演示一些常用的预设样式例如:


         SpannableStringBuilder ssb = new SpannableStringBuilder("这是一个字符串");
        //第一个参数是样式,第二和第三个参数是要改变的区间,最后一个参数对TextView没有用
        //当是EditText的时候决定是否会对两侧新输入的文字进行同样的改变
        //这里的设置是对两侧都不改变
        ssb.setSpan(new UnderlineSpan(),0,2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        ssb.setSpan(new BackgroundColorSpan(Color.GREEN),0,1,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        ssb.setSpan(new ForegroundColorSpan(Color.RED),1,2,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        //获取图片
        Drawable d = getResources().getDrawable(R.mipmap.ic_launcher);
        //这行不能少 设置固有宽高  
        d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
        ImageSpan imageSpan = new ImageSpan(d);
        //替换一个文字为图片
        ssb.setSpan(imageSpan,2,3,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        tv_show.setText(ssb);
         //用超链接标记文本
        ssb.setSpan(new URLSpan("dsaas"), 3, 4,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        //斜粗体
        ssb.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), 4, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        //改变大小
        //这个是相对大小
        //ssb.setSpan(new RelativeSizeSpan(1.5f), 5, 6,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        //绝对大小 设置数值
        ssb.setSpan(new AbsoluteSizeSpan(30), 5, 6,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        ssb.setSpan(new StyleSpan(Typeface.ITALIC), 5, 6,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        //删除线标
        ssb.setSpan(new StrikethroughSpan(), 6, 7,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

上面的设置效果如下:
这里写图片描述

所以需要去实现一个自定义的Span,要满足我们的需求需要的效果就是在背景图片上绘制文字就可以了, 因此选择去继承一个ImageSpan来写我们的Span 重写ImageSpan的draw方法即可

代码如下:

  
   public class MySpan extends ImageSpan {



    private int textSize = 20;//默认
    private int color = Color.GRAY;
    private TextView mTextView;
    static float textboundhight;
    static float textY;

    /**
     *
     * @param d 接收图片
     * @param tv 通过传入的TextView获取需要的属性
     */
    public MySpan(Drawable d, TextView tv) {
        super(d);
        mTextView = tv;
        textSize = (int) mTextView.getTextSize();

    }


    @Override
    public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y,
                     int bottom, Paint paint) {
        //获取需要设置样式的字符串
        String str = text.subSequence(start, end).toString();
        //得到宽高
        Rect bounds = new Rect();
        //先根据TextView 属性 设置字体大小
        paint.setTextSize(textSize);
        //获得字符串所占空间大小
        paint.getTextBounds(str, 0, str.length(), bounds);
        //得到宽高
        int textHeight = bounds.height();
        int textWidth = bounds.width();

        //设置背景绘制宽高 根据字符串大小扩大一定比例 否则会紧贴字符
        getDrawable().setBounds(0, (int) (top*1.3), (int) (bounds.width()*1.3), bottom);
        //调用父类draw绘制背景
        super.draw(canvas, str, start, end, x, top, y, bottom, paint);
        //绘制文本
        //文本颜色
        paint.setColor(mTextView.getTextColors().getDefaultColor());
        //文本字体
        paint.setTypeface(Typeface.create("normal", Typeface.NORMAL));

        //得到之前设置的背景图的大小 
        Rect bounds1 = getDrawable().getBounds();

        //根据背景图算出 字符串居中绘制的位置
        float textX = x + bounds1.width() / 2 - bounds.width() / 2;
        if (textboundhight == 0) {
            textboundhight = bounds.height();
            textY = (bounds1.height()) / 2 + textboundhight / 2 ;
        }
        //绘制字符串
        canvas.drawText(str, textX, textY, paint);

    }
}
  • 到这里Span就写好了,下面是我封装的一个类 可以将传入的字符串数组和textView关联 很简单,直接贴代码:

    public class SpannableStringUtils {


    private static int startX;
    private static int endX;

    public static void getLabelStyleText(
            Context context, String[] strArray
            , TextView textView, @DrawableRes int backgroudId) {
        String message = "";
        String[] strings = strArray;
        for (int i = 0; i < strings.length; i++) {
            message += strings[i];
            if (i != strings.length - 1) {
                message += " ";
            }

        }


        SpannableStringBuilder ssb = new SpannableStringBuilder(message);
        for (int i = 0; i < strings.length; i++) {
            int length = strings[i].length();
            int lastLength = 0;
            if (i != 0) {
                lastLength = strings[i - 1].length()-1;
            }
            endX = startX+strings[i].length();
            ssb.setSpan(new MySpan(context.getResources().getDrawable(backgroudId), textView)
                    , startX, endX, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
            startX = endX+1;
        }
        textView.setText(ssb);
    }
}

实际使用中碰到一个bug 会导致所有文字挤到一起去,就像这样……
这里写图片描述

这个问题是因为延迟加载的TextView只进行了一次的测量绘制布局所导致的 如果是在Activity或Fragment中进行了完整的生命周期则不会有这个问题

但比如 Fragment或Activity先设置了一个加载时的loading布局 等数据加载完毕才替换为标签所在的布局时 就会产生这个问题

调用 Invalidate 或 postInvalidate 重新使TextView进行一遍绘制即可

SpannableStringUtils.getLabelStyleText(mContext, strData, tvHomeLabel, R.drawable.empty_button);
//或者在handler中使用invalidate
tvHomeLabel.postInvalidate();

这回真写完了

PS: 写完之后一看实现起来真是非常 可实现的时候就不一样了……各种蒙蔽 尤其是碰到挤成一坨的时候 那是何等日狗 …… 目测并不完善 有问题 或者建议 欢迎评论