• 自定义 View 绘制文字 HenCoder-3 [MD]


    博文地址

    我的GitHub 我的博客 我的微信 我的邮箱
    baiqiantao baiqiantao bqt20094 baiqiantao@sina.com

    目录

    扔物线自定义 View 系列教程总结-3

    全文整理自 扔物线(HenCoder)自定义 View 系列文章

    重新整理的目标:

    • 内容压缩:去除活跃气氛的段子、图片,去除无意义的解释、代码,去除不刚兴趣的内容,压缩比至少 50%
    • 排版优化:更清晰的结构,更精简的标题,更规范的缩进、标点符号、代码格式,好的结构才能更好的吸收
    • MarkDown:以标准的 MarkDown 格式重新编排,纯文本更易迭代维护

    扔物线自定义 View 系列教程分绘制布局触摸反馈三部分内容。

    文字的绘制

    文字的绘制所能控制的内容太多太细,其中有一部分内容并不是很常用,所以这期的内容只要做到能理解、知道有什么东西、大概怎么用就好,到你真正需要用的时候再拐回来看就是。

    Canvas 绘制文字的方式

    Canvas 的文字绘制方法有三个:drawText()drawTextRun()drawTextOnPath()

    drawText 绘制文字

    drawText(String text, float x, float y, Paint paint) //文字内容、文字的坐标
    

    注意:这个坐标并不是文字的左上角,而是一个与左下角比较接近的位置。如果你像绘制其他内容一样,在绘制文字的时候把坐标填成 (0, 0),文字并不会显示在 View 的左上角,而是会几乎完全显示在 View 的上方,到了 View 外部看不到的位置:

    坐标 y:baseline 基线

    为什么其它的 Canvas.drawXXX() 方法,都是以左上角作为基准点的,而 drawText() 却是文字左下方?

    这是因为,drawText() 参数中的 y ,指的是文字的 基线( baseline ) 的位置。也就是这条线:

    众所周知,不同的语言和文字,每个字符的高度和上下位置都是不一样的。要让不同的文字并排显示的时候整体看起来稳当,需要让它们上下对齐。但这个对齐的方式,不能是简单的「底部对齐」或「顶部对齐」或「中间对齐」,而应该是一种类似于「重心对齐」的方式。就像电线上的小鸟一样:

    每只小鸟的最高点和最低点都不一样,但画面很平衡

    而这个用来让所有文字互相对齐的基准线,就是 基线(baseline)

    坐标 x:字符空隙

    从前面图中的标记可以看出来,「Hello HenCoder」绘制出来之后的 x 点并不是字母 "H" 左边的位置,而是比它的左边再往左一点点。那么这个「往左的一点点」是什么呢?

    它是字母 "H" 的左边的空隙。绝大多数的字符,它们的宽度都是要略微大于实际显示的宽度的。字符的左右两边会留出一部分空隙,用于文字之间的间隔,以及文字和边框的间隔。就像这样:

    用竖线标记出边界后的文字。

    所以,明白为什么 x 坐标在 "H" 的左边再往左一点点的位置,而不是紧贴着 "H" 的左边线了吗?就是因为 "H" 的这个留出的空隙。

    drawTextRun 运行时环境

    这个方法是在 API 23 新加入的,并且对中英文没用,不需要关心。

    drawTextOnPath 绘制文字

    沿着一条 Path 来绘制文字。这是一个耍杂技的方法。

    drawTextOnPath(String text, Path path, float hOffset, float vOffset, Paint paint) //文字相对于 Path 的水平偏移量和竖直偏移量
    
    canvas.drawPath(path, paint); // 把 Path 也绘制出来,理解起来更方便
    canvas.drawTextOnPath("Hello HeCoder", path, 0, 0, paint);
    

    使用 drawTextOnPath() 绘制时,Path 的拐弯处要用圆角,别用尖角,否则拐角处的文字就重叠在一起了。

    StaticLayout 绘制多行文字

    Canvas.drawText() 只能绘制单行的文字,而不能换行:

    • 它不能在 View 的边缘自动折行:到了 View 的边缘处,文字继续向后绘制到看不见的地方,而不是自动换行
    • 它不能在换行符 处换行:在换行符 的位置并没有换行,而只是加了个空格

    如果要使用 Canvas.drawText() 绘制多行的文字,你必须自行把文字切断后分多次使用 drawText() 来绘制。使用 StaticLayout 可以解决这个问题。

    StaticLayout 并不是一个 View 或者 ViewGroup ,而是 android.text.Layout 的子类,它是纯粹用来绘制文字的。StaticLayout 既可以为文字设置宽度上限来让文字自动换行,也会在 处主动换行。

    构造方法:StaticLayout(CharSequence source, TextPaint paint, int width, Layout.Alignment align, float spacingmult, float spacingadd, boolean includepad)

    • width:文字区域的宽度,文字到达这个宽度后就会自动换行
    • align:文字的对齐方向
    • spacingmult:行间距的倍数,通常情况下填 1 就好
    • spacingadd:行间距的额外增加值,通常情况下填 0 就好
    • includepad:是否在文字上下添加额外的空间,来避免某些过高的字符的绘制出现越界
    String text1 = "Lorem Ipsum is simply dummy text of the printing and typesetting industry.";
    StaticLayout staticLayout1 = new StaticLayout(text1, paint, 600, Layout.Alignment.ALIGN_NORMAL, 1, 0, true);
    String text2 = "a
    bc
    defghi
    jklm
    nopqrst
    uvwx
    yz";
    StaticLayout staticLayout2 = new StaticLayout(text2, paint, 600, Layout.Alignment.ALIGN_NORMAL, 1, 0, true);
    
    canvas.save();
    canvas.translate(50, 100); //对绘制的内容进行移
    staticLayout1.draw(canvas);
    
    canvas.translate(0, 200);
    staticLayout2.draw(canvas);
    canvas.restore();
    

    如果你需要进行多行文字的绘制,并且对文字的排列和样式没有太复杂的花式要求,那么使用 StaticLayout 就好。

    Paint 对文字绘制的辅助

    Paint 对文字绘制的辅助有两类方法:设置显示效果的和测量文字尺寸

    设置显示效果类

    常用的几个方法

    • setTextSize(float textSize) 设置文字大小
    • setTypeface(Typeface typeface) 设置字体
    • setFakeBoldText(boolean fakeBoldText) 是否使用伪粗体。之所以叫伪粗体,因为它并不是通过选用更高 weight 的字体让文字变粗,而是通过程序在运行时把文字给「描粗」了
    • setStrikeThruText(boolean strikeThruText) 是否加删除线
    • setUnderlineText(boolean underlineText) 是否加下划线
    • setTextSkewX(float skewX) 设置文字横向错切角度(文字倾斜度)
    • setTextScaleX(float scaleX) 设置文字横向放缩,也就是文字变胖变瘦
    • setLetterSpacing(float letterSpacing) 设置字符间距,默认值是 0。在默认的字符间距为 0 的情况下,字符和字符之间也没有紧紧贴着
    • setFontFeatureSettings(String settings) 用 CSS 的 font-feature-settings 的方式来设置文字
    paint.setTextSize(36);
    paint.setTypeface(Typeface.createFromAsset(getContext().getAssets(), "Satisfy-Regular.ttf"));
    paint.setTextSkewX(-0.5f);
    paint.setTextScaleX(0.8f);
    paint.setLetterSpacing(0.2f);
    paint.setFontFeatureSettings("smcp"); // 设置 "small caps"
    
    canvas.drawText("Hello HenCoder", 100, 150, paint);
    

    不常用的几个方法

    下面几个方法没啥用,不用管它:

    • setHinting(int mode) 字体微调
    • setElegantTextHeight(boolean elegant) 这个方法对中英文没用
    • setSubpixelText(boolean subpixelText) 是否开启次像素级的抗锯齿
    • setLinearText(boolean linearText) 不知道干嘛的

    setTextAlign 对齐方式

    对齐方式有三种:LEFT(默认值)、CETNERRIGHT

    paint.setTextAlign(Paint.Align.LEFT);
    canvas.drawText(text, 500, 150, paint);
    
    paint.setTextAlign(Paint.Align.CENTER);
    canvas.drawText(text, 500, 150 + textHeight, paint);
    
    paint.setTextAlign(Paint.Align.RIGHT);
    canvas.drawText(text, 500, 150 + textHeight * 2, paint);
    

    setTextLocale 地域

    Locale 直译是「地域」,其实就是你在系统里设置的「语言」或「语言区域」,比如「简体中文(中国)」「English (US)」「English (UK)」。

    有些同源的语言,在文化发展过程中对一些相同的字衍生出了不同的写法(比如中国大陆和日本对于某些汉字的写法就有细微差别。注意,不是繁体和简体这种同音同义不同字,而真的是同样的一个字有两种写法)。系统语言不同,同样的一个字的显示就有可能不同。

    Canvas 绘制的时候,默认使用的是系统设置里的 Locale。而通过此方法就可以在不改变系统设置的情况下,直接修改绘制时的 Locale

    paint.setTextLocale(Locale.CHINA); // 简体中文
    canvas.drawText(text, 150, 150, paint);
    
    paint.setTextLocale(Locale.TAIWAN); // 繁体中文
    canvas.drawText(text, 150, 150 + textHeight, paint);
    
    paint.setTextLocale(Locale.JAPAN); // 日语
    canvas.drawText(text, 150, 150 + textHeight * 2, paint);
    

    Android 7.0 ( API v24) 加入了多语言区域的支持,所以在 API v24 以及更高版本上,还可以使用 setTextLocales(LocaleList locales) 来为绘制设置多个语言区域。

    测量文字尺寸类

    不论是文字,还是图形或 Bitmap,只有知道了尺寸,才能更好地确定应该摆放的位置。由于文字的绘制和图形或 Bitmap 的绘制比起来,尺寸的计算复杂得多,所以它有一整套的方法来计算文字尺寸。

    getFontSpacing 获取行距

    即推荐的两行文字的 baseline 的距离。这个值是系统根据文字的字体和字号自动计算的。它的作用是当你要手动绘制多行文字(而不是使用 StaticLayout)的时候,可以在换行的时候给 y 坐标加上这个值来下移文字。

    canvas.drawText(texts[0], 100, 150, paint);
    canvas.drawText(texts[1], 100, 150 + paint.getFontSpacing, paint);
    canvas.drawText(texts[2], 100, 150 + paint.getFontSpacing * 2, paint);
    

    getFontMetrics 文字排印

    FontMetrics提供了几个文字排版印刷方面的数值:ascentdescenttopbottomleading

    ascentdescent 这两个值也可以通过 Paint.ascent()Paint.descent() 来快捷获取

    如图,图中有两行文字,每一行都有 6 条线:

    • baseline:上图中 黑色的线,它的作用是作为文字显示的基准线
    • ascent/descent:上图中 绿色橙色的线,它们的作用是限制普通字符的顶部和底部范围
      • 普通的字符(并非全部字符),上不会高过 ascent ,下不会低过 descent
      • ascent 的值是图中绿线和 baseline 的相对位移,它的值为负
      • descent 的值是图中橙线和 baseline 相对位移,值为正
    • top/bottom:上图中 蓝色红色的线,它们的作用是限制所有 glyph顶部和底部范围
      • 除了普通字符,有些字形的显示范围是会超过 ascentdescent 的,而 topbottom 则限制的是所有字形的显示范围,包括这些特殊字形
      • 例如上图的第二行文字里,就有两个泰文的字形分别超过了 ascentdescent 的限制,但它们都在 topbottom 两条线的范围内
      • top 的值是图中蓝线和 baseline 的相对位移,它的值为负
      • bottom 的值是图中红线和 baseline 相对位移,值为正
    • leading:这个值在上图中没有标记出来,因为它并不是指的某条线和 baseline 的相对位移
      • leading 指的是行的额外间距,即对于上下相邻的两行,上行的 bottom 线和下行的 top 线的距离
      • 也就是上图中 第一行的红线第二行的蓝线 的距离
      • leading 这个词的本意其是行距,即两个相邻行的 baseline 之间的距离。不过对于很多非专业领域,leading 的意思被改变了,被大家当做行的额外间距来用。

    它还有一个重载方法 getFontMetrics(FontMetrics fontMetrics) ,计算结果会直接填进传入的 FontMetrics 对象,而不是重新创建一个对象,这种用法在需要频繁获取 FontMetrics 的时候性能会好些。

    另外,这两个方法还有一对同样结构的对应的方法 getFontMetricsInt()getFontMetricsInt(FontMetricsInt fontMetrics) ,用于获取 FontMetricsInt 类型的结果。

    两种行间距的区别

    从定义可以看出,上图中两行文字的 font spacing (即相邻两行的 baseline 的距离) 可以通过 bottom(正值) + top(负值) + leading(正值)来计算得出,但你真的运行一下会发现,这个值的结果是要大于 getFontSpacing() 的返回值的。

    两个方法计算得出的 font spacing 竟然不一样?

    这并不是 bug,而是因为 getFontSpacing() 的结果并不是通过 FontMetrics 的标准值计算出来的,而是另外计算出来的一个值,它能够做到在两行文字不显得拥挤的前提下缩短行距,以此来得到更好的显示效果。所以如果你要对文字手动换行绘制,多数时候应该选取 getFontSpacing() 来得到行距,不但使用更简单,显示效果也会更好。

    getTextBounds 测量文字范围

    getTextBounds(String text, int start, int end, Rect bounds) //文字的起始和结束位置,测算结果存储位置
    getTextBounds(char[] text, int index, int count, Rect bounds)
    
    canvas.drawText(text, offsetX, offsetY, paint);
    paint.getTextBounds(text, 0, text.length(), bounds);
    bounds.left += offsetX;
    bounds.top += offsetY;
    bounds.right += offsetX;
    bounds.bottom += offsetY;
    paint.setStyle(Paint.Style.STROKE);
    canvas.drawRect(bounds, paint);
    

    measureText 测量文字宽度

    canvas.drawText(text, offsetX, offsetY, paint);
    float textWidth = paint.measureText(text);
    canvas.drawLine(offsetX, offsetY, offsetX + textWidth, offsetY, paint);
    

    两种测量方法的区别

    如果你用代码分别使用 getTextBounds()measureText() 来测量文字的宽度,你会发现 measureText() 测出来的宽度总是比 getTextBounds() 大一点点。这是因为这两个方法其实测量的是两个不一样的东西。

    getTextBounds测量的是文字的显示范围。形象点来说,你这段文字外放置一个可变的矩形,然后把矩形尽可能地缩小,一直小到这个矩形恰好紧紧包裹住文字,那么这个矩形的范围,就是这段文字的 bounds。

    measureText()测量的是文字绘制时所占用的宽度。前面已经讲过,一个文字在界面中,往往需要占用比他的实际显示宽度更多一点的宽度,以此来让文字和文字之间保留一些间距,不会显得过于拥挤。

    上面的这幅图,我并没有设置 setLetterSpacing() ,这里的 letter spacing 是默认值 0,但你可以看到,图中每两个字母之间都是有空隙的。另外,下方那条用于表示文字宽度的横线,在左边超出了第一个字母 H 一段距离的,在右边也超出了最后一个字母 r,而就是两边的这两个「超出」,导致了 measureText()getTextBounds() 测量出的宽度要大一些。

    在实际的开发中,测量宽度要用 measureText() 还是 getTextBounds() ,需要根据情况而定。不过你只要掌握了上面我所说的它们的本质,在选择的时候就不会为难和疑惑了。

    getTextWidths 获取字符宽度

    获取字符串中每个字符的宽度,并把结果填入参数 widths

    这相当于 measureText() 的一个快捷方法,它的计算等价于对字符串中的每个字符分别调用 measureText() ,并把它们的计算结果分别填入 widths 的不同元素。

    breakText 测量文字宽度

    这个方法也是用来测量文字宽度的,但和 measureText() 的区别是:breakText() 是在给出宽度上限的前提下测量文字的宽度,如果文字的宽度超出了上限,那么在临近超限的位置截断文字

    int breakText(String text, boolean measureForwards, float maxWidth, float[] measuredWidth)

    • 返回值:截取的文字个数,如果宽度没有超限,则是文字的总个数
    • text:要测量的文字
    • measureForwards:文字的测量方向,true 表示由左往右测量
    • maxWidth:给出的宽度上限
    • measuredWidth:方法测量完成后会把截取的文字宽度赋值给 measuredWidth[0]

    这个方法可以用于多行文字的折行计算

    int measuredCount;
    float[] measuredWidth = {0};
    
    // 宽度上限 300 (不够用,截断)
    measuredCount = paint.breakText(text, 0, text.length(), true, 300, measuredWidth);
    canvas.drawText(text, 0, measuredCount, 150, 150, paint);
    
    // 宽度上限 400 (不够用,截断)
    measuredCount = paint.breakText(text, 0, text.length(), true, 400, measuredWidth);
    canvas.drawText(text, 0, measuredCount, 150, 150 + fontSpacing, paint);
    
    // 宽度上限 500 (够用)
    measuredCount = paint.breakText(text, 0, text.length(), true, 500, measuredWidth);
    canvas.drawText(text, 0, measuredCount, 150, 150 + fontSpacing * 2, paint);
    
    // 宽度上限 600 (够用)
    measuredCount = paint.breakText(text, 0, text.length(), true, 600, measuredWidth);
    canvas.drawText(text, 0, measuredCount, 150, 150 + fontSpacing * 3, paint);
    

    两个光标相关的方法

    对于 EditText 以及类似的场景,会需要绘制光标。光标的计算很麻烦,不过 API 23 引入了两个新的方法,有了这两个方法后,计算光标就方便了很多。

    getRunAdvance(CharSequence text, int start, int end, int contextStart, int contextEnd, boolean isRtl, int offset)

    • 对于一段文字,计算出某个字符处光标的 x 坐标
    • start end 是文字的起始和结束坐标
    • contextStartcontextEnd 是上下文的起始和结束坐标
    • isRtl 是文字的方向
    • offset 是字数的偏移,即计算第几个字符处的光标

    getOffsetForAdvance(CharSequence text, int start, int end, int contextStart, int contextEnd, boolean isRtl, float advance)

    • 给出一个位置的像素值,计算出文字中最接近这个位置的字符偏移量(即第几个字符最接近这个坐标)
    • text 是要测量的文字
    • start end 是文字的起始和结束坐标
    • contextStart contextEnd 是上下文的起始和结束坐标
    • isRtl 是文字方向
    • advance 是给出的位置的像素值

    这两个方法一起使用,就可以实现「获取用户点击处的文字坐标」的需求。

    hasGlyph 单独的字形

    检查指定的字符串中是否是一个单独的字形 (glyph)。

    2021-4-24

  • 相关阅读:
    PHP (20140519)
    PHP (20140516)
    js(20140517)在JS方法中返回多个值的三种方法
    PHP (20140515)
    PHP (20140514)
    Java内网发送邮件
    每日一“酷”之Cookie
    每日一“酷”之Queue
    每日一“酷”之pprint
    每日一“酷”之copy
  • 原文地址:https://www.cnblogs.com/baiqiantao/p/14698662.html
Copyright © 2020-2023  润新知