需求&效果
相信很多同学都多多少少接触过一些常用的Span,例如,用于设置TextView里某段文字字体大小的AbsoluteSizeSpan,可以改变背景颜色的BackgroundColorSpan,还有可以直接画出一个图片ImageSpan等等,常用的Span用法百度谷歌一下一大把,这里就不再赘述。今天,我想和大家分享稍微高♂级一点的内容:如何通过extends ReplacementSpan在TextView控件区域内画自己想画的东西。单是用文字说,你可能会感觉有点懵,下面我们先来看来自某个知名App里的ListView item的效果图:
上图里自带背景的“精”、“热”这两个小icon,就是我想要实现的最终效果。可能有人要说,这还不简单,我做一个水平LinearLayout,然后往里面放三个TextView也能实现,根本不需要用到Span。用常规的布局实现缺点太多了,容我一个个给你数:
1. 只用一个水平的LinearLayout实现不了,“阿狸”的“狸”字会出现在“热”icon的右下方,而不是在行首。
2. 这个是ListView的Item,如果icon显示的个数由服务端控制,动态addView会导致ListView滑动不够流畅,而提前在xml写好若干个View(如最大个数9个),会导致单个Item里View的个数增加,不够优雅,性能也不好。
3. 效果要求:垂直方向,icon的中轴线与正常文字的中轴线要对齐,一旦正常文字的字号改变,icon的位置也要手动调整。
4. …
如果icon是单纯的图片,其实用文章开始提到的ImageSpan就可以搞定,但是如果ui组的同事比较懒,不愿意因为改变色值或内容而重新切图呢,比如我们公司的(逃- -!!,那背景和字都得我们自己画,开始动手吧!
ReplacementSpan实践
关于ReplacementSpan这个类,开发者文档和源码里几乎没明确说明这玩意儿到底是啥。不过通过名称,我们可以猜测,这个是可以用作替换功能的Span,也就是用这个Span替代文字。
ReplacementSpan只有两个抽象方法需要我们@Override:
/**
* 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以及一堆坐标传给我们,我们覆盖这个方法,就可以在特定位置画一些我们想画的东西了。
下面我盗了一张FontMetrics图以助于你理解,其实TextView里文字所占的高度就是FontMetrics的descent到ascent的距离。PS:以baseline为参考线,descent为正,ascent为负
接下来show you code:
IconTextSpan.java
public class IconTextSpan extends ReplacementSpan {
private Context mContext;
private int mBgColorResId; //Icon背景颜色
private String mText; //Icon内文字
private float mBgHeight; //Icon背景高度
private float mBgWidth; //Icon背景宽度
private float mRadius; //Icon圆角半径
private float mRightMargin; //右边距
private float mTextSize; //文字大小
private int mTextColorResId; //文字颜色
private Paint mBgPaint; //icon背景画笔
private Paint mTextPaint; //icon文字画笔
public IconTextSpan(Context context, int bgColorResId, String text) {
if (TextUtils.isEmpty(text)) {
return;
}
//初始化默认数值
initDefaultValue(context, bgColorResId, text);
//计算背景的宽度
this.mBgWidth = caculateBgWidth(text);
//初始化画笔
initPaint();
}
/**
* 初始化画笔
*/
private void initPaint() {
//初始化背景画笔
mBgPaint = new Paint();
mBgPaint.setColor(mContext.getResources().getColor(mBgColorResId));
mBgPaint.setStyle(Paint.Style.FILL);
mBgPaint.setAntiAlias(true);
//初始化文字画笔
mTextPaint = new TextPaint();
mTextPaint.setColor(mContext.getResources().getColor(mTextColorResId));
mTextPaint.setTextSize(mTextSize);
mTextPaint.setAntiAlias(true);
mTextPaint.setTextAlign(Paint.Align.CENTER);
}
/**
* 初始化默认数值
*
* @param context
*/
private void initDefaultValue(Context context, int bgColorResId, String text) {
this.mContext = context.getApplicationContext();
this.mBgColorResId = bgColorResId;
this.mText = text;
this.mBgHeight = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 17f, mContext.getResources().getDisplayMetrics());
this.mRightMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, mContext.getResources().getDisplayMetrics());
this.mRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, mContext.getResources().getDisplayMetrics());
this.mTextSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 13, mContext.getResources().getDisplayMetrics());
this.mTextColorResId = R.color.white_a;
}
/**
* 计算icon背景宽度
*
* @param text icon内文字
*/
private float caculateBgWidth(String text) {
if (text.length() > 1) {
//多字,宽度=文字宽度+padding
Rect textRect = new Rect();
Paint paint = new Paint();
paint.setTextSize(mTextSize);
paint.getTextBounds(text, 0, text.length(), textRect);
float padding = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 4, mContext.getResources().getDisplayMetrics());
return textRect.width() + padding * 2;
} else {
//单字,宽高一致为正方形
return mBgHeight;
}
}
/**
* 设置右边距
*
* @param rightMarginDpValue
*/
public void setRightMarginDpValue(int rightMarginDpValue) {
this.mRightMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, rightMarginDpValue, mContext.getResources().getDisplayMetrics());
}
/**
* 设置宽度,宽度=背景宽度+右边距
*/
@Override
public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
return (int) (mBgWidth + mRightMargin);
}
/**
* draw
* @param text 完整文本
* @param start setSpan里设置的start
* @param end setSpan里设置的start
* @param x
* @param top 当前span所在行的上方y
* @param y y其实就是metric里baseline的位置
* @param bottom 当前span所在行的下方y(包含了行间距),会和下一行的top重合
* @param paint 使用此span的画笔
*/
@Override
public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
//画背景
Paint bgPaint = new Paint();
bgPaint.setColor(mContext.getResources().getColor(mBgColorResId));
bgPaint.setStyle(Paint.Style.FILL);
bgPaint.setAntiAlias(true);
Paint.FontMetrics metrics = paint.getFontMetrics();
float textHeight = metrics.descent - metrics.ascent;
//算出背景开始画的y坐标
float bgStartY = y + (textHeight - mBgHeight) / 2 + metrics.ascent;
//画背景
RectF bgRect = new RectF(x, bgStartY, x + mBgWidth, bgStartY + mBgHeight);
canvas.drawRoundRect(bgRect, mRadius, mRadius, bgPaint);
//把字画在背景中间
TextPaint textPaint = new TextPaint();
textPaint.setColor(mContext.getResources().getColor(mTextColorResId));
textPaint.setTextSize(mTextSize);
textPaint.setAntiAlias(true);
textPaint.setTextAlign(Paint.Align.CENTER); //这个只针对x有效
Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
float textRectHeight = fontMetrics.bottom - fontMetrics.top;
canvas.drawText(mText, x + mBgWidth / 2, bgStartY + (mBgHeight - textRectHeight) / 2 - fontMetrics.top, textPaint);
}
MainActivity.java
public class MainActivity extends AppCompatActivity {
private TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = (TextView) findViewById(R.id.tv);
textView.setTextSize(20);
List<ReplacementSpan> spans = new ArrayList<>();
String content = "Android是一种基于Linux的自由及开放源代码的操作系统,主要使用于移动设备,如智能手机和平板电脑,由Google公司和开放手机联盟领导及开发。尚未有统一中文名称,中国大陆地区较多人使用“安卓”或“安致”。";
StringBuilder stringBuilder = new StringBuilder();
//第一个Span
stringBuilder.append(" ");
IconTextSpan topSpan = new IconTextSpan(getApplicationContext(), R.color.colorPrimary, "置顶");
spans.add(topSpan);
//第二个Span
stringBuilder.append(" ");
IconTextSpan hotSpan = new IconTextSpan(getApplicationContext(), R.color.colorAccent, "热");
hotSpan.setRightMarginDpValue(5);
spans.add(hotSpan);
stringBuilder.append(content);
SpannableString spannableString = new SpannableString(stringBuilder.toString());
//循环遍历设置Span
for (int i = 0; i < spans.size(); i++) {
spannableString.setSpan(spans.get(i), i, i + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
textView.setText(spannableString);
}
}
最终的显示效果是这样的,聪明的小伙伴们,你们看懂了么~