Draw Text in Deep

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。

file

文本测量

文本的测量是非常复杂,因为要适配全球几百种语言不同的排版,除了前面提到的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来判断一段文字是否能在一行放下,这对于布局渲染是非常有帮助的。

/**
 * Returns null if not boring; the width, ascent, and descent if boring.
 */
val boring = BoringLayout.isBoring(drawText, textPaint)

StaticLayout

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

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

StaticLayout的基本使用如下所示。

val spannable = SpannableString(drawText)
spannable.setSpan(RelativeSizeSpan(2f), 0, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
val staticLayout = StaticLayout(
    spannable, textPaint, width, Layout.Alignment.ALIGN_NORMAL,
    1F, 0F, true
)
val width = staticLayout.getLineWidth(0)
val height = staticLayout.height
Log.d("xys", "line width $width height $height")
staticLayout.draw(canvas)

Demo如图所示。

file

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

// API 26 
val staticLayout = StaticLayout.Builder
        .obtain(text, start, end, textPaint, width)
        .build()

通过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的示例代码如下所示。

String text = "This is some text."

TextPaint myTextPaint = new TextPaint();
mTextPaint.setAntiAlias(true);
mTextPaint.setTextSize(16 * getResources().getDisplayMetrics().density);
mTextPaint.setColor(0xFF000000);

float width = mTextPaint.measureText(text);
float height = -mTextPaint.ascent()   mTextPaint.descent();

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的右边开始绘制。

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

file

文本的居中绘制

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的坐标。

file

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的文字排版,需要在换行的时候动态折断或生成一行新的字符串。

基本使用方式如下所示。

measuredCount = paint.breakText(text, 0, text.length(), true, showWidth, measuredWidth);
canvas.drawText(text, 0, measuredCount, paint);

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

其它

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

  • textScaleX
  • letterSpacing(API 21 )
  • textSkewX

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

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

https://github.com/xuyisheng/TextMatrix欢迎关注我的微信公众号——Android群英传

要在QPainter中实现自动换行功能,可以使用QFontMetrics类来计算文本的宽度和高度,然后根据所需的宽度和高度进行换行。以下是一个简单的示例代码: ```python from PyQt5.QtGui import QPainter, QFontMetrics from PyQt5.QtCore import Qt # ... def paintEvent(self, event): painter = QPainter(self) font = painter.font() font.setPointSize(14) painter.setFont(font) text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla facilisi. Sed eget mi nec mi porta dictum. Aliquam erat volutpat. Donec eleifend nulla eget nulla malesuada convallis. Sed quis diam eget dolor auctor aliquam ut eget eros. Sed eget neque porttitor, cursus nulla facilisis, gravida lectus. Sed vel pharetra purus. Sed in sollicitudin mi, id interdum velit. Vivamus gravida dapibus dolor, nec facilisis lacus imperdiet eget. Maecenas sed turpis sed velit iaculis bibendum. Duis feugiat augue in sodales pulvinar. In non mi euismod, laoreet nibh sit amet, vestibulum sapien. Fusce ac tristique turpis. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Sed in nulla id neque consequat ultricies. Nulla facilisi." rect = self.geometry() top_margin = 20 bottom_margin = 20 left_margin = 20 right_margin = 20 fm = QFontMetrics(font) y = top_margin for line in text.split('\n'): x = left_margin for word in line.split(): word_width = fm.width(word) if x + word_width > rect.width() - right_margin: x = left_margin y += fm.height() painter.drawText(x, y, word) x += word_width + fm.width(' ') y += fm.height() painter.end() ``` 在此示例中,我们首先设置了字体大小为14,并将其应用于绘图器。然后,我们定义了要绘制的多行文本。接下来,我们定义了绘制区域的边距,并使用QFontMetrics类计算每个单词的宽度和高度。最后,我们使用嵌套的循环来逐个单词地绘制文本,并在达到绘制区域的右侧时换行。 请注意,此示例仅处理空格分隔的单词,而不考虑其他标点符号。要处理其他标点符号,需要对代码进行修改。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值