Draw Text in Deep

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。

file

文本測量

文本的測量是非常復(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如圖所示。

file

如果是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è)例子,可以很清楚的了解這一原理。

file

文本的居中繪制

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)。

file

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群英傳

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容