這期是 HenCoder 自定義繪製的第三期:文字的繪製。css
以前的內容在這裏:
HenCoder Android 開發進階 自定義 View 1-1 繪製基礎
HenCoder Android 開發進階 自定義 View 1-2 Paint 詳解html
若是你沒據說過 HenCoder,能夠先看看這個:
HenCoder:給高級 Android 工程師的進階手冊java
上期的 Paint 詳解裏已經說過,文字的繪製所能控制的內容太多太細,必須拆成單獨的一期專門來說。今天這期,就是來把這些細節講清楚的。android
須要說明的有兩點:git
下面進入正題。github
Canvas
的文字繪製方法有三個:drawText()
drawTextRun()
和 drawTextOnPath()
。canvas
drawText()
是 Canvas
最基本的繪製文字的方法:給出文字的內容和位置, Canvas
按要求去繪製文字。api
String text = "Hello HenCoder";
...
canvas.drawText(text, 200, 100, paint);複製代碼
方法的參數很簡單: text
是文字內容,x
和 y
是文字的座標。但須要注意:這個座標並非文字的左上角,而是一個與左下角比較接近的位置。大概在這裏:微信
而若是你像繪製其餘內容同樣,在繪製文字的時候把座標填成 (0, 0),文字並不會顯示在 View 的左上角,而是會幾乎徹底顯示在 View 的上方,到了 View 外部看不到的位置:app
canvas.drawText(text, 0, 0, paint);複製代碼
↑ 這裏沒有貼錯圖哦
再附上一張圖,應該能更清楚地表達:
這是爲何?爲何其它的 Canvas.drawXXX()
方法,都是以左上角做爲基準點的,而 drawText()
倒是文字左下方?
先別以爲日了狗,這種設計實際上是有道理的。drawText()
參數中的 y
,指的是文字的基線( baseline ) 的位置。也就是這條線:
衆所周知,不一樣的語言和文字,每一個字符的高度和上下位置都是不同的。要讓不一樣的文字並排顯示的時候總體看起來妥當,須要讓它們上下對齊。但這個對齊的方式,不能是簡單的「底部對齊」或「頂部對齊」或「中間對齊」,而應該是一種相似於「重心對齊」的方式。就像電線上的小鳥同樣:
每隻小鳥的最高點和最低點都不同,但畫面很平衡
而這個用來讓全部文字互相對齊的基準線,就是基線( baseline )。 drawText()
方法參數中的 y
值,就是指定的基線的位置。
說完 y
值,再說說 x
值。從前面圖中的標記能夠看出來,「Hello HenCoder」繪製出來以後的 x
點並非字母 "H" 左邊的位置,而是比它的左邊再往左一點點。那麼這個「往左的一點點」是什麼呢?
它是字母 "H" 的左邊的空隙。絕大多數的字符,它們的寬度都是要略微大於實際顯示的寬度的。字符的左右兩邊會留出一部分空隙,用於文字之間的間隔,以及文字和邊框的間隔。就像這樣:
用豎線標記出邊界後的文字。
因此,明白爲何 x
座標在 "H" 的左邊再往左一點點的位置,而不是緊貼着 "H" 的左邊線了嗎?就是由於 "H" 的這個留出的空隙。
除了 drawText(text, x, y, paint)
以外, drawText()
還有幾個重載方法,使用方式跟這個都差很少,我就不說了,你本身看吧。
聲明:這個方法對中國人沒用。因此若是你有興趣,能夠繼續看;而若是你想省時間,直接跳過這個方法看後面的就行了,沒有任何毒反作用。
drawTextRun()
是在 API 23 新加入的方法。它和 drawText()
同樣都是繪製文字,但加入了兩項額外的設置——上下文和文字方向——用於輔助一些文字結構比較特殊的語言的繪製。
額外設置一:上下文。
有些語言的文字,字符的形狀會互相之間影響:一個字你單獨寫是一個樣,和別的字放在一塊兒寫又是另一個樣。不過因爲咱們最熟悉的語言——漢語和英語——都沒有這種狀況,因此只靠說可能不太好理解,我就用圖說明一下吧。
以阿拉伯文爲例。阿拉伯文裏的「عربى(阿拉伯)」是一個四字詞,它的中間兩個字符「رب」在這個詞裏的樣子,和單獨寫的時候的樣子是不一樣的。也就是說,當這四個字寫在一塊兒的時候,中間兩個字因爲受到兩邊的字的影響,形狀被改變了。看圖吧:
上面第二行和第三行的文字是徹底同樣的倆字,你敢信?
哇塞,是否是特別神奇?
不過咱們就不用管它爲何這麼神奇了,也不用替阿拉伯人操心這麼複雜的文字他們使用起來會不會很痛苦,人家都已經用了幾百上千年了。我還說回到 drawTextRun()
。 drawTextRun()
除了文字的內容和位置以外,還能夠設置文字的上下文(也就是要繪製的文字的左邊和右邊是什麼文字,雖然這些文字並不會被繪製出來),從而讓一樣的文字能夠按需表現出不一樣的顯示效果。
額外設置二:文字方向。
除了上下文, drawTextRun()
還能夠設置文字的方向,即文字是從左到右仍是從右到左排列的。
介紹完這兩類額外設置,來看一下具體的方法吧:
drawTextRun(CharSequence text, int start, int end, int contextStart, int contextEnd, float x, float y, boolean isRtl, Paint paint)
參數:text
:要繪製的文字start
:從那個字開始繪製end
:繪製到哪一個字結束contextStart
:上下文的起始位置。contextStart
須要小於等於 start
contextEnd
:上下文的結束位置。contextEnd
須要大於等於 end
x
:文字左邊的座標y
:文字的基線座標isRtl
:是不是 RTL(Right-To-Left,從右向左)
要實現上面圖中的「一樣的字有不一樣的顯示」效果,調節 contextStart
和 contextEnd
就能夠了,至於具體的實現,你有興趣的話就本身試試吧。
這就是 drawTextRun()
,一個增長了「上下文」和「RTL」支持的加強版本的 drawText()
。不過就像剛纔說過的,這個方法對中國人其實沒什麼用……
沿着一條 Path
來繪製文字。這是一個耍雜技的方法。
canvas.drawPath(path, paint); // 把 Path 也繪製出來,理解起來更方便
canvas.drawTextOnPath("Hello HeCoder", path, 0, 0, paint);複製代碼
籲,拐角處的文字怎麼那麼難看?
因此記住一條原則:
drawTextOnPath()
使用的Path
,拐彎處全用圓角,別用尖角。
具體的方法很簡單:
drawTextOnPath(String text, Path path, float hOffset, float vOffset, Paint paint)
參數裏,須要解釋的只有兩個: hOffset
和 vOffset
。它們是文字相對於 Path
的水平偏移量和豎直偏移量,利用它們能夠調整文字的位置。例如你設置 hOffset
爲 5, vOffset
爲 10,文字就會右移 5 像素和下移 10 像素。
額外講一個 StaticLayout
。這個也是使用 Canvas
來進行文字的繪製,不過並非使用 Canvas
的方法。
Canvas.drawText()
只能繪製單行的文字,而不能換行。它:
不能在 View 的邊緣自動折行
String text = "Lorem Ipsum is simply dummy text of the printing and typesetting industry.";
...
canvas.drawText(text, 50, 100, paint);複製代碼
到了 View 的邊緣處,文字繼續向後繪製到看不見的地方,而不是自動換行
不能在換行符 \n
處換行
String text = "a\nbc\ndefghi\njklm\nnopqrst\nuvwx\nyz";
...
canvas.drawText(text, 50, 100, paint);複製代碼
在換行符
\n
的位置並無換行,而只是加了個空格
若是須要繪製多行的文字,你必須自行把文字切斷後分屢次使用 drawText()
來繪製,或者——使用 StaticLayout
。
StaticLayout
並非一個 View
或者 ViewGroup
,而是 android.text.Layout
的子類,它是純粹用來繪製文字的。 StaticLayout
支持換行,它既能夠爲文字設置寬度上限來讓文字自動換行,也會在 \n
處主動換行。
String text1 = "Lorem Ipsum is simply dummy text of the printing and typesetting industry.";
StaticLayout staticLayout1 = new StaticLayout(text1, paint, 600,
Layout.Alignment.ALIGN_NORMAL, 1, 0, true);
String text2 = "a\nbc\ndefghi\njklm\nnopqrst\nuvwx\nyz";
StaticLayout staticLayout2 = new StaticLayout(text2, paint, 600,
Layout.Alignment.ALIGN_NORMAL, 1, 0, true);
...
canvas.save();
canvas.translate(50, 100);
staticLayout1.draw(canvas);
canvas.translate(0, 200);
staticLayout2.draw(canvas);
canvas.restore();複製代碼
上面代碼中出現的
Canvas.save()
Canvas.translate()
Canvas.restore()
配合起來能夠對繪製的內容進行移動。它們的具體用法我會在下期講,這期你就先依葫蘆畫瓢照搬着用吧。
StaticLayout
的構造方法是 StaticLayout(CharSequence source, TextPaint paint, int width, Layout.Alignment align, float spacingmult, float spacingadd, boolean includepad)
,其中參數裏:
width
是文字區域的寬度,文字到達這個寬度後就會自動換行;align
是文字的對齊方向;spacingmult
是行間距的倍數,一般狀況下填 1 就好;spacingadd
是行間距的額外增長值,一般狀況下填 0 就好;includeadd
是指是否在文字上下添加額外的空間,來避免某些太高的字符的繪製出現越界。
若是你須要進行多行文字的繪製,而且對文字的排列和樣式沒有太複雜的花式要求,那麼使用 StaticLayout
就好。
Paint
對文字繪製的輔助,有兩類方法:設置顯示效果的和測量文字尺寸的。
設置文字大小。
paint.setTextSize(18);
canvas.drawText(text, 100, 25, paint);
paint.setTextSize(36);
canvas.drawText(text, 100, 70, paint);
paint.setTextSize(60);
canvas.drawText(text, 100, 145, paint);
paint.setTextSize(84);
canvas.drawText(text, 100, 240, paint);複製代碼
很簡單,再也不詳細解釋。
設置字體。
paint.setTypeface(Typeface.DEFAULT);
canvas.drawText(text, 100, 150, paint);
paint.setTypeface(Typeface.SERIF);
canvas.drawText(text, 100, 300, paint);
paint.setTypeface(Typeface.createFromAsset(getContext().getAssets(), "Satisfy-Regular.ttf"));
canvas.drawText(text, 100, 450, paint);複製代碼
設置不一樣的 Typeface
就能夠顯示不一樣的字體。咱們中國人談到「字體」,比較熟悉的詞是 font, typeface 和 font 是一個意思,都表示字體。 Typeface
這個類的具體用法,須要瞭解的話能夠直接看文檔,很簡單。
嚴格地說,其實 typeface 和 font 意思不徹底同樣。typeface 指的是某套字體(即 font family ),而 font 指的是一個 typeface 具體的某個 weight 和 size 的分支。不過無所謂啦~作人最緊要系開心啦。
是否使用僞粗體。
paint.setFakeBoldText(false);
canvas.drawText(text, 100, 150, paint);
paint.setFakeBoldText(true);
canvas.drawText(text, 100, 230, paint);複製代碼
之因此叫僞粗體( fake bold ),由於它並非經過選用更高 weight 的字體讓文字變粗,而是經過程序在運行時把文字給「描粗」了。
是否加刪除線。
paint.setStrikeThruText(true);
canvas.drawText(text, 100, 150, paint);複製代碼
是否加下劃線。
paint.setUnderlineText(true);
canvas.drawText(text, 100, 150, paint);複製代碼
設置文字橫向錯切角度。其實就是文字傾斜度的啦。
paint.setTextSkewX(-0.5f);
canvas.drawText(text, 100, 150, paint);複製代碼
設置文字橫向放縮。也就是文字變胖變瘦。
paint.setTextScaleX(1);
canvas.drawText(text, 100, 150, paint);
paint.setTextScaleX(0.8f);
canvas.drawText(text, 100, 230, paint);
paint.setTextScaleX(1.2f);
canvas.drawText(text, 100, 310, paint);複製代碼
設置字符間距。默認值是 0。
paint.setLetterSpacing(0.2f);
canvas.drawText(text, 100, 150, paint);複製代碼
爲何在默認的字符間距爲 0 的狀況下,字符和字符之間也沒有牢牢貼着,這個我在前面講
Canvas.drawText()
的x
參數的時候已經說過了,在這裏應該沒有疑問吧?
用 CSS 的 font-feature-settings
的方式來設置文字。
paint.setFontFeatureSettings("smcp"); // 設置 "small caps"
canvas.drawText("Hello HenCoder", 100, 150, paint);複製代碼
CSS 全稱是 Cascading Style Sheets ,是網頁開發用來設置頁面各類元素的樣式的。咦,網頁開發的設置怎麼會出如今 Android 的 API 裏?
大多數 Android 開發者都不瞭解這個 CSS 的 font-feature-settings
屬性,不過不要緊,這個屬性設置的都是文字的一些次要特性,因此不用着急瞭解這個方法。固然有興趣的話也能夠看一看哈,文檔在這裏。
設置文字的對齊方式。一共有三個值:LEFT
CETNER
和 RIGHT
。默認值爲 LEFT
。
paint.setTextAlign(Paint.Align.LEFT);
canvas.drawText(text, 500, 150, paint);
paint.setTextAlign(Paint.Align.CENTER);
canvas.drawText(text, 500, 150 + textHeight, paint);
paint.setTextAlign(Paint.Align.RIGHT);
canvas.drawText(text, 500, 150 + textHeight * 2, paint);複製代碼
設置繪製所使用的 Locale
。
Locale
直譯是「地域」,其實就是你在系統裏設置的「語言」或「語言區域」(具體名稱取決於你用的是什麼手機),好比「簡體中文(中國)」「English (US)」「English (UK)」。有些同源的語言,在文化發展過程當中對一些相同的字衍生出了不一樣的寫法(好比中國大陸和日本對於某些漢字的寫法就有細微差異。注意,不是繁體和簡體這種同音同義不一樣字,而真的是一樣的一個字有兩種寫法)。系統語言不一樣,一樣的一個字的顯示就有可能不一樣。你能夠試一下把本身手機的語言改爲日文,而後打開微信看看聊天記錄,你會明顯發現文字的顯示發生了不少細微的變化,這就是因爲系統的 Locale
改變所致使的。
Canvas
繪製的時候,默認使用的是系統設置裏的 Locale
。而經過 Paint.setTextLocale(Locale locale)
就能夠在不改變系統設置的狀況下,直接修改繪製時的 Locale
。
paint.setTextLocale(Locale.CHINA); // 簡體中文
canvas.drawText(text, 150, 150, paint);
paint.setTextLocale(Locale.TAIWAN); // 繁體中文
canvas.drawText(text, 150, 150 + textHeight, paint);
paint.setTextLocale(Locale.JAPAN); // 日語
canvas.drawText(text, 150, 150 + textHeight * 2, paint);複製代碼
有意思吧?
另外,因爲 Android 7.0 ( API v24) 加入了多語言區域的支持,因此在 API v24 以及更高版本上,還可使用 setTextLocales(LocaleList locales)
來爲繪製設置多個語言區域。
設置是否啓用字體的 hinting (字體微調)。
如今的 Android 設備大多數都是是用的矢量字體。矢量字體的原理是對每一個字體給出一個字形的矢量描述,而後使用這一個矢量來對全部的尺寸的字體來生成對應的字形。因爲沒必要爲全部字號都設計它們的字體形狀,因此在字號較大的時候,矢量字體也可以保持字體的圓潤,這是矢量字體的優點。不過當文字的尺寸太小(好比高度小於 16 像素),有些文字會因爲失去過多細節而變得不太好看。 hinting 技術就是爲了解決這種問題的:經過向字體中加入 hinting 信息,讓矢量字體在尺寸太小的時候獲得針對性的修正,從而提升顯示效果。效果圖盜一張維基百科的:
功能很強,效果很贊。不過在如今( 2017 年),手機屏幕的像素密度已經很是高,幾乎不會再出現字體尺寸小到須要靠 hinting 來修正的狀況,因此這個方法其實……沒啥用了。能夠忽略。
聲明:這個方法對中國人沒用,不想看的話能夠直接跳過,無毒反作用。
設置是否開啓文字的 elegant height 。開啓以後,文字的高度就變優雅了(誤)。下面解釋一下所謂的 elegant height:
在有些語言中,可能會出現一些很是高的字形:
左邊那幾個泰文文字,挺高的吧?但其實它們已是被壓縮過了的,它們原本比這還要高。
這些比較高的文字,一般都有兩個版本的字體:一個原始版本,一個壓縮了高度的版本。壓縮版本能夠保證讓這些「大高個」文字在和普通文字(例如拉丁文字)放在一塊兒的時候看起來不會顯得太奇怪。事實上,Paint
繪製文字時是用的默認版本就是壓縮版本,就像上圖這樣。
不過有的時候,開發者會須要使用它們的原始(優雅)版本。使用 setElegantTextHeight()
就能夠切換到原始版本:
paint.setElegantTextHeight(true);複製代碼
這字得有多高?2 米 26 ?
那麼,setElegantTextHeight()
的做用到這裏就很清晰了:
其實這個問題我已經在 stackoverflow 回答過一次,原回答在這裏。
不過就像前面說的,因爲中國人經常使用的漢語和英語的文字並不會達到這種高度,因此這個方法對於中國人基本上是沒用的。
是否開啓次像素級的抗鋸齒( sub-pixel anti-aliasing )。
次像素級抗鋸齒這個功能解釋起來很麻煩,簡單說就是根據程序所運行的設備的屏幕類型,來進行鍼對性的次像素級的抗鋸齒計算,從而達到更好的抗鋸齒效果。更詳細的解釋能夠看這篇文章。
不過,和前面講的字體 hinting 同樣,因爲如今手機屏幕像素密度已經很高,因此默認抗鋸齒效果就已經足夠好了,通常不必開啓次像素級抗鋸齒,因此這個方法基本上沒有必要使用。
這個方法老實說我從沒用過,也始終沒有搞懂它是什麼意思,就不強行裝逼了。把文檔中的解釋照搬過來,各位本身研究吧。
Helper for setFlags(), setting or clearing the LINEAR_TEXT_FLAG bit
上面這句中提到的 LINEAR_TEXT_FLAG
:
Paint flag that enables smooth linear scaling of text.
Enabling this flag does not actually scale text, but rather adjusts text draw operations to deal gracefully with smooth adjustment of scale. When this flag is enabled, font hinting is disabled to prevent shape deformation between scale factors, and glyph caching is disabled due to the large number of glyph images that will be generated.
SUBPIXEL_TEXT_FLAG should be used in conjunction with this flag to prevent glyph positions from snapping to whole pixel values as scale factor is adjusted.
以上就是 Paint
的對文字的顯示效果設置類方法。下面介紹它的第二類方法:測量文字尺寸類。
不管是文字,仍是圖形或 Bitmap
,只有知道了尺寸,才能更好地肯定應該擺放的位置。因爲文字的繪製和圖形或 Bitmap
的繪製比起來,尺寸的計算複雜得多,因此它有一整套的方法來計算文字尺寸。
獲取推薦的行距。
即推薦的兩行文字的 baseline 的距離。這個值是系統根據文字的字體和字號自動計算的。它的做用是當你要手動繪製多行文字(而不是使用 StaticLayout)的時候,能夠在換行的時候給 y
座標加上這個值來下移文字。
canvas.drawText(texts[0], 100, 150, paint);
canvas.drawText(texts[1], 100, 150 + paint.getFontSpacing, paint);
canvas.drawText(texts[2], 100, 150 + paint.getFontSpacing * 2, paint);複製代碼
獲取 Paint
的 FontMetrics
。
FontMetrics
是個相對專業的工具類,它提供了幾個文字排印方面的數值:ascent
, descent
, top
, bottom
, leading
。
如圖,圖中有兩行文字,每一行都有 5 條線:top
, ascent
, baseline
, descent
, bottom
。(leading
並無畫出來,由於畫不出來,下面會給出解釋)
baseline
: 上圖中黑色的線。前面已經講過了,它的做用是做爲文字顯示的基準線。
ascent
/ descent
: 上圖中綠色和橙色的線,它們的做用是限制普通字符的頂部和底部範圍。
普通的字符,上不會高過 ascent
,下不會低過 descent
,例如上圖中大部分的字形都顯示在 ascent
和 descent
兩條線的範圍內。具體到 Android 的繪製中, ascent
的值是圖中綠線和 baseline
的相對位移,它的值爲負(由於它在 baseline
的上方); descent
的值是圖中橙線和 baseline
相對位移,值爲正(由於它在 baseline
的下方)。
top
/ bottom
: 上圖中藍色和紅色的線,它們的做用是限制全部字形( glyph )的頂部和底部範圍。
除了普通字符,有些字形的顯示範圍是會超過 ascent
和 descent
的,而 top
和 bottom
則限制的是全部字形的顯示範圍,包括這些特殊字形。例如上圖的第二行文字裏,就有兩個泰文的字形分別超過了 ascent
和 descent
的限制,但它們都在 top
和 bottom
兩條線的範圍內。具體到 Android 的繪製中, top
的值是圖中藍線和 baseline
的相對位移,它的值爲負(由於它在 baseline
的上方); bottom
的值是圖中紅線和 baseline
相對位移,值爲正(由於它在 baseline
的下方)。
leading
: 這個詞在上圖中沒有標記出來,由於它並非指的某條線和 baseline
的相對位移。 leading
指的是行的額外間距,即對於上下相鄰的兩行,上行的 bottom
線和下行的 top
線的距離,也就是上圖中第一行的紅線和第二行的藍線的距離(對,就是那個小細縫)。
leading
這個詞的本意其實並非行的額外間距,而是行距,即兩個相鄰行的baseline
之間的距離。不過對於不少非專業領域,leading
的意思被改變了,被你們當作行的額外間距來用;而 Android 裏的leading
,一樣也是行的額外間距的意思。另外,
leading
在這裏應該讀做 "ledding" 而不是 "leeding" 哦。緣由就不說了,我這越扯越遠沒邊了。
FontMetrics
提供的就是 Paint
根據當前字體和字號,得出的這些值的推薦值。它把這些值以變量的形式存儲,供開發者須要時使用。
FontMetrics.ascent
:float 類型。FontMetrics.descent
:float 類型。FontMetrics.top
:float 類型。FontMetrics.bottom
:float 類型。FontMetrics.leading
:float 類型。另外,ascent
和 descent
這兩個值還能夠經過 Paint.ascent()
和 Paint.descent()
來快捷獲取。
FontMetrics 和 getFontSpacing():
從定義能夠看出,上圖中兩行文字的 font spacing (即相鄰兩行的
baseline
的距離) 能夠經過bottom - top + leading
(top
的值爲負,前面剛說過,記得吧?)來計算得出。但你真的運行一下會發現,
bottom - top + leading
的結果是要大於getFontSpacing()
的返回值的。兩個方法計算得出的 font spacing 居然不同?
這並非 bug,而是由於
getFontSpacing()
的結果並非經過FontMetrics
的標準值計算出來的,而是另外計算出來的一個值,它可以作到在兩行文字不顯得擁擠的前提下縮短行距,以此來獲得更好的顯示效果。因此若是你要對文字手動換行繪製,多數時候應該選取getFontSpacing()
來獲得行距,不但使用更簡單,顯示效果也會更好。
getFontMetrics()
的返回值是 FontMetrics
類型。它還有一個重載方法 getFontMetrics(FontMetrics fontMetrics)
,計算結果會直接填進傳入的 FontMetrics
對象,而不是從新建立一個對象。這種用法在須要頻繁獲取 FontMetrics
的時候性能會好些。
另外,這兩個方法還有一對一樣結構的對應的方法 getFontMetricsInt()
和 getFontMetricsInt(FontMetricsInt fontMetrics)
,用於獲取 FontMetricsInt
類型的結果。
獲取文字的顯示範圍。
參數裏,text
是要測量的文字,start
和 end
分別是文字的起始和結束位置,bounds
是存儲文字顯示範圍的對象,方法在測算完成以後會把結果寫進 bounds
。
paint.setStyle(Paint.Style.FILL);
canvas.drawText(text, offsetX, offsetY, paint);
paint.getTextBounds(text, 0, text.length(), bounds);
bounds.left += offsetX;
bounds.top += offsetY;
bounds.right += offsetX;
bounds.bottom += offsetY;
paint.setStyle(Paint.Style.STROKE);
canvas.drawRect(bounds, paint);複製代碼
它有一個重載方法 getTextBounds(char[] text, int index, int count, Rect bounds)
,用法很是類似,再也不介紹。
測量文字的寬度並返回。
canvas.drawText(text, offsetX, offsetY, paint);
float textWidth = paint.measureText(text);
canvas.drawLine(offsetX, offsetY, offsetX + textWidth, offsetY, paint);複製代碼
咦,前面有了 getTextBounds()
,這裏怎麼又有一個 measureText()
?
若是你用代碼分別使用 getTextBounds()
和 measureText()
來測量文字的寬度,你會發現 measureText()
測出來的寬度老是比 getTextBounds()
大一點點。這是由於這兩個方法其實測量的是兩個不同的東西。
getTextBounds
: 它測量的是文字的顯示範圍(關鍵詞:顯示)。形象點來講,你這段文字外放置一個可變的矩形,而後把矩形儘量地縮小,一直小到這個矩形剛好牢牢包裹住文字,那麼這個矩形的範圍,就是這段文字的 bounds。
measureText()
: 它測量的是文字繪製時所佔用的寬度(關鍵詞:佔用)。前面已經講過,一個文字在界面中,每每須要佔用比他的實際顯示寬度更多一點的寬度,以此來讓文字和文字之間保留一些間距,不會顯得過於擁擠。上面的這幅圖,我並無設置 setLetterSpacing()
,這裏的 letter spacing 是默認值 0,但你能夠看到,圖中每兩個字母之間都是有空隙的。另外,下方那條用於表示文字寬度的橫線,在左邊超出了第一個字母 H
一段距離的,在右邊也超出了最後一個字母 r
(雖然右邊這裏用肉眼不太容易分辨),而就是兩邊的這兩個「超出」,致使了 measureText()
比 getTextBounds()
測量出的寬度要大一些。
在實際的開發中,測量寬度要用 measureText()
仍是 getTextBounds()
,須要根據狀況而定。不過你只要掌握了上面我所說的它們的本質,在選擇的時候就不會爲難和疑惑了。
measureText(String text)
也有幾個重載方法,用法和它大同小異,再也不介紹。
獲取字符串中每一個字符的寬度,並把結果填入參數 widths
。
這至關於 measureText()
的一個快捷方法,它的計算等價於對字符串中的每一個字符分別調用 measureText()
,並把它們的計算結果分別填入 widths
的不一樣元素。
getTextWidths()
一樣也有好幾個變種,使用大同小異,再也不介紹。
這個方法也是用來測量文字寬度的。但和 measureText()
的區別是, breakText()
是在給出寬度上限的前提下測量文字的寬度。若是文字的寬度超出了上限,那麼在臨近超限的位置截斷文字。
int measuredCount;
float[] measuredWidth = {0};
// 寬度上限 300 (不夠用,截斷)
measuredCount = paint.breakText(text, 0, text.length(), true, 300, measuredWidth);
canvas.drawText(text, 0, measuredCount, 150, 150, paint);
// 寬度上限 400 (不夠用,截斷)
measuredCount = paint.breakText(text, 0, text.length(), true, 400, measuredWidth);
canvas.drawText(text, 0, measuredCount, 150, 150 + fontSpacing, paint);
// 寬度上限 500 (夠用)
measuredCount = paint.breakText(text, 0, text.length(), true, 500, measuredWidth);
canvas.drawText(text, 0, measuredCount, 150, 150 + fontSpacing * 2, paint);
// 寬度上限 600 (夠用)
measuredCount = paint.breakText(text, 0, text.length(), true, 600, measuredWidth);
canvas.drawText(text, 0, measuredCount, 150, 150 + fontSpacing * 3, paint);複製代碼
breakText()
的返回值是截取的文字個數(若是寬度沒有超限,則是文字的總個數)。參數中, text
是要測量的文字;measureForwards
表示文字的測量方向,true
表示由左往右測量;maxWidth
是給出的寬度上限;measuredWidth
是用於接受數據,而不是用於提供數據的:方法測量完成後會把截取的文字寬度(若是寬度沒有超限,則爲文字總寬度)賦值給 measuredWidth[0]
。
這個方法能夠用於多行文字的折行計算。
breakText()
也有幾個重載方法,使用大同小異,再也不介紹。
對於 EditText
以及相似的場景,會須要繪製光標。光標的計算很麻煩,不過 API 23 引入了兩個新的方法,有了這兩個方法後,計算光標就方便了不少。
對於一段文字,計算出某個字符處光標的 x
座標。 start
end
是文字的起始和結束座標;contextStart
contextEnd
是上下文的起始和結束座標;isRtl
是文字的方向;offset
是字數的偏移,即計算第幾個字符處的光標。
int length = text.length();
float advance = paint.getRunAdvance(text, 0, length, 0, length, false, length);
canvas.drawText(text, offsetX, offsetY, paint);
canvas.drawLine(offsetX + advance, offsetY - 50, offsetX + advance, offsetY + 10, paint);複製代碼
其實,說是測量光標位置的,本質上這也是一個測量文字寬度的方法。上面這個例子中,start
和 contextStart
都是 0, end
contextEnd
和 offset
都等於 text.length()
。在這種狀況下,它是等價於 measureText(text)
的,即完整測量一段文字的寬度。而對於更復雜的需求,getRunAdvance()
能作的事就比 measureText()
多了。
// 包含特殊符號的繪製(如 emoji 表情)
String text = "Hello HenCoder \uD83C\uDDE8\uD83C\uDDF3" // "Hello HenCoder 🇨🇳"
...
float advance1 = paint.getRunAdvance(text, 0, length, 0, length, false, length);
float advance2 = paint.getRunAdvance(text, 0, length, 0, length, false, length - 1);
float advance3 = paint.getRunAdvance(text, 0, length, 0, length, false, length - 2);
float advance4 = paint.getRunAdvance(text, 0, length, 0, length, false, length - 3);
float advance5 = paint.getRunAdvance(text, 0, length, 0, length, false, length - 4);
float advance6 = paint.getRunAdvance(text, 0, length, 0, length, false, length - 5);
...複製代碼
如上圖,🇨🇳
雖然佔了 4 個字符(\uD83C\uDDE8\uD83C\uDDF3
),但當 offset 是表情中間處時, getRunAdvance()
得出的結果並不會在表情的中間處。爲何?由於這是用來計算光標的方法啊,光標固然不能出如今符號中間啦。
給出一個位置的像素值,計算出文字中最接近這個位置的字符偏移量(即第幾個字符最接近這個座標)。
方法的參數很簡單: text
是要測量的文字;start
end
是文字的起始和結束座標;contextStart
contextEnd
是上下文的起始和結束座標;isRtl
是文字方向;advance
是給出的位置的像素值。填入參數,對應的字符偏移量將做爲返回值返回。
getOffsetForAdvance()
配合上 getRunAdvance()
一塊兒使用,就能夠實現「獲取用戶點擊處的文字座標」的需求。
檢查指定的字符串中是不是一個單獨的字形 (glyph)。最簡單的狀況是,string
只有一個字母(好比 a
)。
以上這些內容,就是文字繪製的相關知識。它們有的經常使用,有的不經常使用,有的甚至能夠說是在某些狀況下沒用,不過你把它們所有搞懂了,在實際的開發中,就知道哪些事情能夠作到,哪些事情作不到,以及應該怎麼作了。
爲了不轉頭就忘,強烈建議你趁熱打鐵,作一下這個練習項目:HenCoderPracticeDraw3
基本繪製(Canvas.drawXXX()
)以及高級繪製(Paint
)都講完了,下節的內容是 Canvas
對繪製的輔助:範圍裁切和幾何變換:
感謝參與這期預發佈內測的讀者:
林子洋、Rookie run、showwiki、孫志帥、Tim Aimee、明天,你好、Mr.Coder、特倫特、承香墨影&version=12020810&lang=zh_CN&nettype=WIFI&a8scene=0&fontScale=100&pass_ticket=3ZSTFlOd8P3j6MV7KWW3Yz6Vk%2FG9MLwZDkm668BrQCc%3D)、HanDongFizz、停停走走、Vinctor
若是你看完以爲有收穫,把文章轉發到你的微博、微信羣、朋友圈、公衆號,讓其餘須要的人也看到吧。