热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

AndroidCanvasdrawText文字居中的一些事(图解)

AndroidCanvasdrawText文字居中的一些事(图解)-1.写在前面在实现自定义控件的过程中,常常会有绘制居中文字的需求,于是在网上搜了一些相关的博客,总是看的一脸懵逼

1.写在前面

在实现自定义控件的过程中,常常会有绘制居中文字的需求,于是在网上搜了一些相关的博客,总是看的一脸懵逼,就想着自己分析一下,在此记录下来,希望对大家能够有所帮助。

2.绘制一段文本

首先把坐标原点移动到控件中心(默认坐标原点在屏幕左上角),这样看起来比较直观一些,然后绘制x、y轴,此时原点向上y为负,向下y为正,向左x为负,向右x为正,以(0,0)坐标开始绘制一段文本:

@Override
public void draw(Canvas canvas) {
 super.draw(canvas);
 // 将坐标原点移到控件中心
 canvas.translate(getWidth() / 2, getHeight() / 2);
 // x轴
 canvas.drawLine(-getWidth() / 2, 0, getWidth() / 2, 0, paint);
 // y轴
 canvas.drawLine(0, -getHeight() / 2, 0, getHeight() / 2, paint);

 // 绘制文字
 paint.setTextSize(sp2px(50));
 canvas.drawText("YangLe", 0, 0, paint);
}

看下绘制的文本:

绘制文本

咦,为什么绘制的文本在第一象限,y坐标不是指定的0吗,为什么文本没有在x轴的上面或下面,而是穿过了x轴,带着这些疑问继续往下看:

首先看一个重要的类:

public static class FontMetrics {
 /**
 * The maximum distance above the baseline for the tallest glyph in
 * the font at a given text size.
 */
 public float top;
 /**
 * The recommended distance above the baseline for singled spaced text.
 */
 public float ascent;
 /**
 * The recommended distance below the baseline for singled spaced text.
 */
 public float descent;
 /**
 * The maximum distance below the baseline for the lowest glyph in
 * the font at a given text size.
 */
 public float bottom;
 /**
 * The recommended additional space to add between lines of text.
 */
 public float leading;
}

FontMetrics类是Paint的一个内部类,主要定义了绘制文本时的一些关键坐标位置,看下这些值都代表什么:

关键坐标

看图说话:

  • top:从基线(x轴)向上绘制区域的最高点,此值为负值
  • ascent:单行文本,从基线(x轴)向上绘制的推荐最高点,此值为负值
  • baseline:基线,此值为0
  • descent:单行文本,从基线(x轴)向下绘制的推荐最低点,此值为正值
  • bottom:从基线(x轴)向下绘制区域的最低点,此值为正值
  • leading:推荐的额外行距,一般为0

下面再来看看drawText这个方法:

/**
 * Draw the text, with origin at (x,y), using the specified paint. The origin is interpreted
 * based on the Align setting in the paint.
 *
 * @param text The text to be drawn
 * @param x The x-coordinate of the origin of the text being drawn
 * @param y The y-coordinate of the baseline of the text being drawn
 * @param paint The paint used for the text (e.g. color, size, style)
 */
public void drawText(@NonNull String text, float x, float y, @NonNull Paint paint) {
 super.drawText(text, x, y, paint);
}

重点看下x、y参数的含义:

  • x:绘制文本的起始x坐标
  • y:绘制文本的baseline在y轴方向的位置

有点难理解,举个栗子,上文中的x、y参数传的是(0,0),此时的baseline正好是坐标系中x轴,就相当于从y轴开始向右绘制,以x轴作为文本的baseline进行绘制。

如果参数传(0,10),此时绘制文本的baseline从x轴开始向下移动10px,也就是以y10作为文本的baseline进行绘制,y10就是绘制文本的baseline在y轴方向的位置。

注意:baseline是绘制文本的基线,相对于绘制文本区域来说,相当于x轴,向上为负(top、ascent),向下为正(descent、bottom),但是这个x轴并不是控件的x轴,切记切记!!!

还记得我们在上文中提出的疑问吗,这下可以解释了:

为什么绘制的文本在第一象限?

因为我们把坐标原点移到了控件中心,文本的baseline正好为x轴,top、ascent值为负,所以绘制的文本在第一象限。

y坐标不是指定的0吗,为什么文本没有在x轴的上面或下面,而是穿过了x轴?

drawText方法默认x轴方向是从左到右绘制的,y轴方向是从baseline为基准绘制的,文中的baseline正好为x轴,以baseline为基准绘制文本向下还有一段距离,所以文本穿过了x轴。

3.绘制居中的文本

在上文中,我们学习了如何绘制一段文本,以及其中参数和坐标的含义,接下来进入正题,看下如何才能绘制居中的文本。

首先看一张图,此时文本的baseline正好为x轴,如果想要文本居中显示的话,就需要先计算文本的宽度和高度:

  • 宽度:调用Paint的measureText方法就可以获得文本的宽度
  • 高度:文本的高度就是实际绘制区域的高度,可以用(fontMetrics.descent - fontMetrics.ascent)获取,因为ascent为负数,所以最终算出来的是两者的和

