參考
drawText復(fù)雜但很重要;
四線格與基線

在canvas在利用drawText繪制文字時(shí),也是有規(guī)則的,這個(gè)規(guī)則就是基線!

基線就是4線格的第三條線,確定好基線的位置,文字的位置就好辦了
Canvas.drawText()
drawText()與基線
/**
* x: 繪制原點(diǎn)x坐標(biāo)
* y:繪制原點(diǎn)y坐標(biāo)
*/
public void drawText(String text, float x, float y, Paint paint)
示例代碼(在坐標(biāo)近原點(diǎn)處,畫文字)
val paint1 = Paint(Paint.ANTI_ALIAS_FLAG).apply {
strokeWidth = 2f
style = Paint.Style.STROKE
color = Color.RED
textSize = 38f
}
canvas.drawText("Android開發(fā)藝術(shù)探索", 0f, 10f, paint1)
實(shí)際效果為:

為什么會(huì)這樣?
但這里有兩個(gè)參數(shù)需要非常注意,表示原點(diǎn)坐標(biāo)的x和y,這里傳進(jìn)去的原點(diǎn)參數(shù)(x,y)不是所在繪制文字所在矩形的左上角的點(diǎn)。
如果要畫"harvic's blog"這幾個(gè)字,這個(gè)原點(diǎn)坐標(biāo)應(yīng)當(dāng)是下圖中綠色小點(diǎn)的位置,y所代表的是基線的位置!

這樣,就能明白上面的繪制為什么被擋住了,基線跟Y坐標(biāo)有關(guān)系;
指定基線位置
val paint1 = Paint(Paint.ANTI_ALIAS_FLAG).apply {
strokeWidth = 2f
style = Paint.Style.FILL
color = Color.RED
textSize = 48f
}
val baselineX = 0f
val baselineY = 200f
// 畫基線
canvas.drawLine(baselineX, baselineY, width.toFloat(), baselineY, paint1)
paint1.color = Color.GREEN
canvas.drawText("Android開發(fā)藝術(shù)探索,fffggg", baselineX, baselineY, paint1)

結(jié)論
- drawText是中的參數(shù)y是基線的位置;
- 一定要清楚的是,只要x坐標(biāo)、基線位置、文字大小確定以后,文字的位置就是確定的了;
paint.setTextAlign(Paint.Align.XXX)
設(shè)置文字對(duì)齊方向,在drawText()中,y表示基線baseline位置,那么x表示什么呢?
x代表所要繪制文字所在矩形的相對(duì)位置。相對(duì)位置就是指指定點(diǎn)(x,y)在在所要繪制矩形的位置;相對(duì)位置,只有左、中、右三種,由paint.setTextAlign(Paint.Align.XXX)指定;
相對(duì)位置是根據(jù)所要繪制文字所在矩形來計(jì)算的;
示例:
val stringText = "Android開發(fā)藝術(shù)探索,fffggg"
val paint1 = Paint(Paint.ANTI_ALIAS_FLAG).apply {
strokeWidth = 2f
textSize = 48f
}
// 平移到中間
canvas.translate(width / 4.toFloat(), height / 2.toFloat())
paint1.color = Color.GRAY
paint1.style = Paint.Style.STROKE
val rect = Rect()
paint1.getTextBounds(stringText, 0, stringText.length, rect)
// 畫文字所在的矩形
canvas.drawRect(0f, 0f, rect.width().toFloat(), rect.height().toFloat(), paint1)
val baseLineX = -width / 2.toFloat()
val baselineY = rect.height().toFloat()
// 畫基線
paint1.color = Color.RED
canvas.drawLine(baseLineX, baselineY, width.toFloat(), baselineY, paint1)
// 畫文字
paint1.color = Color.GREEN
paint1.style = Paint.Style.FILL
paint1.textAlign = Paint.Align.LEFT // CENTER,RIGHT
canvas.drawText(stringText, 0f, baselineY, paint1)
效果:
- 紅色為基線;
- 灰色框?yàn)閠ext所在rect;
- 注意綠色文字變化;



drawText的四線格與FontMetrics
Text的繪圖五線格
通過上面的例子,我們可以看到文字繪制所在的矩形,沒有完全包含文字(垂直方法,注意后面的g字母),這是因?yàn)橄到y(tǒng)在繪制Text時(shí),還是有其它線的;

除了基線,如上圖所示,另外還有四條線,共5條線:
- ascent: 系統(tǒng)建議的,繪制單個(gè)字符時(shí),字符應(yīng)當(dāng)?shù)淖罡吒叨人诰€
- descent:系統(tǒng)建議的,繪制單個(gè)字符時(shí),字符應(yīng)當(dāng)?shù)淖畹透叨人诰€
- top: 可繪制的最高高度所在線
- bottom: 可繪制的最低高度所在線
- baseline:基線
為什么需要這么多線?,來自原博客的解釋:
我們來看一下電視的顯示。用過視頻處理工具的同學(xué),在制作視頻時(shí),視頻顯示位置都會(huì)有一個(gè)安全區(qū)域框,如下圖所示:

