Android自定义View系列:文字测量

我们在自定义 View 的时候经常会需要文字测量,使用 canvas.drawText() 实现,但是文字测量绘制也有难点和注意事项。

居中的纵向测量

静态文字和动态文字

文本有分为静态文字和动态文字。静态文字也就是固定不变化的文本,动态文字就是会动态更改的文本

知道这两个概念对文字测量比较重要,因为这决定了你在不同的场景要用不同的测量 API,达到绘制准确的目的和展示 View 良好的视觉效果。

getTextBounds()

比如我们在绘制一个进度条,我们希望在 View 中间显示文本:

String text = "abab";
canvas.drawText(text, getWidth() / 2f, getHeight() / 2f, paint);

绘制的结果会是这样的:

在这里插入图片描述

会发现文字横向是居中了,但是纵向视线效果并没有居中。

当然这并不是错误,而是文字是在基准线上:

在这里插入图片描述
文字绘制都有一个基准线 baseline,它并不是在文字的底部,比如文本修改为 agag

在这里插入图片描述
文本不同,基准线是固定的,这是 baseline 基准线。

知道文本是在基准线位置纵向往上偏移导致没有居中,那么就需要把偏移移回来。

那这个偏移又要怎么计算出来?我们需要计算文本上边界和下边界的差得到,通过 Paint.getTextBounds(String text, int start, int end, Rect bounds) 获取文本边界值:

在这里插入图片描述

paint.getTextBounds(text, 0, text.length, bounds);
// 获取到文本上下边界,有正负号
int left = bounds.left;
int top = bounds.top;
int right = bounds.right;
int bottom = bounds.bottom;

// 获取文本偏移
int offset = (bounds.top + bounds.bottom) / 2;
canvas.drawText(text, getWidth() / 2f, getHeight() / 2f - offset, paint);

需要注意的是,getTextBounds() 适合于测量静态固定的文字,不适合动态文字。如果用这个 API 测量会动态更改的文字,因为拿到的是文本的边界 top 和 bottom 计算就会不断变化,比如文本从 abab 到 ababg 不断变更,这就会导致文本不断跳动的问题。

ascent和descent

我们上面分析到使用 getTextBounds() 只适用于静态文字,在动态文字上显示可能会出现文本跳动问题。那么动态文字要怎么测量才比较精准?就是 ascentdescent
在这里插入图片描述
在这里插入图片描述

上面的四根线 top、bottom 、ascent、descent、baseline 基本是固定的,所以使用它们来计算动态文字,就能有一个相对精准的位置。

Paint.FontMetrics fm = new Paint.FontMatrics();
paint.getFontMatrics(fm);
float top = fm.top;
float bottom = fm.bottom;
float ascent = fm.ascent;
float descent = fm.descent;

// 获取文本偏移
float offset = (fm.descent + fm.ascent) / 2;
canvas.drawText(text, getWidth() / 2f, getHeight() / 2f - offset, paint);

对齐

使用 Paint.setTextAlign() 设置文本的对齐方式:

// Paint.Align.LEFT
// Paint.ALgin.TOP
// Paint.Align.RIGHT
// Paint.Align.BOTTOM
paint.setTextAlign(Paint.Align.LEFT);

换行

对于长文本换行,有两种方式:staticLayoutbreakText()

StaticLayout

public class Utils {

    public static float dp2px(float dp) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, Resources.getSystem().getDisplayMetrics());
    }
}

public class ImageTextView extends View {
    private static final String text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean justo sem, sollicitudin in" +
            "maximus a, vulputate id magna. Nulla non guam a massa sollicitudin commodo fermentum et est. Suspendisse" +
            "potenti. Praesent dolor dui, dignissim guis tellus tincidunt, porttitor vulputate nisl. Aenean tempus" +
            "lobortis finibus. Quisque nec nisl laoreet, placerat metus sit amet, consectetur est. Donec nec quam tortor." +
            "Aenean aliquet dui in enim venenatis, sed luctus ipsummaximus, Nam feugiat nisi rhoncus lacus facilisis" +
            "pellentesque nec vitae lorem. Donec et risus eu ligula dapibus lobortis vel vulputate turpis. Vestibulum" +
            "ante ipsum primis in faucibus orci luctus et ultrices posere cubilia Curae; In porttitor, risus aliguam" +
            "rutrum finibus, ex mi ultricies arcu, quis or nare lectus tortor nec metus. Donec ultricies metus at magna" +
            "cursus congue. Nam eu sem eget enim pretium venenatis. Duis nibh ligula, lacinia ac nisi vestibulum," +
            "vulputate lacinia tortor.";

    private final TextPaint textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
    private StaticLayout staticLayout;

    public ImageTextView(Context context) {
        this(context, null);
    }

