Android在任意位置繪製文本

基礎

一般狀況下,在屏幕的特定位置上顯示文字是個很簡單的事情。使用TextView,結合各類XxxLayout,基本上想在哪顯示文字均可以。但當顯示的文字須要頻繁更新的時候,使用TextView可能就不是那麼明智了。canvas

前段時間遇到這樣一個需求,如圖: 字體

外圍圈圈旋轉填充的過程當中,中間的數字(指代百分比)從0到100變化,動畫在幾百毫秒內完成。文字在圈圈的正中顯示。看到需求,首先想到的天然是使用 TextView來顯示中間的數字,經過不斷 setText來更新文本顯示。然而,運行起來後發現 TextView的更新有很嚴重的卡頓,打開 TextView#onDraw方法,發現這個方法裏作了不少事情, onDraw如此頻繁地被調用,卡頓是天然的。若是直接繼承 ViewonDraw時使用 Canvas#drawText實現文本繪製,省去 TextView的大量額外計算,效率則會提高不少。

Canvas#drawText的原型以下:動畫

public void drawText(@NonNull String text, float x, float y, @NonNull Paint paint)
複製代碼

繪製文本的時候,咱們須要傳入(x,y)座標參數讓Canvas知道咱們指望在哪一個位置繪製文本。那麼問題來了,(x,y)究竟是哪一個點呢?x、y分別傳入多少才能讓文字在圈圈的中間顯示呢?本文將經過這個例子,來說述Android中如何靈活地在想要的位置繪製文本。spa

上述需求中,若是咱們能找到文本的中心點和(x, y)的關係,而後把這個中心點和圈圈的中心點對齊,算出相應的(x, y),文本就能顯示在圈圈的中心了。code

首先經過以下實例代碼來觀察文本位置和(x,y)座標的關係:cdn

String text = "afp8";
canvas.drawText(text, x, y, paint);
// 畫兩條垂直相交的直線直觀地展現點(x,y)的位置
drawHorizontalLine(canvas, y, Color.BLUE); // 自定義方法,畫一條水平線
drawVerticalLine(canvas, x, Color.BLUE);   // 自定義方法,畫一條垂直線
複製代碼

運行結果以下: blog

由此看到,(x,y)是文本區域左下角的一個點,x值是文本區域的左邊沿,y水平線對齊"a" "f" "8" 的底部,但"p"有一部分超出了y水平線。實際上,y水平線就是字體排印學中的**「基線(baseline)」 ,大部分英文字母和阿拉伯數字都繪製在基線之上,例外的如上述實例中的「p」等,下半部分會超出基線。基線如下的部分叫作「降部(descent)」 ,相應地,在基線之上的部分稱爲「升部(ascent)」**。 Paint類提供了 Paint#descentPaint#ascent方法獲取文本的降部和升部。如下實例代碼結合降部和升部,畫出兩條水平線:

drawHorizontalLine(canvas, y + paint.descent(), Color.GREEN);
drawHorizontalLine(canvas, y + paint.ascent(), Color.RED); // paint.ascent()返回負數,所以是"+"
複製代碼

運行結果以下: 繼承

經過升部降部, 咱們計算出了文本繪製區域的上下邊沿,上下邊沿y座標的中值就是文本中心點的y座標。中心點的y座標已經能夠計算出來,那x座標呢?咱們能夠想到,既然已經知道文本區域左邊沿,只要知道文本區域的寬度,左邊向右平移半個寬度,就能得出中心點的x座標。試一下:

float textWidth = paint.measureText("afp8");
drawVerticalLine(canvas, x + textWidth / 2, Color.BLACK);
複製代碼

運行結果以下: ip

利用文本寬度, 咱們也順利計算出文本區域中心點的x座標。所以,上述需求也就能夠輕鬆實現了。

擴展

Paint#setTextAlign

上述實例中,要找到文本區域中心點的x座標,實際上還有更簡單的實現方式,就是設置畫筆的對齊方式爲Paint.Align.CenterPaint#setTextAlign做用是設置畫筆繪製文本時(x,y)參考點的水平對齊方式,能夠是Paint.Align.LEFTPaint.Align.CENTERPaint.Align.Right(默認是LEFT),三者的區別可經過如下實例代碼來體現:文檔

drawHorizontalLine(canvas, y, Color.BLUE);

paint.setTextAlign(Paint.Align.LEFT);
canvas.drawText("left", x, y, paint);
drawVerticalLine(canvas, x, Color.BLUE);

paint.setTextAlign(Paint.Align.CENTER);
canvas.drawText("center", x + 550, y, paint);
drawVerticalLine(canvas, x + 550, Color.RED);

paint.setTextAlign(Paint.Align.RIGHT);
canvas.drawText("right", x + 1200, y, paint);
drawVerticalLine(canvas, x + 1200, Color.GREEN);
複製代碼

運行結果以下:

Paint#getTextBounds VS. Paint#measureText

根據文檔,Paint#getTextBounds能夠獲取一個能包裹住文本的最小的矩形,矩形原點默認是(0,0)。參考如下實例代碼,並將Paint#getTextBoundsPaint#measureText獲取的文本寬度進行比較:

paint.getTextBounds(text, 0, text.length(), rect);
// 獲取的矩形的原點是(0,0),加100和加300是爲了讓矩形的左上角和文本的左上角對齊
canvas.drawRect(rect.left + 100, rect.top + 300,
    rect.right + 100, rect.bottom + 300, paint);
float textWidth = paint.measureText(text);
drawVerticalLine(canvas, x + textWidth, Color.GREEN);
複製代碼

運行結果以下:

如文檔所說, Paint#getTextBounds獲得的矩形是能包裹文本的最小的矩形,對齊後矩形的四邊都緊貼着文本。而 Paint#measureText獲取的文本寬度實際上比 Paint#getTextBounds獲得的矩形寬度要大。並且標註文本區域的升部和降部的兩條水平線間的距離比 Paint#getTextBounds獲得的矩形的高度也要大一些。

中文字符

維基百科中說東亞字體無基線,也無升部和降部,那Android裏中文的繪製是怎樣的一種狀況呢?先看一箇中文字符繪製的實例:

canvas.drawText("中文", x, y, paint);
drawHorizontalLine(canvas, y + paint.descent(), Color.GREEN);
drawHorizontalLine(canvas, y + paint.ascent(), Color.BLACK);
複製代碼

運行結果以下:

從運行結果看,彷佛和英文字符的繪製並沒有兩樣,好像也是有「基線」和「升部」「降部」的。那怎麼理解「東亞字體無基線」呢?這裏直接貼 一個來自知乎用戶的解釋

總結

  • 使用Canvas#drawText進行文本繪製時,參考點(x,y)的x座標根據畫筆的對齊方式而定,能夠經過Paint#setTextAlign設置左、中、右對齊。而y座標是基線的y座標。
  • 使用Paint#ascentPaint#descent獲取文本區域的升部和降部,進而能夠定位文本區域的上下邊沿。
  • Paint#getTextBounds獲取一個能包裹住文本的最小矩形,矩形原點默認爲(0,0)。
  • 中文字符的繪製和英文字符並沒有區別,也可以使用相似的基線和升部、降部。
相關文章
相關標籤/搜索