Android 图解Canvas drawText文字居中的那些事

0
回复
97
查看
[复制链接]

377

主题

381

帖子

7417

安币

手工艺人

发表于 2019-5-15 10:35:01 | 显示全部楼层 |阅读模式
如果对本篇文章感兴趣,请前往,原文地址:http://www.apkbus.com/blog-822721-80063.html

![图片描述](/data/attachment/album/201905/15/102629ipzcau3scrdrlrur.jpg)## 1.写在前面在实现自定义控件的过程中,常常会有绘制居中文字的需求,于是在网上搜了一些相关的博客,总是看的一脸懵逼,就想着自己分析一下,在此记录下来,希望对大家能够有所帮助。## 2.绘制一段文本首先把坐标原点移动到控件中心(默认坐标原点在屏幕左上角),这样看起来比较直观一些,然后绘制x、y轴,此时原点向上y为负,向下y为正,向左x为负,向右x为正,以(0,0)坐标开始绘制一段文本:```@Overridepublic 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);}```看下绘制的文本:![图片描述](/data/attachment/album/201905/15/102657upmzmjecjp2jp6if.webp)咦,为什么绘制的文本在第一象限,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的一个内部类,主要定义了绘制文本时的一些关键坐标位置,看下这些值都代表什么:![图片描述](/data/attachment/album/201905/15/102702lrar8pqxxxa80ik0.webp)看图说话:- 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),如下图所示:![图片描述](/data/attachment/album/201905/15/102706ntmh6omrgyolqemt.webp)现在的公式为:>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;看下代码:```@Overridepublic 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);}```看下居中了吗:![图片描述](/data/attachment/album/201905/15/102917kdnm5r9gzcu1qg09.webp)大功告成!## 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:参数未知看下效果:![图片描述](/data/attachment/album/201905/15/102956suzc5k2ekz5qic2u.webp)使用StaticLayout,每行设置的宽度是相同的,当需求为每行显示不同长度的文本时,这种方式就不能使用了,别担心,接着来看下第二种方式。## 4.2 方式二使用循环drawText的方式进行绘制,看图说话:![图片描述](/data/attachment/album/201905/15/103145pnxlljn33jmm0kjr.webp)现在需要绘制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 < textLines; i  ) {        // 文本宽度        float textWidth = paint.measureText(texts);        // 文本baseline在y轴方向的位置        float baselineY;        if (i < basePosition) {// x轴上,值为负// 总高度的/2 - 已绘制的文本高度 - 文本的top值(绝对值)baselineY = -(textTotalHeight / 2 - textHeight * i - top);        } else if (i > basePosition) {// x轴下,值为正// 总高度的/2 - 未绘制的文本高度 - 文本的bottom值(绝对值)baselineY = textTotalHeight / 2 - textHeight * (textLines - i - 1) - bottom;        } else {// x轴中,值为正// 计算公式请参考单行文本居中公式baselineY = (ascent - descent) / 2;        }        canvas.drawText(texts, -textWidth / 2, baselineY, paint);    }}```对照上图再看代码就很好理解了,觉得代码中的公式还有可以优化的地方,如果你有好的方法,可以留言告诉我哈。再看下中文版的多行文本:![图片描述](/data/attachment/album/201905/15/103118k7q46z8ewit866p5.webp)## 5.TextAlignPaint的TextAlign属性决定了绘制文本相对于drawText方法中x参数的相对位置。举个栗子:- Paint.Align.LEFT:默认属性,x坐标为绘制文本的最左侧坐标- Paint.Align.CENTER:x坐标为绘制文本的水平中心坐标- Paint.Align.RIGHT:x坐标为绘制文本的最右侧坐标看图理解下:![图片描述](/data/attachment/album/201905/15/102800pje7kwnp1w67pjvj.webp)![图片描述](/data/attachment/album/201905/15/102804q1km7ad0qktaqp1t.webp)![图片描述](/data/attachment/album/201905/15/102808xeltkr1lke6w2veb.webp)## 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一下吧!代码:http://www.apkbus.com/thread-607118-1-1.html  继续阅读全文



想在安卓巴士找到更多优质博文,可移步博客区

如果对本篇文章感兴趣,请前往,
原文地址:
http://www.apkbus.com/blog-822721-80063.html
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

领先的中文移动开发者社区
18620764416
7*24全天服务
意见反馈:1294855032@qq.com

扫一扫关注我们

Powered by Discuz! X3.2© 2001-2019 Comsenz Inc.( 粤ICP备15117877号 )