Android系統(tǒng)提供了Textview來提供文字的顯示,但很多時(shí)候開發(fā)者還需要使用Canvas來繪制Text,這時(shí)候,canvas.drawText()就不像Textview的使用這么簡單了,需要掌握文字的測量以及渲染的流程。
Paint.FontMetrics
FontMetrics是文字測量的重要方法,它提供了下面這些變量,來展示文字測量的相關(guān)參數(shù):
- baseline:字符繪制基線
- ascent:字符最高點(diǎn)到baseline的距離
- top:字符最高點(diǎn)到baseline的最大距離
- descent:字符最低點(diǎn)到baseline的距離
- bottom:字符最低點(diǎn)到baseline的最大距離
- leading:行間距,即前一行的descent與下一行的ascent之間的距離,單行則為0(注意不是行距)
要注意的是,這些參數(shù)都是以baseline為基準(zhǔn),所以在baseline之上的參數(shù)均為負(fù)值,baseline之下的參數(shù)才為正值,且這些值是距離,而非坐標(biāo)?;蛘呖梢岳斫鉃閎aseline.y = 0的時(shí)候的坐標(biāo)值。
top要大于ascent,原因是需要為拉丁語等帶符號(hào)的語言留出位置
由這些參數(shù),可以定義下面的這些與渲染有關(guān)的參數(shù)。
-
字體的高度
可以通過descent + Math.abs(ascent)計(jì)算得到。
-
行間距(leading)
TextView的行間距調(diào)整設(shè)置是通過setLineSpacing(add, mult)方法,在xml中,可以通過lineSpacingExtra和lineSpacingMultiplier來設(shè)置,在Paint自定義繪制Text中,可以使用Paint.fontMetrics中的leading屬性設(shè)置
-
行高
即字符所在行的高度 = ascent + descent + leading,即字符的高度 + 行間距,可以通過descent+Math.abs(ascent) + leading得到。如果在TextView中,可以直接通過getLineHeight()方法獲取。
-
字符間距(kerning)
對(duì)于textView和Paint繪制的Text,可以分別使用各自類中的getLetterSpacing()和setLetterSpacing()方法獲取和設(shè)置字符間距,對(duì)于TextView還可以在布局文件中使用屬性letterSpacing進(jìn)行定義。(注意以上的方法和屬性是在API 21引入的,對(duì)于之前的版本,只能通過SpannableString類及相應(yīng)的方法來間接調(diào)整。)
通過下面這張圖,大家可以非常清楚的了解FontMetrics。