现在有了宽度,把绘制文本的x坐标向左移动(宽度 / 2)就可以水平居中,但是垂直方向就不能这么干了,我们要将文本向下移动baseline到文本中心的距离,也就是(高度 / 2 - fontMetrics.descent),如下图所示:

计算baseLineY

现在的公式为:

float baseLineY
= (fontMetrics.descent - fontMetrics.ascent) / 2 - fontMetrics.descent;
= -fontMetrics.ascent / 2 - fontMetrics.descent / 2;
= -(fontMetrics.ascent + fontMetrics.descent) / 2;
= Math.abs(fontMetrics.ascent + fontMetrics.descent) / 2;

Paint中也有获取ascent和descent值的方法,所以公式最终为:

float baseLineY = Math.abs(paint.ascent() + paint.descent()) / 2;

注意:此公式是相对于坐标原点在控件中心来计算的,如果坐标原点在左上角,baseLineY需要加上控件高度的一半。

float baseLineY = height / 2 + Math.abs(paint.ascent() + paint.descent()) / 2;

看下代码:

@Override
public void draw(Canvas canvas) {
 super.draw(canvas);
 // 将坐标原点移到控件中心
 canvas.translate(getWidth() / 2, getHeight() / 2);
 // x轴
 canvas.drawLine(-getWidth() / 2, 0, getWidth() / 2, 0, paint);
 // y轴
 canvas.drawLine(0, -getHeight() / 2, 0, getHeight() / 2, paint);

 // 绘制居中文字
 paint.setTextSize(sp2px(50));
 paint.setColor(Color.GRAY);
 // 文字宽
 float textWidth = paint.measureText("YangLe'Blog");
 // 文字baseline在y轴方向的位置
 float baseLineY = Math.abs(paint.ascent() + paint.descent()) / 2;
 canvas.drawText("YangLe'Blog", -textWidth / 2, baseLineY, paint);
}

看下居中了吗:

绘制居中文本

大功告成!

4.绘制多行居中的文本

注意:drawText方法不支持绘制多行文本

4.1 方式一

使用支持自动换行的StaticLayout:

/**
 * 绘制多行居中文本(方式1)
 *
 * @param canvas 画布
 */
private void drawCenterMultiText1(Canvas canvas) {
 String text = "ABC";

 // 画笔
 TextPaint textPaint = new TextPaint();
 textPaint.setAntiAlias(true);
 textPaint.setColor(Color.GRAY);

 // 设置宽度超过50dp时换行
 StaticLayout staticLayout = new StaticLayout(text, textPaint, dp2px(50),
  Layout.Alignment.ALIGN_CENTER, 1f, 0f, false);
 canvas.save();
 // StaticLayout默认从(0,0)点开始绘制
 // 如果需要调整位置,只能在绘制之前移动Canvas的起始坐标
 canvas.translate(-staticLayout.getWidth() / 2, -staticLayout.getHeight() / 2);
 staticLayout.draw(canvas);
 canvas.restore();
}

看下StaticLayout的构造方法参数含义:

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);
}
  • source:需要分行的文本
  • paint:画笔对象
  • width:layout的宽度,文本超出宽度时自动换行
  • align:layout的对其方式
  • spacingmult:相对行间距,相对字体大小,1f表示行间距为1倍的字体高度
  • spacingadd:基础行距偏移值,实际行间距等于(spacingmult + spacingadd)
  • includepad:参数未知

看下效果:

StaticLayout

使用StaticLayout,每行设置的宽度是相同的,当需求为每行显示不同长度的文本时,这种方式就不能使用了,别担心,接着来看下第二种方式。

4.2 方式二

使用循环drawText的方式进行绘制,看图说话:

计算baseLineY

现在需要绘制A、B、C三行文本,红色A代表每行文本默认的绘制位置,绿色的线代表每行文本的baseline,x轴为红色A的baseline,现在分为三种情况:

  • 文本在x轴上方:红色A的baseline向上移动a距离,总高度的/2 - 文本的top值(绝对值)
  • 文本在x轴中间:红色A的baseline向下移动b距离,计算公式请参考单行文本居中公式
  • 文本在x轴下方:红色A的baseline向下移动c距离,总高度的/2 - 文本的bottom值(绝对值)

看下代码:

/**
 * 绘制多行居中文本(方式2)
 *
 * @param canvas 画布
 */
private void drawCenterMultiText2(Canvas canvas) {
 String[] texts = {"A", "B", "C"};

 Paint.FontMetrics fOntMetrics= paint.getFontMetrics();
 // top绝对值
 float top = Math.abs(fontMetrics.top);
 // ascent绝对值
 float ascent = Math.abs(fontMetrics.ascent);
 // descent,正值
 float descent = fontMetrics.descent;
 // bottom,正值
 float bottom = fontMetrics.bottom;
 // 行数
 int textLines = texts.length;
 // 文本高度
 float textHeight = top + bottom;
 // 文本总高度
 float textTotalHeight = textHeight * textLines;
 // 基数
 float basePosition = (textLines - 1) / 2f;

 for (int i = 0; i  basePosition) {
   // x轴下,值为正
   // 总高度的/2 - 未绘制的文本高度 - 文本的bottom值(绝对值)
   baselineY = textTotalHeight / 2 - textHeight * (textLines - i - 1) - bottom;

  } else {
   // x轴中,值为正
   // 计算公式请参考单行文本居中公式
   baselineY = (ascent - descent) / 2;
  }

  canvas.drawText(texts[i], -textWidth / 2, baselineY, paint);
 }
}

