3

Draw Text in Deep

 3 years ago
source link: https://blog.csdn.net/eclipsexys/article/details/103470950
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Android系统提供了Textview来提供文字的显示,但很多时候开发者还需要使用Canvas来绘制Text,这时候,canvas.drawText()就不像Textview的使用这么简单了,需要掌握文字的测量以及渲染的流程。

Paint.FontMetrics

FontMetrics是文字测量的重要方法,它提供了下面这些变量,来展示文字测量的相关参数:

  • baseline:字符绘制基线

  • ascent:字符最高点到baseline的距离

  • top:字符最高点到baseline的最大距离

  • descent:字符最低点到baseline的距离

  • bottom:字符最低点到baseline的最大距离

  • leading:行间距,即前一行的descent与下一行的ascent之间的距离,单行则为0(注意不是行距)

要注意的是,这些参数都是以baseline为基准,所以在baseline之上的参数均为负值,baseline之下的参数才为正值,且这些值是距离,而非坐标。或者可以理解为baseline.y = 0的时候的坐标值。

top要大于ascent,原因是需要为拉丁语等带符号的语言留出位置

由这些参数,可以定义下面的这些与渲染有关的参数。

  • 字体的高度

    可以通过descent + Math.abs(ascent)计算得到。

  • 行间距(leading)

    TextView的行间距调整设置是通过setLineSpacing(add, mult)方法,在xml中,可以通过lineSpacingExtra和lineSpacingMultiplier来设置,在Paint自定义绘制Text中,可以使用Paint.fontMetrics中的leading属性设置

  • 即字符所在行的高度 = ascent + descent + leading,即字符的高度 + 行间距,可以通过descent+Math.abs(ascent) + leading得到。如果在TextView中,可以直接通过getLineHeight()方法获取。

  • 字符间距(kerning)

    对于textView和Paint绘制的Text,可以分别使用各自类中的getLetterSpacing()和setLetterSpacing()方法获取和设置字符间距,对于TextView还可以在布局文件中使用属性letterSpacing进行定义。(注意以上的方法和属性是在API 21引入的,对于之前的版本,只能通过SpannableString类及相应的方法来间接调整。)

通过下面这张图,大家可以非常清楚的了解FontMetrics。

format,png

文本的测量是非常复杂,因为要适配全球几百种语言不同的排版,除了前面提到的FontMetrics,Android的渲染API还提供了很多测量文本的API。

getFontSpacing()

这个API用于获取推荐的行距。即两行文字间的baseline的距离。

这个值是系统根据文本的字体和字号自动计算的。当你使用drawText一行行绘制文字的时候,可以在换行的时候获取下一行的baseline坐标。

如果使用StaticLayout进行多行文本的绘制,则不需要通过这个API来获取行距

这里有一点需要注意的是,getFontSpacing所获取的行距,与FontMetrics获取的bottom + abs(top) + leading行距是不一样的,这主要是因为这两个API的计算方式不同,系统推荐使用getFontSpacing来获取多行文本绘制时的行距。

getTextBounds()

获取文字的实际显示范围。这个API返回的是当前绘制文字的最小矩形,即能完全包裹文字的矩形范围。

measureText()

与getTextBounds不同,measureText返回的是文字的实际占用位置,即理论上文字应该占用的区域。

getTextWidths()

这个API返回的数组中,包含了每个字符的实际宽度,在排版中,这个宽度也叫“advance width”。它们累加的和,即为measureText返回的长度。

如果所选字体为等宽字体,则每个字符的宽度是相同的,如果非等宽字体,则不同字符的宽度是不同的。

文字渲染Layout

在Android中,文字渲染的基类是Layout类,它包含了文字测量、渲染和布局的所有功能,Layout类有几个子类:

  • BoringLayout

  • StaticLayout

  • DynamicLayout

一般来说,如果待渲染文本是属于Spannable的文本对象,则使用动态布局DynamicLayout,否则,使用isBoring判断是不是单纯的单行布局,如果是则使用BoringLayout,其他情况使用StaticLayout。

BoringLayout用于绘制仅一行文本的场景,它比较重要的地方是,它提供了一个静态方法isBoring来判断一段文字是否能在一行放下,这对于布局渲染是非常有帮助的。

StaticLayout

