Android 圖解Canvas drawText文字居中的那些事

GitHub傳送門git

1.寫在前面

在實現自定義控件的過程當中,經常會有繪製居中文字的需求,因而在網上搜了一些相關的博客,老是看的一臉懵逼,就想着本身分析一下,在此記錄下來,但願對你們可以有所幫助。github

2.繪製一段文本

首先把座標原點移動到控件中心(默認座標原點在屏幕左上角),這樣看起來比較直觀一些,而後繪製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軸。

3.繪製居中的文本

在上文中,咱們學習瞭如何繪製一段文本,以及其中參數和座標的含義,接下來進入正題,看下如何才能繪製居中的文本。

首先看一張圖,此時文本的baseline正好爲x軸,若是想要文本居中顯示的話,就須要先計算文本的寬度和高度:

  • 寬度:調用Paint的measureText方法就能夠得到文本的寬度

  • 高度:文本的高度就是實際繪製區域的高度,能夠用(fontMetrics.descent - fontMetrics.ascent)獲取,由於ascent爲負數,因此最終算出來的是二者的和

如今有了寬度,把繪製文本的x座標向左移動(寬度 / 2)就能夠水平居中,可是垂直方向就不能這麼幹了,咱們要將文本向下移動baseline到文本中心的距離,也就是(高度 / 2 - fontMetrics.descent),以下圖所示:

計算baseLineY

如今的公式爲:

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

	// 設置寬度超過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

使用StaticLayout,每行設置的寬度是相同的,當需求爲每行顯示不一樣長度的文本時,這種方式就不能使用了,別擔憂,接着來看下第二種方式。

4.2 方式二

使用循環drawText的方式進行繪製,看圖說話:

計算baseLineY

如今須要繪製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);
	}
}
複製代碼

對照上圖再看代碼就很好理解了,以爲代碼中的公式還有能夠優化的地方,若是你有好的方法,能夠留言告訴我哈。

再看下中文版的多行文本:

多行居中文本

5.TextAlign

Paint的TextAlign屬性決定了繪製文本相對於drawText方法中x參數的相對位置。

舉個栗子:

  • Paint.Align.LEFT:默認屬性,x座標爲繪製文本的最左側座標

  • Paint.Align.CENTER:x座標爲繪製文本的水平中心座標

  • Paint.Align.RIGHT:x座標爲繪製文本的最右側座標

看圖理解下:

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.寫在最後

源碼已經上傳到GitHub上了,歡迎Fork,以爲還不錯就Start一下吧!

GitHub傳送門

點我下載本文Demo的Apk

相關文章
相關標籤/搜索