对照上图再看代码就很好理解了,觉得代码中的公式还有可以优化的地方,如果你有好的方法,可以留言告诉我哈。
再看下中文版的多行文本:

多行居中文本

5.TextAlign

Paint的TextAlign属性决定了绘制文本相对于drawText方法中x参数的相对位置。
举个栗子:

  • Paint.Align.LEFT:默认属性,x坐标为绘制文本的最左侧坐标
  • Paint.Align.CENTER:x坐标为绘制文本的水平中心坐标
  • Paint.Align.RIGHT:x坐标为绘制文本的最右侧坐标

看图理解下:

Paint.Align.LEFT


Paint.Align.CENTER


Paint.Align.RIGHT

6.文本居中的公式

坐标原点在控件中心:

float baseLineY = Math.abs(paint.ascent() + paint.descent()) / 2;

坐标原点在控件左上角:

float baseLineY = height / 2 + Math.abs(paint.ascent() + paint.descent()) / 2;

7.写在最后

源码已经上传到GitHub上了,欢迎Fork,觉得还不错就Start一下吧!

GitHub传送门

点我下载本文Demo的Apk

总结


推荐阅读
  • 本文介绍了一个基本的同步Socket程序,演示了如何实现客户端与服务器之间的简单消息传递。此外,文章还概述了Socket的基本工作流程,并计划在未来探讨同步与异步Socket的区别。 ... [详细]
  • SpringBoot底层注解用法及原理
    2.1、组件添加1、Configuration基本使用Full模式与Lite模式示例最佳实战配置类组件之间无依赖关系用Lite模式加速容器启动过程,减少判断配置类组 ... [详细]
  • 本文介绍了如何通过创建自定义 XML 文件来修改 Android 中 Spinner 的项样式,包括颜色和大小的调整。 ... [详细]
  • 本文详细介绍了Oracle RMAN中的增量备份机制,重点解析了差异增量和累积增量备份的概念及其在不同Oracle版本中的实现。通过对比两种备份方式的特点,帮助读者选择合适的备份策略。 ... [详细]
  • 深入解析C++ Atomic编程中的内存顺序
    在多线程环境中,为了防止多个线程同时修改同一数据导致的竞争条件,通常会使用内核级同步对象,如事件、互斥锁和信号量等。然而,这些方法往往伴随着高昂的上下文切换成本。本文将探讨如何利用C++11中的原子操作和内存顺序来优化多线程编程,减少不必要的开销。 ... [详细]
  • ED Tree HDU4812 点分治+逆元
    这道题非常巧妙!!!我们进行点分治的时候,算出当前子节点的所有子树中的节点,到当前节点节点的儿子节点的距离,如下图意思就是当前节点的红色节点,我们要求出红色节点的儿子节点绿色节点, ... [详细]
  • 深入解析Nacos服务自动注册机制
    本文将探讨Nacos服务自动注册的具体实现方法,特别是如何通过Spring事件机制完成服务注册。通过对Nacos源码的详细分析,帮助读者理解其背后的原理。 ... [详细]
  • iOS 小组件开发指南
    本文详细介绍了iOS小部件(Widget)的开发流程,从环境搭建、证书配置到业务逻辑实现,提供了一系列实用的技术指导与代码示例。 ... [详细]
  • 利用Cookie实现用户登录状态的持久化
    本文探讨了如何使用Cookie技术在Web应用中实现用户登录状态的持久化,包括Cookie的基本概念、优势及主要操作方法,并通过一个简单的Java Web项目示例展示了具体实现过程。 ... [详细]
  • 本文将详细介绍如何配置并整合MVP架构、Retrofit网络请求库、Dagger2依赖注入框架以及RxAndroid响应式编程库,构建高效、模块化的Android应用。 ... [详细]
  • A1166 峰会区域安排问题(25分)PAT甲级 C++满分解析【图论】
    峰会是指国家元首或政府首脑之间的会议。合理安排峰会的休息区是一项复杂的工作,理想的情况是邀请的每位领导人都是彼此的直接朋友。 ... [详细]
  • 2022年4月15日的算法练习题,包括最长公共子序列和线段树的应用。 ... [详细]
  • 本文详细介绍了Golang中string类型的内部结构及其特性,包括字符串的定义、表示方式、数据结构以及相关的操作方法,如字符串拼接和类型转换等。 ... [详细]
  • 本文将作为我硕士论文的一部分,但鉴于其内容的独特性和趣味性,决定单独发布。文中将定义一些皮亚诺公理,并介绍如何使用这些公理进行等式替换,以证明定理。 ... [详细]
  • Exploring issues and solutions when defining multiple Faust agents programmatically. ... [详细]
author-avatar
mobiledu2502898417
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有