文本測量
文本的測量是非常復(fù)雜,因?yàn)橐m配全球幾百種語言不同的排版,除了前面提到的FontMetrics,Android的渲染API還提供了很多測量文本的API。
getFontSpacing()
這個(gè)API用于獲取推薦的行距。即兩行文字間的baseline的距離。
這個(gè)值是系統(tǒng)根據(jù)文本的字體和字號(hào)自動(dòng)計(jì)算的。當(dāng)你使用drawText一行行繪制文字的時(shí)候,可以在換行的時(shí)候獲取下一行的baseline坐標(biāo)。
如果使用StaticLayout進(jìn)行多行文本的繪制,則不需要通過這個(gè)API來獲取行距
這里有一點(diǎn)需要注意的是,getFontSpacing所獲取的行距,與FontMetrics獲取的bottom + abs(top) + leading行距是不一樣的,這主要是因?yàn)檫@兩個(gè)API的計(jì)算方式不同,系統(tǒng)推薦使用getFontSpacing來獲取多行文本繪制時(shí)的行距。
getTextBounds()
獲取文字的實(shí)際顯示范圍。這個(gè)API返回的是當(dāng)前繪制文字的最小矩形,即能完全包裹文字的矩形范圍。
measureText()
與getTextBounds不同,measureText返回的是文字的實(shí)際占用位置,即理論上文字應(yīng)該占用的區(qū)域。
getTextWidths()
這個(gè)API返回的數(shù)組中,包含了每個(gè)字符的實(shí)際寬度,在排版中,這個(gè)寬度也叫“advance width”。它們累加的和,即為measureText返回的長度。
如果所選字體為等寬字體,則每個(gè)字符的寬度是相同的,如果非等寬字體,則不同字符的寬度是不同的。
文字渲染Layout
在Android中,文字渲染的基類是Layout類,它包含了文字測量、渲染和布局的所有功能,Layout類有幾個(gè)子類:
- BoringLayout
- StaticLayout
- DynamicLayout
一般來說,如果待渲染文本是屬于Spannable的文本對(duì)象,則使用動(dòng)態(tài)布局DynamicLayout,否則,使用isBoring判斷是不是單純的單行布局,如果是則使用BoringLayout,其他情況使用StaticLayout。
BoringLayout用于繪制僅一行文本的場景,它比較重要的地方是,它提供了一個(gè)靜態(tài)方法isBoring來判斷一段文字是否能在一行放下,這對(duì)于布局渲染是非常有幫助的。
/**
* 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如圖所示。

如果是API26+,可以使用新的API構(gòu)造StaticLayout,代碼如下所示。
// API 26+
val staticLayout = StaticLayout.Builder
.obtain(text, start, end, textPaint, width)
.build()
通過StaticLayout.Builder可以設(shè)置一些API26+的額外參數(shù),例如alignment、textDirection、lineSpacing、justificationMode等,其中justificationMode用于多行文本的兩邊對(duì)齊顯示。
關(guān)于StaticLayout這里有一篇比較好的文章推薦給大家。
https://medium.com/over-engineering/drawing-multiline-text-to-canvas-on-android-9b98f0bfa16a
TextPaint與Paint
TextPaint是Paint的子類,與Paint的使用基本一致,但大多用于StaticLayout或者是用于測量計(jì)算時(shí)使用。
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設(shè)置的是文本的對(duì)齊方式,一共有三種,LEFT、CETNER和RIGHT,默認(rèn)值為LEFT,它的作用是在繪制的時(shí)候確定繪制的方向,例如設(shè)置為LEFT,那么文本繪制的時(shí)候,就是從baseline的StartX開始向右繪制文本,如果是CENTER,那么就是從StartX開始,向兩邊開始繪制文字,同理,RIGHT為StartX向左開始繪制文本,這里要注意的是,TextAlign確定的是方向,而非在顯示區(qū)域內(nèi)的對(duì)齊方式,它的一個(gè)作用是幫助開發(fā)者進(jìn)行居中的繪制,例如設(shè)置Paint的TextAlign為CENTER,drawText的時(shí)候起點(diǎn)x = canvas.getWidth() / 2即可。文本會(huì)根據(jù)基準(zhǔn)線的中點(diǎn)開始向左右開始繪制文字,最終自然就變成了居中顯示了。如果你設(shè)定了RIGHT,那么從baseline的StartX的右邊開始繪制。
通過下面這個(gè)例子,可以很清楚的了解這一原理。

文本的居中繪制
Android中文本的繪制都是使用baseline進(jìn)行定位的,通過fontMetrics和已知的區(qū)域坐標(biāo),是可以推算出文字的其它關(guān)鍵坐標(biāo)的,所以,文本在任意區(qū)域的任意位置繪制問題,其實(shí)就是一個(gè)坐標(biāo)運(yùn)算的問題,根據(jù)已知變量和fontMetrics的相關(guān)參數(shù),來計(jì)算baseline的距離,下面就是文本垂直居中的推算過程。
文本的descent:
descentY = baselineY + fontMetrics.descent;
文本的字體高度:
fontHeight = fontMetrics.descent- fontMetrics.ascent
當(dāng)文本垂直居中時(shí)的bottom距離應(yīng)該為:
descentY=1/2 * height + 1/2 * fontHeight
baselineY = 1/2 * height - 1/2 * ( fontMetrics.ascent + fontMetrics.descent )
此時(shí)求得baseline的值,即cavans.drawText()里的y的坐標(biāo)。

breakText
這個(gè)API與BoringLayout中的isBoring方法有些類似,主要是對(duì)文中進(jìn)行一行的測量。
breakText (CharSequence text, int start, int end, boolean measureForwards, float maxWidth, float[] measuredWidth)
這個(gè)方法讓我們可以設(shè)置一個(gè)最大寬度,在不超過這個(gè)寬度的范圍內(nèi)返回實(shí)際測量值,text表示我們的文本字符串,start表示測量字符串的開始位置,end表示測量字符串的結(jié)束位置,measureForwards表示測量的方向,maxWidth表示一個(gè)給定的最大寬度在這個(gè)寬度內(nèi)能測量出幾個(gè)字符,measuredWidth為一個(gè)可選項(xiàng),不為空時(shí)返回真實(shí)的測量值。
類似的方法還有breakText (String text, boolean measureForwards, float maxWidth, float[] measuredWidth)和breakText (char[] text, int index, int count, float maxWidth, float[] measuredWidth)。
這個(gè)方法在一些自定義文本繪制的場景下比較常用,例如閱讀類APP的文字排版,需要在換行的時(shí)候動(dòng)態(tài)折斷或生成一行新的字符串。
基本使用方式如下所示。
measuredCount = paint.breakText(text, 0, text.length(), true, showWidth, measuredWidth);
canvas.drawText(text, 0, measuredCount, paint);
通過上面的方法,就得到了當(dāng)前這一行可以容納text文本中的多少個(gè)字符,如果showWidth不夠展示全部的字符,text文本則會(huì)被截?cái)?,measuredCount就是該截?cái)嗟奈恢谩?/p>
其它
canvas中還有很多其它關(guān)于繪制文本的API,都是樣式上的參數(shù),這里不詳細(xì)解釋,例如:
- textScaleX
- letterSpacing(API 21+)
- textSkewX
這些都是一些設(shè)置文本樣式的API,大家自己在Demo中設(shè)置下就知道樣式了。
整個(gè)文章的演示Demo上傳到GitHub了,大家可以自己在手機(jī)上測試下,加深對(duì)文本渲染的了解,地址如下所示。
https://github.com/xuyisheng/TextMatrix
歡迎大家關(guān)注我的微信公眾號(hào)——Android群英傳