GitHub傳送門git
在實現自定義控件的過程當中,經常會有繪製居中文字的需求,因而在網上搜了一些相關的博客,老是看的一臉懵逼,就想着本身分析一下,在此記錄下來,但願對你們可以有所幫助。github
首先把座標原點移動到控件中心(默認座標原點在屏幕左上角),這樣看起來比較直觀一些,而後繪製x、y軸,此時原點向上y爲負,向下y爲正,向左x爲負,向右x爲正,以(0,0)座標開始繪製一段文本:canvas
@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); } 複製代碼
看下繪製的文本:bash
咦,爲何繪製的文本在第一象限,y座標不是指定的0嗎,爲何文本沒有在x軸的上面或下面,而是穿過了x軸,帶着這些疑問繼續往下看:markdown
首先看一個重要的類:ide
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的一個內部類,主要定義了繪製文本時的一些關鍵座標位置,看下這些值都表明什麼:oop
看圖說話:學習
top:從基線(x軸)向上繪製區域的最高點,此值爲負值字體
ascent:單行文本,從基線(x軸)向上繪製的推薦最高點,此值爲負值優化
baseline:基線,此值爲0
descent:單行文本,從基線(x軸)向下繪製的推薦最低點,此值爲正值
bottom:從基線(x軸)向下繪製區域的最低點,此值爲正值
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參數的含義:
x:繪製文本的起始x座標
y:繪製文本的baseline在y軸方向的位置
有點難理解,舉個栗子,上文中的x、y參數傳的是(0,0),此時的baseline正好是座標系中x軸,就至關於從y軸開始向右繪製,以x軸做爲文本的baseline進行繪製。
若是參數傳(0,10),此時繪製文本的baseline從x軸開始向下移動10px,也就是以y10做爲文本的baseline進行繪製,y10就是繪製文本的baseline在y軸方向的位置。
注意:baseline是繪製文本的基線,相對於繪製文本區域來講,至關於x軸,向上爲負(top、ascent),向下爲正(descent、bottom),可是這個x軸並非控件的x軸,切記切記!!!
還記得咱們在上文中提出的疑問嗎,這下能夠解釋了:
爲何繪製的文本在第一象限?
由於咱們把座標原點移到了控件中心,文本的baseline正好爲x軸,top、ascent值爲負,因此繪製的文本在第一象限。
y座標不是指定的0嗎,爲何文本沒有在x軸的上面或下面,而是穿過了x軸?
drawText方法默認x軸方向是從左到右繪製的,y軸方向是從baseline爲基準繪製的,文中的baseline正好爲x軸,以baseline爲基準繪製文本向下還有一段距離,因此文本穿過了x軸。
在上文中,咱們學習瞭如何繪製一段文本,以及其中參數和座標的含義,接下來進入正題,看下如何才能繪製居中的文本。
首先看一張圖,此時文本的baseline正好爲x軸,若是想要文本居中顯示的話,就須要先計算文本的寬度和高度:
寬度:調用Paint的measureText方法就能夠得到文本的寬度
高度:文本的高度就是實際繪製區域的高度,能夠用(fontMetrics.descent - fontMetrics.ascent)獲取,由於ascent爲負數,因此最終算出來的是二者的和
如今有了寬度,把繪製文本的x座標向左移動(寬度 / 2)就能夠水平居中,可是垂直方向就不能這麼幹了,咱們要將文本向下移動baseline到文本中心的距離,也就是(高度 / 2 - fontMetrics.descent),以下圖所示:
如今的公式爲:
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); } 複製代碼
看下居中了嗎:
大功告成!
注意:drawText方法不支持繪製多行文本
使用支持自動換行的StaticLayout:
/** * 繪製多行居中文本(方式1) * * @param canvas 畫布 */ private void drawCenterMultiText1(Canvas canvas) { String text = "ABC"; // 畫筆 TextPaint textPaint = new TextPaint(); textPaint.setAntiAlias(true); textPaint.setColor(Color.GRAY); // 設置寬度超過50dp時換行 StaticLayout staticLayout = new StaticLayout(text, textPaint, dp2px(50), Layout.Alignment.ALIGN_CENTER, 1f, 0f, false); canvas.save(); // StaticLayout默認從(0,0)點開始繪製 // 若是須要調整位置,只能在繪製以前移動Canvas的起始座標 canvas.translate(-staticLayout.getWidth() / 2, -staticLayout.getHeight() / 2); staticLayout.draw(canvas); canvas.restore(); } 複製代碼
看下StaticLayout的構造方法參數含義:
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:基礎行距偏移值,實際行間距等於(spacingmult + spacingadd)
includepad:參數未知
看下效果:
使用StaticLayout,每行設置的寬度是相同的,當需求爲每行顯示不一樣長度的文本時,這種方式就不能使用了,別擔憂,接着來看下第二種方式。
使用循環drawText的方式進行繪製,看圖說話:
如今須要繪製A、B、C三行文本,紅色A表明每行文本默認的繪製位置,綠色的線表明每行文本的baseline,x軸爲紅色A的baseline,如今分爲三種狀況:
文本在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; // 行數 int textLines = texts.length; // 文本高度 float textHeight = top + bottom; // 文本總高度 float textTotalHeight = textHeight * textLines; // 基數 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); } } 複製代碼
對照上圖再看代碼就很好理解了,以爲代碼中的公式還有能夠優化的地方,若是你有好的方法,能夠留言告訴我哈。
再看下中文版的多行文本:
Paint的TextAlign屬性決定了繪製文本相對於drawText方法中x參數的相對位置。
舉個栗子:
Paint.Align.LEFT:默認屬性,x座標爲繪製文本的最左側座標
Paint.Align.CENTER:x座標爲繪製文本的水平中心座標
Paint.Align.RIGHT:x座標爲繪製文本的最右側座標
看圖理解下:
座標原點在控件中心:
float baseLineY = Math.abs(paint.ascent() + paint.descent()) / 2;
座標原點在控件左上角:
float baseLineY = height / 2 + Math.abs(paint.ascent() + paint.descent()) / 2;
源碼已經上傳到GitHub上了,歡迎Fork,以爲還不錯就Start一下吧!