StaticLayout的使用场景为多行文本的渲染和SpannableString的渲染。

SpannableString是不能通过Paint.getTextBounds或者是Paint.measureText来测量的

StaticLayout的基本使用如下所示。

Demo如图所示。

format,png

如果是API26+,可以使用新的API构造StaticLayout,代码如下所示。

通过StaticLayout.Builder可以设置一些API26+的额外参数,例如alignment、textDirection、lineSpacing、justificationMode等,其中justificationMode用于多行文本的两边对齐显示。

关于StaticLayout这里有一篇比较好的文章推荐给大家。

https://medium.com/over-engineering/drawing-multiline-text-to-canvas-on-android-9b98f0bfa16a

TextPaint与Paint

TextPaint是Paint的子类,与Paint的使用基本一致,但大多用于StaticLayout或者是用于测量计算时使用。

TextPaint的示例代码如下所示。

TextAlign

TextAlign设置的是文本的对齐方式,一共有三种,LEFT、CETNER和RIGHT,默认值为LEFT,它的作用是在绘制的时候确定绘制的方向,例如设置为LEFT,那么文本绘制的时候,就是从baseline的StartX开始向右绘制文本,如果是CENTER,那么就是从StartX开始,向两边开始绘制文字,同理,RIGHT为StartX向左开始绘制文本,这里要注意的是,TextAlign确定的是方向,而非在显示区域内的对齐方式,它的一个作用是帮助开发者进行居中的绘制,例如设置Paint的TextAlign为CENTER,drawText的时候起点x = canvas.getWidth() / 2即可。文本会根据基准线的中点开始向左右开始绘制文字,最终自然就变成了居中显示了。如果你设定了RIGHT,那么从baseline的StartX的右边开始绘制。

通过下面这个例子,可以很清楚的了解这一原理。

format,png

文本的居中绘制

Android中文本的绘制都是使用baseline进行定位的,通过fontMetrics和已知的区域坐标,是可以推算出文字的其它关键坐标的,所以,文本在任意区域的任意位置绘制问题,其实就是一个坐标运算的问题,根据已知变量和fontMetrics的相关参数,来计算baseline的距离,下面就是文本垂直居中的推算过程。

文本的descent:descentY = baselineY + fontMetrics.descent; 文本的字体高度:fontHeight = fontMetrics.descent- fontMetrics.ascent 当文本垂直居中时的bottom距离应该为:descentY=1/2 height + 1/2 fontHeight

baselineY = 1/2 height - 1/2 ( fontMetrics.ascent + fontMetrics.descent ) 此时求得baseline的值,即cavans.drawText()里的y的坐标。

format,png

breakText

这个API与BoringLayout中的isBoring方法有些类似,主要是对文中进行一行的测量。

breakText (CharSequence text, int start, int end, boolean measureForwards, float maxWidth, float[] measuredWidth) 这个方法让我们可以设置一个最大宽度,在不超过这个宽度的范围内返回实际测量值,text表示我们的文本字符串,start表示测量字符串的开始位置,end表示测量字符串的结束位置,measureForwards表示测量的方向,maxWidth表示一个给定的最大宽度在这个宽度内能测量出几个字符,measuredWidth为一个可选项,不为空时返回真实的测量值。类似的方法还有breakText (String text, boolean measureForwards, float maxWidth, float[] measuredWidth)和breakText (char[] text, int index, int count, float maxWidth, float[] measuredWidth)。这个方法在一些自定义文本绘制的场景下比较常用,例如阅读类APP的文字排版,需要在换行的时候动态折断或生成一行新的字符串。

基本使用方式如下所示。

通过上面的方法,就得到了当前这一行可以容纳text文本中的多少个字符,如果showWidth不够展示全部的字符,text文本则会被截断,measuredCount就是该截断的位置。

canvas中还有很多其它关于绘制文本的API,都是样式上的参数,这里不详细解释,例如:

  • textScaleX

  • letterSpacing(API 21+)

  • textSkewX

这些都是一些设置文本样式的API,大家自己在Demo中设置下就知道样式了。

整个文章的演示Demo上传到GitHub了,大家可以自己在手机上测试下,加深对文本渲染的了解,地址如下所示。

https://github.com/xuyisheng/TextMatrix


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK