Android Canvas drawText文字居中的一些事(圖解)
1.寫在前面
在實現(xiàn)自定義控件的過程中,常常會有繪制居中文字的需求,于是在網(wǎng)上搜了一些相關(guān)的博客,總是看的一臉懵逼,就想著自己分析一下,在此記錄下來,希望對大家能夠有所幫助。
2.繪制一段文本
首先把坐標原點移動到控件中心(默認坐標原點在屏幕左上角),這樣看起來比較直觀一些,然后繪制x、y軸,此時原點向上y為負,向下y為正,向左x為負,向右x為正,以(0,0)坐標開始繪制一段文本:
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
// 將坐標原點移到控件中心
canvas.translate(getWidth() / 2, getHeight() / 2);
// x軸
canvas.drawLine(-getWidth() / 2, 0, getWidth() / 2, 0, paint);
// y軸
canvas.drawLine(0, -getHeight() / 2, 0, getHeight() / 2, paint);
// 繪制文字
paint.setTextSize(sp2px(50));
canvas.drawText("YangLe", 0, 0, paint);
}
看下繪制的文本:

繪制文本
咦,為什么繪制的文本在第一象限,y坐標不是指定的0嗎,為什么文本沒有在x軸的上面或下面,而是穿過了x軸,帶著這些疑問繼續(xù)往下看:
首先看一個重要的類:
public static class FontMetrics {
/**
* The maximum distance above the baseline for the tallest glyph in
* the font at a given text size.
*/
public float top;
/**
* The recommended distance above the baseline for singled spaced text.
*/
public float ascent;
/**
* The recommended distance below the baseline for singled spaced text.
*/
public float descent;
/**
* The maximum distance below the baseline for the lowest glyph in
* the font at a given text size.
*/
public float bottom;
/**
* The recommended additional space to add between lines of text.
*/
public float leading;
}
FontMetrics類是Paint的一個內(nèi)部類,主要定義了繪制文本時的一些關(guān)鍵坐標位置,看下這些值都代表什么:

關(guān)鍵坐標
看圖說話:
- top:從基線(x軸)向上繪制區(qū)域的最高點,此值為負值
- ascent:單行文本,從基線(x軸)向上繪制的推薦最高點,此值為負值
- baseline:基線,此值為0
- descent:單行文本,從基線(x軸)向下繪制的推薦最低點,此值為正值
- bottom:從基線(x軸)向下繪制區(qū)域的最低點,此值為正值
- leading:推薦的額外行距,一般為0
下面再來看看drawText這個方法:
/**
* Draw the text, with origin at (x,y), using the specified paint. The origin is interpreted
* based on the Align setting in the paint.
*
* @param text The text to be drawn
* @param x The x-coordinate of the origin of the text being drawn
* @param y The y-coordinate of the baseline of the text being drawn
* @param paint The paint used for the text (e.g. color, size, style)
*/
public void drawText(@NonNull String text, float x, float y, @NonNull Paint paint) {
super.drawText(text, x, y, paint);
}
重點看下x、y參數(shù)的含義:
- x:繪制文本的起始x坐標
- y:繪制文本的baseline在y軸方向的位置
有點難理解,舉個栗子,上文中的x、y參數(shù)傳的是(0,0),此時的baseline正好是坐標系中x軸,就相當于從y軸開始向右繪制,以x軸作為文本的baseline進行繪制。
如果參數(shù)傳(0,10),此時繪制文本的baseline從x軸開始向下移動10px,也就是以y10作為文本的baseline進行繪制,y10就是繪制文本的baseline在y軸方向的位置。
注意:baseline是繪制文本的基線,相對于繪制文本區(qū)域來說,相當于x軸,向上為負(top、ascent),向下為正(descent、bottom),但是這個x軸并不是控件的x軸,切記切記!?。?br />
還記得我們在上文中提出的疑問嗎,這下可以解釋了:
為什么繪制的文本在第一象限?
因為我們把坐標原點移到了控件中心,文本的baseline正好為x軸,top、ascent值為負,所以繪制的文本在第一象限。
y坐標不是指定的0嗎,為什么文本沒有在x軸的上面或下面,而是穿過了x軸?
drawText方法默認x軸方向是從左到右繪制的,y軸方向是從baseline為基準繪制的,文中的baseline正好為x軸,以baseline為基準繪制文本向下還有一段距離,所以文本穿過了x軸。
3.繪制居中的文本
在上文中,我們學習了如何繪制一段文本,以及其中參數(shù)和坐標的含義,接下來進入正題,看下如何才能繪制居中的文本。
首先看一張圖,此時文本的baseline正好為x軸,如果想要文本居中顯示的話,就需要先計算文本的寬度和高度:
- 寬度:調(diào)用Paint的measureText方法就可以獲得文本的寬度
- 高度:文本的高度就是實際繪制區(qū)域的高度,可以用(fontMetrics.descent - fontMetrics.ascent)獲取,因為ascent為負數(shù),所以最終算出來的是兩者的和
現(xiàn)在有了寬度,把繪制文本的x坐標向左移動(寬度 / 2)就可以水平居中,但是垂直方向就不能這么干了,我們要將文本向下移動baseline到文本中心的距離,也就是(高度 / 2 - fontMetrics.descent),如下圖所示:

計算baseLineY
現(xiàn)在的公式為:
float baseLineY = (fontMetrics.descent - fontMetrics.ascent) / 2 - fontMetrics.descent; = -fontMetrics.ascent / 2 - fontMetrics.descent / 2; = -(fontMetrics.ascent + fontMetrics.descent) / 2; = Math.abs(fontMetrics.ascent + fontMetrics.descent) / 2;
Paint中也有獲取ascent和descent值的方法,所以公式最終為:
float baseLineY = Math.abs(paint.ascent() + paint.descent()) / 2;
注意:此公式是相對于坐標原點在控件中心來計算的,如果坐標原點在左上角,baseLineY需要加上控件高度的一半。
float baseLineY = height / 2 + Math.abs(paint.ascent() + paint.descent()) / 2;
看下代碼:
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
// 將坐標原點移到控件中心
canvas.translate(getWidth() / 2, getHeight() / 2);
// x軸
canvas.drawLine(-getWidth() / 2, 0, getWidth() / 2, 0, paint);
// y軸
canvas.drawLine(0, -getHeight() / 2, 0, getHeight() / 2, paint);
// 繪制居中文字
paint.setTextSize(sp2px(50));
paint.setColor(Color.GRAY);
// 文字寬
float textWidth = paint.measureText("YangLe'Blog");
// 文字baseline在y軸方向的位置
float baseLineY = Math.abs(paint.ascent() + paint.descent()) / 2;
canvas.drawText("YangLe'Blog", -textWidth / 2, baseLineY, paint);
}
看下居中了嗎:

繪制居中文本
大功告成!
4.繪制多行居中的文本
注意:drawText方法不支持繪制多行文本
4.1 方式一
使用支持自動換行的StaticLayout:
/**
* 繪制多行居中文本(方式1)
*
* @param canvas 畫布
*/
private void drawCenterMultiText1(Canvas canvas) {
String text = "ABC";
// 畫筆
TextPaint textPaint = new TextPaint();
textPaint.setAntiAlias(true);
textPaint.setColor(Color.GRAY);
// 設(shè)置寬度超過50dp時換行
StaticLayout staticLayout = new StaticLayout(text, textPaint, dp2px(50),
Layout.Alignment.ALIGN_CENTER, 1f, 0f, false);
canvas.save();
// StaticLayout默認從(0,0)點開始繪制
// 如果需要調(diào)整位置,只能在繪制之前移動Canvas的起始坐標
canvas.translate(-staticLayout.getWidth() / 2, -staticLayout.getHeight() / 2);
staticLayout.draw(canvas);
canvas.restore();
}
看下StaticLayout的構(gòu)造方法參數(shù)含義:
public StaticLayout(CharSequence source, TextPaint paint, int width, Alignment align,
float spacingmult, float spacingadd, boolean includepad) {
this(source, 0, source.length(), paint, width, align, spacingmult, spacingadd, includepad);
}
- source:需要分行的文本
- paint:畫筆對象
- width:layout的寬度,文本超出寬度時自動換行
- align:layout的對其方式
- spacingmult:相對行間距,相對字體大小,1f表示行間距為1倍的字體高度
- spacingadd:基礎(chǔ)行距偏移值,實際行間距等于(spacingmult + spacingadd)
- includepad:參數(shù)未知
看下效果:

StaticLayout
使用StaticLayout,每行設(shè)置的寬度是相同的,當需求為每行顯示不同長度的文本時,這種方式就不能使用了,別擔心,接著來看下第二種方式。
4.2 方式二
使用循環(huán)drawText的方式進行繪制,看圖說話:

計算baseLineY
現(xiàn)在需要繪制A、B、C三行文本,紅色A代表每行文本默認的繪制位置,綠色的線代表每行文本的baseline,x軸為紅色A的baseline,現(xiàn)在分為三種情況:
- 文本在x軸上方:紅色A的baseline向上移動a距離,總高度的/2 - 文本的top值(絕對值)
- 文本在x軸中間:紅色A的baseline向下移動b距離,計算公式請參考單行文本居中公式
- 文本在x軸下方:紅色A的baseline向下移動c距離,總高度的/2 - 文本的bottom值(絕對值)
看下代碼:
/**
* 繪制多行居中文本(方式2)
*
* @param canvas 畫布
*/
private void drawCenterMultiText2(Canvas canvas) {
String[] texts = {"A", "B", "C"};
Paint.FontMetrics fontMetrics = paint.getFontMetrics();
// top絕對值
float top = Math.abs(fontMetrics.top);
// ascent絕對值
float ascent = Math.abs(fontMetrics.ascent);
// descent,正值
float descent = fontMetrics.descent;
// bottom,正值
float bottom = fontMetrics.bottom;
// 行數(shù)
int textLines = texts.length;
// 文本高度
float textHeight = top + bottom;
// 文本總高度
float textTotalHeight = textHeight * textLines;
// 基數(shù)
float basePosition = (textLines - 1) / 2f;
for (int i = 0; i < textLines; i++) {
// 文本寬度
float textWidth = paint.measureText(texts[i]);
// 文本baseline在y軸方向的位置
float baselineY;
if (i < basePosition) {
// x軸上,值為負
// 總高度的/2 - 已繪制的文本高度 - 文本的top值(絕對值)
baselineY = -(textTotalHeight / 2 - textHeight * i - top);
} else if (i > basePosition) {
// x軸下,值為正
// 總高度的/2 - 未繪制的文本高度 - 文本的bottom值(絕對值)
baselineY = textTotalHeight / 2 - textHeight * (textLines - i - 1) - bottom;
} else {
// x軸中,值為正
// 計算公式請參考單行文本居中公式
baselineY = (ascent - descent) / 2;
}
canvas.drawText(texts[i], -textWidth / 2, baselineY, paint);
}
}
對照上圖再看代碼就很好理解了,覺得代碼中的公式還有可以優(yōu)化的地方,如果你有好的方法,可以留言告訴我哈。
再看下中文版的多行文本:

多行居中文本
5.TextAlign
Paint的TextAlign屬性決定了繪制文本相對于drawText方法中x參數(shù)的相對位置。
舉個栗子:
- Paint.Align.LEFT:默認屬性,x坐標為繪制文本的最左側(cè)坐標
- Paint.Align.CENTER:x坐標為繪制文本的水平中心坐標
- Paint.Align.RIGHT:x坐標為繪制文本的最右側(cè)坐標
看圖理解下:

Paint.Align.LEFT

Paint.Align.CENTER

Paint.Align.RIGHT
6.文本居中的公式
坐標原點在控件中心:
float baseLineY = Math.abs(paint.ascent() + paint.descent()) / 2;
坐標原點在控件左上角:
float baseLineY = height / 2 + Math.abs(paint.ascent() + paint.descent()) / 2;
7.寫在最后
源碼已經(jīng)上傳到GitHub上了,歡迎Fork,覺得還不錯就Start一下吧!
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。
相關(guān)文章
Android中的TimePickerView(時間選擇器)的用法詳解
這篇文章主要介紹了Android中的TimePickerView時間選擇器的用法,這是一個第三方從底部彈出來的日期選擇器,文中結(jié)合實例代碼給大家介紹的非常詳細,需要的朋友可以參考下2022-04-04
Android使用Intent隱式實現(xiàn)頁面跳轉(zhuǎn)
這篇文章主要為大家詳細介紹了Android使用Intent隱式來實現(xiàn)向上跳轉(zhuǎn),具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-08-08
Android關(guān)于Button背景或樣式失效問題解決方法
大家好,本篇文章主要講的是Android關(guān)于Button背景或樣式失效問題解決方法,感興趣的同學趕快來看一看吧,對你有幫助的話記得收藏一下2022-01-01
Android實現(xiàn)計時與倒計時的常用方法小結(jié)
這篇文章主要介紹了Android實現(xiàn)計時與倒計時的常用方法,總結(jié)并對比分析了幾種常用計時方法的特點,具有一定參考借鑒價值,需要的朋友可以參考下2015-10-10

