文章目录
- 一、效果和思路
- 二、字体度量
- 三、实现
- 一、自定义Span
- 二、自定义View来实现
一、效果和思路
要实现如下效果
错误的思路:
1. 直接使用一个TextView设置背景即可。
2.使用SpannableString设置BackgroundColorSpan即可。
上面这两种思路都会产生中间没有白色的分割线,连成一起。这是因为用一个TextView设置背景的时候,背景的设置只会在TextView的边框区域产生作用。要实现以上效果,需要自定义一个。
二、字体度量
字体的度量,是指对于指定字号的某种字体,在度量方面的各种属性,其描述参数包括:
- baseline:字符基线
- ascent:字符最高点到baseline的推荐距离
- top:字符最高点到baseline的最大距离
- descent:字符最低点到baseline的推荐距离
- bottom:字符最低点到baseline的最大距离
- leading:行间距,即前一行的descent与下一行的ascent之间的距离
Paint.FontMetrics(Int)类,定义了字符的ascent、top、descent和bottom。
注意 :这几个属性值,是相对于baseline的坐标值,而不是距离值.
由上面的图我们可以得到以下几个结论:
- 字符的高度=descent++Math.abs(ascent);
- 字符的行高=字符的高度+行间距(也及leading)=Math.abs(ascent) + descent + leading=getLineHeight()
- 字符串的宽度=Paint.measureText(“xxxx”),字符串的宽高也可以通过Paint.getTextBounds()获得。
三、实现
了解以上知识,我们的思路是绘制高度为字符的高度,宽度为字符串的宽度的一个矩形背景即可。因此我们可以采用以下方式来实现:
一、自定义Span
这种方式实现LineBackgroundSpan接口主要实现drawBackground方法:
/**
* start 换行的起始位置
* end 换行的结束位置
* lineNumber 所在的行号
*/
void drawBackground(@NonNull Canvas canvas, @NonNull Paint paint,
@Px int left, @Px int right,
@Px int top, @Px int baseline, @Px int bottom,
@NonNull CharSequence text, int start, int end,
int lineNumber);
代码如下:
package com.livideo.emptyproject;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.text.style.LineBackgroundSpan;
import androidx.annotation.NonNull;
public class CustomTextSpan implements LineBackgroundSpan {
private final Paint mPaint;
public CustomTextSpan(){
mPaint=new Paint();
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(Color.parseColor("#FF03DAC5"));
mPaint.setAntiAlias(true);
}
@Override
public void drawBackground(@NonNull Canvas canvas, @NonNull Paint paint, int left, int right, int top, int baseline, int bottom, @NonNull CharSequence text, int start, int end, int lineNumber) {
int legth = (int) paint.measureText(text.subSequence(start,end).toString());
Rect rect = new Rect();
rect.left=0;
rect.right=legth;
rect.top= (int) (baseline+paint.ascent());
rect.bottom= (int) (baseline+paint.descent());
canvas.drawRect(rect,mPaint);
}
}
这里要注意获得的ascent和descent,获取字符串的宽度一定要用绘制字体的paint来获得,不能使用绘制背景的mPaint。
二、自定义View来实现
- 自定义view的实现方式有个难点在于绘制文字换行的问题。
使用drawText()并不能自动换行,我们可以采用StaticLayout来实现换行并绘制文字。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
StaticLayout staticLayout = new StaticLayout(content, textPaint, canvas.getWidth(), Layout.Alignment.ALIGN_NORMAL, 1.0F, 0,
false);
staticLayout.draw(canvas);
}
- 这里通过直接new 对象创建staticLayout,实际上这个方式已经在API23以上被弃用转而采用build模式来创建,但是为了低版本的适配我们采用这种方式。
/**
* source 传入被绘制的内容
* width 宽度,字符超过这个宽度会自动换行
* align 对齐方式有ALIGN_CENTER,ALIGN_NORMAL,ALIGN_OPPOSITE 三种
* spacingmult 相对行间距,spacingadd 相对字体大小,1.5f表示行间距为1.5倍的字体高度
* spacingadd 在基础行距上添加多少。实际行间距等于这两者的和
* includedpad 在设置字体的ascent 和descent中是否包含padding,默认是true的,在阿拉伯语和其他语言时有用。我们这里设置false
*/
public StaticLayout(CharSequence source, TextPaint paint,
int width,
Alignment align, float spacingmult, float spacingadd,
boolean includepad) {
this(source, 0, source.length(), paint, width, align,
spacingmult, spacingadd, includepad);
}
通过上述代码我们只能绘制出一个带换行的textview,要想绘制出背景,必须要知道每一行的起始位置和结束位置,及每一行字体的baseline的位置。
staticLayout.getLineCount()可以获得行数,staticLayout.getLineBaseline(int line)也即是基线的位置。
private void drwaBackGround(Canvas canvas, StaticLayout staticLayout) {
for (int i = 0; i < staticLayout.getLineCount(); i++) {
Paint.FontMetrics fontMes = staticLayout.getPaint().getFontMetrics();
RectF rect = new RectF();
rect.left = staticLayout.getLineLeft(i);
rect.right = staticLayout.getLineRight(i);
//注意这里绘制背景的top和bottom取得是字体的ascent和descent;
//如果你bottom取的值是staticLayout.getLineDescent(i)则绘制的是
//字符的行高也就是字符的高度加上行间距
rect.top=staticLayout.getLineBaseline(i)+fontMes.ascent;
rect.bottom=staticLayout.getLineBaseline(i)+fontMes.descent;
canvas.drawRect(rect, bgPaint);
}
}
完整代码如下
package com.livideo.emptyproject;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.text.Layout;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import androidx.annotation.Nullable;
public class MyTextView extends View {
private String content = "";
private TextPaint textPaint;
private Paint bgPaint;
public MyTextView(Context context) {
super(context);
initView(context);
}
public MyTextView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initView(context);
}
public MyTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context);
}
private void initView(Context context) {
textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
textPaint.setColor(Color.RED);
textPaint.setStyle(Paint.Style.FILL);
textPaint.setAntiAlias(true);
textPaint.setTextSize(60);
bgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
bgPaint.setColor(Color.GREEN);
bgPaint.setStyle(Paint.Style.FILL);
bgPaint.setAntiAlias(true);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
StaticLayout staticLayout = new StaticLayout(content, textPaint, canvas.getWidth(), Layout.Alignment.ALIGN_NORMAL, 1.0F, 0,
false);
drwaBackGround(canvas, staticLayout);
staticLayout.draw(canvas);
}
private void drwaBackGround(Canvas canvas, StaticLayout staticLayout) {
for (int i = 0; i < staticLayout.getLineCount(); i++) {
Paint.FontMetrics fontMes = staticLayout.getPaint().getFontMetrics();
RectF rect = new RectF();
rect.left = staticLayout.getLineLeft(i);
rect.right = staticLayout.getLineRight(i);
//注意这里绘制背景的top和bottom取得是字体的ascent和descent;
//如果你bottom取的值是staticLayout.getLineDescent(i)则绘制的是
//字符的行高也就是字符的高度加上行间距
rect.top=staticLayout.getLineBaseline(i)+fontMes.ascent;
rect.bottom=staticLayout.getLineBaseline(i)+fontMes.descent;
canvas.drawRect(rect, bgPaint);
}
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}