Android TextView自定义背景高亮显示字体

一、效果和思路

要实现如下效果

错误的思路:

1. 直接使用一个TextView设置背景即可。
2.使用SpannableString设置BackgroundColorSpan即可。

上面这两种思路都会产生中间没有白色的分割线,连成一起。这是因为用一个TextView设置背景的时候,背景的设置只会在TextView的边框区域产生作用。要实现以上效果,需要自定义一个。

二、字体度量

字体的度量,是指对于指定字号的某种字体,在度量方面的各种属性,其描述参数包括:

  1. baseline:字符基线
  2. ascent:字符最高点到baseline的推荐距离
  3. top:字符最高点到baseline的最大距离
  4. descent:字符最低点到baseline的推荐距离
  5. bottom:字符最低点到baseline的最大距离
  6. 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来实现

  1. 自定义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);
    }
  1. 这里通过直接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;
    }


}

欢迎关注我的个人公众号:

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值