如上圖所示,黑色部分表示電視屏幕,紅色框就表示安全區(qū)域框。
這個(gè)安全框是用來干嘛的?這個(gè)安全框就是系統(tǒng)推薦給我們的顯示區(qū)域,雖然說我們可以講電視屏幕是每個(gè)區(qū)域都是可以顯示圖像的,但由于制式的不同,每個(gè)國(guó)家的屏幕大小并不一定我們這里的屏幕大小一致,當(dāng)遇到不一致時(shí),就會(huì)裁剪。但系統(tǒng)給我們推薦的顯示區(qū)域是無論哪種制式都是可以完整顯示出來的,所以我們?cè)谥谱饕曨l時(shí),盡量要把要顯示的圖像放在所推薦的顯示區(qū)域內(nèi)。
同樣,在這里,我們?cè)诶L制文字時(shí),ascent是推薦的繪制文字的最高高度,就表示在繪制文字時(shí),盡力要在這個(gè)最高高度以下繪制文字。descent是推薦的繪制文字的最底高度線,同樣表示是在繪制文字時(shí)盡量在這個(gè)descent線以上來繪制文字。而top線則指該文字可以繪制的最高高度線,bottom則是表示該文字可以繪制的最低高度線。ascent,descent是系統(tǒng)建議上的繪制高度,而top,bottom則是物理上屏幕最高,最低可以畫的高度值。他們的差別與我們上面說的視頻處理的安全框和屏幕的道理是一樣的。
FontMetrics(內(nèi)容全部copy 自原博客)
系統(tǒng)在畫文字時(shí)的五條線,baseline、ascent、descent、top、bottom;
baseline的位置是在構(gòu)造drawText()時(shí)的參數(shù)y來決定的;
那ascent,descent,top,bottom這些線的位置要怎么計(jì)算出來呢?
Android給我們提供了一個(gè)類:FontMetrics,它里面有5個(gè)成員變量:
public static class FontMetrics {
public float top;
public float ascent;
public float descent;
public float bottom;
public float leading; // 特殊
}
計(jì)算方法分別為:
- ascent = ascent線的y坐標(biāo) - baseline線的y坐標(biāo);
- descent = descent線的y坐標(biāo) - baseline線的y坐標(biāo);
- top = top線的y坐標(biāo) - baseline線的y坐標(biāo);
- bottom = bottom線的y坐標(biāo) - baseline線的y坐標(biāo);
- leading: 行間距,即前一行的descent與下一行的ascent之間的距離;

