代码已上传Github,欢迎star
UI效果

ReplacementSpan介绍
ReplacementSpan类比较简单,主要用到getSize、draw这两个方法。下面是参数介绍。
int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm)
该方法的返回值会被作为span的width
text:TextView中要显示的文本内容
start、end:span的起始位置
fm:包含TextView的字体属性值,top、ascent、descent、bottom、leading
void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint)
在该方法内绘制span到画布
canvas:画布
text :TextView中要显示的文本内容
start、end:span的起始位置
x:Edge of the replacement closest to the leading margin(不知道该怎么翻译【捂脸】)
top:该行文字显示区域的top值
y: 字体的baseline值
bottom:该行文字显示区域的bottom值
paint:画笔
代码实现
计算绘制位置需要了解字体的相关知识,见下图

绘制背景边框
private void drawTagRect(Canvas canvas, float x, int y, Paint paint) {
paint.setColor(mColor);
paint.setAntiAlias(true);
Paint.FontMetricsInt fontMetrics = paint.getFontMetricsInt();
RectF oval = new RectF(x, y + fontMetrics.ascent, x + mSize - mRightMarginPx, y + fontMetrics.descent);
paint.setStyle(Paint.Style.STROKE);
canvas.drawRoundRect(oval, mRadiusPx, mRadiusPx, paint);
}
RectF(float left, float top, float right, float bottom)
使用x
作为left
,x + mSize - mRightMarginPx
作为right
,其中mSize
为span
的宽度。UI效果图中的Tag
距离右边的文字有空隙,所以这里需要减去空隙的宽度才是线框背景的宽度。
UI效果图中显示线框背景的高度正好等于右边文字高度,参考字体属性值的示意图,ascent线与descent线之间为文字显示区域。所以,使用y + fontMetrics.ascent
作为top
,y + fontMetrics.descent
作为bottom
。
绘制文字
private void drawTagText(Canvas canvas, CharSequence text, float x, int start, int end, int y, Paint paint) {
paint.setTextSize(mTextSizePx);
paint.setColor(mColor);
paint.setAntiAlias(true);
paint.setTextAlign(Paint.Align.CENTER);
Paint.FontMetricsInt fontMetrics = paint.getFontMetricsInt();
final int textCenterX = (int) x + (mSize - mRightMarginPx) / 2;
int textBaselineY = (y - fontMetrics.descent - fontMetrics.ascent) / 2 + fontMetrics.descent;
final String tag = text.subSequence(start, end).toString();
canvas.drawText(tag, textCenterX, textBaselineY, paint);
}
paint.setTextAlign(Paint.Align.CENTER);
设置画笔以textCenterX
为中心点绘制文字
int textBaselineY = (y - fontMetrics.descent - fontMetrics.ascent) / 2 + fontMetrics.descent;
计算出文字在背景线框中居中显示的baseline
值,便于理解,可以将上面的计算方式转换为
int textBaselineY = y / 2 + (fontMetrics.descent + Math.abs(fontMetrics.ascent)) / 2 - fontMetrics.descent;

下面是Tag字体属性值
top ... -32
ascent ... -28
descent ... 7
bottom ... 9
leading ... 0
可以发现top、ascent值为负,是因为这里是按照baseline为0的标准进行计算的
遇到的问题
-
设置不同倍数行间距对参数的影响
android:lineSpacingMultiplier="1.3"
start --- 0 end --- 2 x --- 0.0 top --- 0 y --- 42 bottom --- 68
android:lineSpacingMultiplier="3"
start --- 0 end --- 2 x --- 0.0 top --- 0 y --- 42 bottom --- 156
根据以上信息可以看出,调整TextView行间距,主要会影响到bottom的值,所以在绘制文字时不要使用bottom来计算位置
完整代码
public class TagSpan extends ReplacementSpan {
// span width
private int mSize;
// text and background wireframe color
private int mColor;
// tag text size
private int mTextSizePx;
// background radius
private int mRadiusPx;
// background wireframe right margin
private int mRightMarginPx;
public RadiusBackgroundSpan(int color, int textSizePx, int radiusPx, int rightMarginPx) {
mColor = color;
mTextSizePx = textSizePx;
mRadiusPx = radiusPx;
mRightMarginPx = rightMarginPx;
}
@Override
public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
mSize = (int) paint.measureText(text, start, end) + mRightMarginPx;
return mSize;
}
@Override
public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
drawTagRect(canvas, x, y, paint);
drawTagText(canvas, text, x, start, end, y, paint);
}
private void drawTagRect(Canvas canvas, float x, int y, Paint paint) {
paint.setColor(mColor);
paint.setAntiAlias(true);
Paint.FontMetricsInt fontMetrics = paint.getFontMetricsInt();
final float strokeWidth = paint.getStrokeWidth();
RectF oval = new RectF(x + strokeWidth + 0.5f, y + fontMetrics.ascent, x + mSize + strokeWidth + 0.5f - mRightMarginPx, y + fontMetrics.descent);
paint.setStyle(Paint.Style.STROKE);
canvas.drawRoundRect(oval, mRadiusPx, mRadiusPx, paint);
}
private void drawTagText(Canvas canvas, CharSequence text, float x, int start, int end, int y, Paint paint) {
paint.setTextSize(mTextSizePx);
paint.setColor(mColor);
paint.setAntiAlias(true);
paint.setTextAlign(Paint.Align.CENTER);
Paint.FontMetricsInt fontMetrics = paint.getFontMetricsInt();
final int textCenterX = (int) x + (mSize - mRightMarginPx) / 2;
int textBaselineY = (y - fontMetrics.descent - fontMetrics.ascent) / 2 + fontMetrics.descent;
final String tag = text.subSequence(start, end).toString();
canvas.drawText(tag, textCenterX, textBaselineY, paint);
}
}
网友评论