    public ImageTextView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ImageTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        textPaint.setTextSize(Utils.dp2px(15));
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (staticLayout == null) {
            // CharSequence source:文本
            // TextPaint paint:文字绘制的画笔
            // int width:提供给文本显示的宽度,用于计算自动换行使用
            // Layout.Alignment align:
            // float spacingmult:纵向是否需要额外的高度,不需要填写1
            // float spacingadd:是否需要额外的空格,不需要填写0
            // boolean includepad:是否需要加上额外上下间隔,不需要填写false
            staticLayout = new StaticLayout(text, textPaint, getWidth(),
                    Layout.Alignment.ALIGN_NORMAL, 1, 0, false);
        }
        staticLayout.draw(canvas);
    }
}

在这里插入图片描述
StaticLayout 会自动帮你分词,剩余空间如果不够显示单词会自动换到下一行。

如果文本当中会嵌套有图片或其他控件就不适合使用了,需要我们自己去定义换行,使用 breakText()

breakText()

在文字中嵌套有图片的场景,我们需要自己一行行的去计算和绘制。breakText() 需要我们自己计算文本的显示和换行位置,没有 StaticLayout 的分词效果。

public class Utils {

    public static Bitmap getBitmap(Resources res, int width) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(res, R.drawable.test, options);
        options.inJustDecodeBounds = false;
        options.inDensity = options.outWidth;
        options.inTargetDensity = width;
        return BitmapFactory.decodeResource(res, R.drawable.test, options);
    }

    public static float dp2px(float dp) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, Resources.getSystem().getDisplayMetrics());
    }
}

public class ImageTextView extends View {
    private static final String text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean justo sem, sollicitudin in" +
            "maximus a, vulputate id magna. Nulla non guam a massa sollicitudin commodo fermentum et est. Suspendisse" +
            "potenti. Praesent dolor dui, dignissim guis tellus tincidunt, porttitor vulputate nisl. Aenean tempus" +
            "lobortis finibus. Quisque nec nisl laoreet, placerat metus sit amet, consectetur est. Donec nec quam tortor." +
            "Aenean aliquet dui in enim venenatis, sed luctus ipsummaximus, Nam feugiat nisi rhoncus lacus facilisis" +
            "pellentesque nec vitae lorem. Donec et risus eu ligula dapibus lobortis vel vulputate turpis. Vestibulum" +
            "ante ipsum primis in faucibus orci luctus et ultrices posere cubilia Curae; In porttitor, risus aliguam" +
            "rutrum finibus, ex mi ultricies arcu, quis or nare lectus tortor nec metus. Donec ultricies metus at magna" +
            "cursus congue. Nam eu sem eget enim pretium venenatis. Duis nibh ligula, lacinia ac nisi vestibulum," +
            "vulputate lacinia tortor.";

    private static final float IMAGE_WIDTH = Utils.dp2px(150);
    private static final float IMAGE_OFFSET = Utils.dp2px(100);

    private final TextPaint textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
    private final Paint.FontMetrics fontMetrics = new Paint.FontMetrics();
    private final float[] cutWidth = new float[1];

    private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
    private final Bitmap bitmap;

    public ImageTextView(Context context) {
        this(context, null);
    }

    public ImageTextView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ImageTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        textPaint.setTextSize(Utils.dp2px(15));
        textPaint.getFontMetrics(fontMetrics);

        bitmap = Utils.getBitmap(getResources(), (int) Utils.dp2px(150));
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawBitmap(bitmap, getWidth() - IMAGE_WIDTH, IMAGE_OFFSET, paint);

        int length = text.length();
        float fontSpacing = textPaint.getFontSpacing();
        float verticalOffset = fontSpacing;
        for (int start = 0, count; start < length; start += count, verticalOffset += fontSpacing) {
            float usableWidth; // 可绘制文本的宽度
            float textTop = verticalOffset + fontMetrics.top;
            float textBottom = verticalOffset + fontMetrics.bottom;
            if (textTop > IMAGE_OFFSET && textTop < (IMAGE_OFFSET + bitmap.getHeight())
                    || textBottom > IMAGE_OFFSET && textBottom < (IMAGE_OFFSET + bitmap.getHeight())) {
                // 文字在图片区域,文字要减去图片绘制区域宽度
                usableWidth = getWidth() - IMAGE_WIDTH;
            } else {
                // 文字不在图片区域,绘制到 View 宽度
                usableWidth = getWidth();
            }
            // 返回可以截止绘制的位置count,用于这一行文本绘制的结束位置
            // measureForward:是否从左往右测量
            // measureWidth:当前行截取完换行时,这一行还有剩余的像素
            count = textPaint.breakText(text, start, length, true, usableWidth, cutWidth);
            canvas.drawText(text, start, start + count, 0, verticalOffset, textPaint);
        }
    }
}

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值