上圖中,我們先說明兩點(diǎn),然后再回過頭來看上面的公式:
- X軸,Y軸的正方向走向是X軸向右是正方向,Y軸向下是正方向,所以越往下Y坐標(biāo)越大!
- 大家千萬不要將FontMetrics中的ascent,descent,top,bottom與現(xiàn)實(shí)中的ascent,descent,top,bottom所在線混淆!這幾條線是真實(shí)存在的,而FontMetrics中的ascent,descent,top,bottom這個(gè)變量的值就是用來計(jì)算這幾條線的位置的。
下面我們將看到如何利用這幾個(gè)變量來計(jì)算這幾條線的位置。
看完這個(gè)圖,我們?cè)僦匦抡f說這幾個(gè)公式,我們拿一個(gè)來說吧,其它都是相同的道理。
ascent = ascent線的y坐標(biāo) - baseline線的y坐標(biāo)
FontMetrics的這幾個(gè)變量的值都是以baseline為基準(zhǔn)的,對(duì)于ascent來說,baseline線在ascent線之下,所以必然baseline的y值要大于ascent線的y值,所以ascent變量的值是負(fù)的。
同理,對(duì)于descent而言:
descent = descent線的y坐標(biāo) - baseline線的y坐標(biāo)
descent線在baseline線之下,所以必然descent線的y坐標(biāo)要大于baseline線的y坐標(biāo),所以descent變量的值必然是正數(shù)。
得到Text四線格的各線位置
如何通過這些變量來得到對(duì)應(yīng)線所在位置吧。
我們先列出來一個(gè)公式:
ascent線Y坐標(biāo) = baseline線Y坐標(biāo) + fontMetric.ascent;
推算過程如下:
-
因?yàn)閍scent線的Y坐標(biāo)等于baseline線的Y坐標(biāo)減去從baseline線到ascent線的這段距離, 也就是:(|fontMetric.ascent|表示取絕對(duì)值)
- ascent線Y坐標(biāo) = baseline線Y坐標(biāo) - |fontMetric.ascent|;
-
又因?yàn)閒ontMetric.ascent是負(fù)值,所以:
- ascent線Y坐標(biāo) = baseline線Y坐標(biāo) - |fontMetric.ascent|;
- ascent線Y坐標(biāo) = baseline線Y坐標(biāo) - ( -fontMetric.ascent);
- ascent線Y坐標(biāo) = baseline線Y坐標(biāo) + fontMetric.ascent;
-
這就是整個(gè)推算過程,沒什么難度,同理可以得到:
- ascent線Y坐標(biāo) = baseline線的y坐標(biāo) + fontMetric.ascent;
- descent線Y坐標(biāo) = baseline線的y坐標(biāo) + fontMetric.descent;
- top線Y坐標(biāo) = baseline線的y坐標(biāo) + fontMetric.top;
- bottom線Y坐標(biāo) = baseline線的y坐標(biāo) + fontMetric.bottom;
獲取FontMetrics對(duì)象
獲取FontMetrics對(duì)象是根據(jù)paint對(duì)象來獲取的,有了她,我們5條線,就可以畫出來了;
val fontMetrics = paint1.fontMetrics // 通過paint對(duì)象獲取 fontMetrics對(duì)象
val fontMetricsInt = paint1.fontMetricsInt // int 類型
val paint1 = Paint(Paint.ANTI_ALIAS_FLAG).apply {
strokeWidth = 2f
textSize = 100f
setTextAlign(Paint.Align.LEFT)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val stringText = "Android開發(fā)藝術(shù)探索"
val baselineX = 0f
val baselineY = 200f
// 畫文字
canvas.drawText(stringText, baselineX, baselineY, paint1)
// 計(jì)算各線在位置
val fontMetrics = paint1.fontMetrics
val top = baselineY + fontMetrics.top
val ascent = baselineY + fontMetrics.ascent
val descent = baselineY + fontMetrics.descent
val bottom = baselineY + fontMetrics.bottom
// top
paint1.color = Color.BLUE
canvas.drawLine(baselineX, top, width.toFloat(), top, paint1)
// ascent
paint1.color = Color.GREEN
canvas.drawLine(baselineX, ascent, width.toFloat(), ascent, paint1)
//畫基線
paint1.color = Color.RED
canvas.drawLine(baselineX, baselineY, width.toFloat(), baselineY, paint1)
// descent
paint1.color = Color.BLACK
canvas.drawLine(baselineX, descent, width.toFloat(), descent, paint1)
// bottom
paint1.color = Color.MAGENTA
canvas.drawLine(baselineX, bottom, width.toFloat(), bottom, paint1)
}
效果如下(注意黑色的線(descent),不是那么明顯):

獲取繪制文字的寬、高
上面的例子中,我們通過paint.getTextBounds獲取過;
這里,如何獲取所繪制字符串所占區(qū)域的高度、寬度和僅包裹字符串的最小矩形呢?
字符串所占高度和寬度
高度的獲取
字符串所占高度很容易得到,直接用bottom線所在位置的Y坐標(biāo)減去top線所在位置的Y坐標(biāo)就是字符串所占的高度:
// 1.高度
Paint.FontMetricsInt fm = paint.getFontMetricsInt();
int top = baseLineY + fm.top;
int bottom = baseLineY + fm.bottom;
//所占高度
int height = bottom - top;
// 2.寬
int width = paint.measureText(String text);
最小矩形
要獲取最小矩形,我們上面有用到過getTextBounds:
val rect = Rect()
paint1.getTextBounds(stringText, 0, stringText.length, rect)

從上圖中,可以看到這個(gè)矩形的左上角位置為(1,-81),右下角的位置為(1044,21);左上角的Y坐標(biāo)是個(gè)負(fù)數(shù)?因?yàn)槲覀儾]有給getTextBounds()傳遞基線位置。那它就是以(0,0)為基線來得到這個(gè)最小矩形的!所以這個(gè)最小矩形的位置就是以(0,0)為基線的結(jié)果!
獲取最小矩形的實(shí)際位置

