要实现这样一个效果

最节省时间的办法就是一个水平的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方法可以设置不同的样式,系统预设了很多样式的实现可以选择
下面演示一些常用的预设样式例如:
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: 写完之后一看实现起来真是非常 可实现的时候就不一样了……各种蒙蔽 尤其是碰到挤成一坨的时候 那是何等日狗 …… 目测并不完善 有问题 或者建议 欢迎评论