在上面這個(gè)圖中,我們將黑色矩形平行下移距離Y(黃色線依照的是基線的位置),那么平移后的左上角點(diǎn)的y坐標(biāo)就是 y2 = y1 + Y;
同樣的道理,由于paint.getTextBounds()得到最小矩形的基線是y = 0;那我們直接將這個(gè)矩形移動(dòng)baseline的距離就可以得到這個(gè)矩形實(shí)際應(yīng)當(dāng)在的位置了。
所以矩形應(yīng)當(dāng)所在實(shí)際位置的坐標(biāo)是:
Rect minRect = new Rect();
paint.getTextBounds(text,0,text.length(),minRect);
//最小矩形,實(shí)際top位置 , top 再加上 baselineY
int minTop = bounds.top + baselineY;
//最小矩形,實(shí)際bottom位置, bottom 再加上 baselineY
int minBottom = bounds.bottom + baselineY;
例子:
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
strokeWidth = 2f
textSize = 100f
setTextAlign(Paint.Align.LEFT)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val stringText = "Android g 開發(fā)藝術(shù)探索"
val baselineX = 0f
val baselineY = 200f
// == 1. 畫text所占的區(qū)域Rect
val fm = paint.fontMetricsInt
val top = baselineY + fm.top // 加 baselineY
val bottom = baselineY + fm.bottom
val width = paint.measureText(stringText).toInt()
val rect = RectF(baselineX, top, baselineX + width, bottom)
paint.color = Color.GREEN
canvas.drawRect(rect, paint)
// == 2.寫文字
paint.color = Color.BLACK
canvas.drawText(stringText, baselineX, baselineY, paint)
// == 3.畫文字對(duì)應(yīng)的最小矩形
val minRect = Rect()
paint.getTextBounds(stringText, 0, stringText.length, minRect)
minRect.top = baselineY.toInt() + minRect.top
minRect.bottom = baselineY.toInt() + minRect.bottom
paint.color = Color.RED
paint.style = Paint.Style.STROKE
canvas.drawRect(minRect, paint)
}

紅色為最小矩形(安全區(qū)域),外層綠色背景為text所在矩形區(qū)域,也就是上面提到的電視屏幕
給定左上頂點(diǎn),并繪制文字
假定給出所要繪制矩形的左上角頂點(diǎn)坐標(biāo)(left,top),然后畫出這個(gè)文字;
因?yàn)?code>drawText()中傳進(jìn)去的Y坐標(biāo)是基線的位置,所以我們就必須根據(jù)top的位置計(jì)算出baseline的位置;
公式:
上面得出:
top線Y坐標(biāo) = baseline線的y坐標(biāo) + fontMetric.top;
所以:
fontMetrics.top = top - baseline;
所以:
baseline = top - FontMetrics.top;
因?yàn)镕ontMetrics.top是可以得到的,又因?yàn)槲覀兊膖op坐標(biāo)是給定的,所以通過這個(gè)公式就能得到baseline的位置了。
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
strokeWidth = 2f
textSize = 38f
setTextAlign(Paint.Align.LEFT)
}
val stringText = "Android g 開發(fā)藝術(shù)探索"
val top = 200f
val baselineX = 0f
// == 畫top線
paint.color = Color.BLUE
canvas.drawLine(baselineX, top, width.toFloat(), top, paint)
// == 計(jì)算baseline,并畫出baseline
val fontMetrics = paint.fontMetrics
val baseLineY = top - fontMetrics.top
paint.color = Color.RED
canvas.drawLine(baselineX, baseLineY, width.toFloat(), baseLineY, paint)
// == 畫文字
paint.color = Color.GREEN
canvas.drawText(stringText, baselineX, baseLineY, paint)

給定中間線位置繪圖
即,將文字的垂直中心為中間線;如下圖:

如何根據(jù)center(中間線)來推導(dǎo)出baseline?
圖中center線正是在top線和bottom線的正中間。
為了方便推導(dǎo)公式,原作者另外標(biāo)了三個(gè)距離A,B,C;
很顯然,距離A和距離C是相等的,都等于文字所在矩形高度以的一半,即:
A = C = (bottom - top)/2;
因?yàn)椋?br>
bottom = baseline + FontMetrics.bottom;
top = baseline+FontMetrics.top;
所以,將它們兩個(gè)代入上面的公式,就可得到:
A = C = (FontMetrics.bottom - FontMetrics.top)/2;
而距離B,則表示Center線到baseline的距離;
很顯然距離:
B = C - (bottom - baseline);
又因?yàn)?
FontMetrics.bottom = bottom-baseline;
C = A;
所以:
B = A - FontMetrics.bottom;
所以:
baseline = center + B = center + A - FontMetrics.bottom = center + (FontMetrics.bottom - FontMetrics.top)/2 - FontMetrics.bottom;
根據(jù)上面的推導(dǎo)過程,我們最終可知,當(dāng)給定中間線center位置以后,baseline的位置為:
baseline = center + (FontMetrics.bottom - FontMetrics.top)/2 - FontMetrics.bottom;
val stringText = "Android g 開發(fā)藝術(shù)探索"
val center = 200f
val baselineX = 0f
// == 畫center線
paint.color = Color.BLUE
canvas.drawLine(baselineX, center, width.toFloat(), center, paint)
// == 計(jì)算baseline,并畫出baseline
val fontMetrics = paint.fontMetrics
val baseLineY = center + (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.bottom
paint.color = Color.RED
canvas.drawLine(baselineX, baseLineY, width.toFloat(), baseLineY, paint)
// == 畫文字
paint.color = Color.BLACK
canvas.drawText(stringText, baselineX, baseLineY, paint)
