HenCoder Android 开发进阶:自定义 View 1-3 drawText() 文字的绘制
這期是 HenCoder 自定義繪制的第三期:文字的繪制。
之前的內容在這里:?
HenCoder Android 開發進階 自定義 View 1-1 繪制基礎?
HenCoder Android 開發進階 自定義 View 1-2 Paint 詳解
如果你沒聽說過 HenCoder,可以先看看這個:?
HenCoder:給高級 Android 工程師的進階手冊
簡介
上期的 Paint 詳解里已經說過,文字的繪制所能控制的內容太多太細,必須拆成單獨的一期專門來講。今天這期,就是來把這些細節講清楚的。
需要說明的有兩點:
下面進入正題。
1 Canvas 繪制文字的方式
Canvas?的文字繪制方法有三個:drawText()?drawTextRun()?和?drawTextOnPath()。
1.1 drawText(String text, float x, float y, Paint paint)
drawText()?是?Canvas?最基本的繪制文字的方法:給出文字的內容和位置,?Canvas?按要求去繪制文字。
String text = "Hello HenCoder";...canvas.drawText(text, 200, 100, paint);
方法的參數很簡單:?text?是文字內容,x?和?y?是文字的坐標。但需要注意:這個坐標并不是文字的左上角,而是一個與左下角比較接近的位置。大概在這里:
而如果你像繪制其他內容一樣,在繪制文字的時候把坐標填成 (0, 0),文字并不會顯示在 View 的左上角,而是會幾乎完全顯示在 View 的上方,到了 View 外部看不到的位置:
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()?還有幾個重載方法,使用方式跟這個都差不多,我就不說了,你自己看吧。
1.2 drawTextRun()
聲明:這個方法對中國人沒用。所以如果你有興趣,可以繼續看;而如果你想省時間,直接跳過這個方法看后面的就好了,沒有任何毒副作用。
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()?。不過就像剛才說過的,這個方法對中國人其實沒什么用……
1.3 drawTextOnPath()
沿著一條?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 像素。
1.4 StaticLayout
額外講一個?StaticLayout。這個也是使用?Canvas?來進行文字的繪制,不過并不是使用?Canvas?的方法。
Canvas.drawText()?只能繪制單行的文字,而不能換行。它:
- 不能在 View 的邊緣自動折行
到了 View 的邊緣處,文字繼續向后繪制到看不見的地方,而不是自動換行
- 不能在換行符?\n?處換行
在換行符?\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?就好。
2 Paint 對文字繪制的輔助
Paint?對文字繪制的輔助,有兩類方法:設置顯示效果的和測量文字尺寸的。
2.1 設置顯示效果類
2.1.1 setTextSize(float textSize)
設置文字大小。
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);
很簡單,不再詳細解釋。
2.1.2 setTypeface(Typeface typeface)
設置字體。
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 的分支。不過無所謂啦~做人最緊要系開心啦。
2.1.3 setFakeBoldText(boolean fakeBoldText)
是否使用偽粗體。
paint.setFakeBoldText(false); canvas.drawText(text, 100, 150, paint); paint.setFakeBoldText(true); canvas.drawText(text, 100, 230, paint);
之所以叫偽粗體( fake bold ),因為它并不是通過選用更高 weight 的字體讓文字變粗,而是通過程序在運行時把文字給「描粗」了。
2.1.4 setStrikeThruText(boolean strikeThruText)
是否加刪除線。
paint.setStrikeThruText(true); canvas.drawText(text, 100, 150, paint);
2.1.5 setUnderlineText(boolean underlineText)
是否加下劃線。
paint.setUnderlineText(true); canvas.drawText(text, 100, 150, paint);
2.1.6 setTextSkewX(float skewX)
設置文字橫向錯切角度。其實就是文字傾斜度的啦。
paint.setTextSkewX(-0.5f); canvas.drawText(text, 100, 150, paint);
2.1.7 setTextScaleX(float scaleX)
設置文字橫向放縮。也就是文字變胖變瘦。
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);
2.1.8 setLetterSpacing(float letterSpacing)
設置字符間距。默認值是 0。
paint.setLetterSpacing(0.2f); canvas.drawText(text, 100, 150, paint);
為什么在默認的字符間距為 0 的情況下,字符和字符之間也沒有緊緊貼著,這個我在前面講?Canvas.drawText()?的?x?參數的時候已經說過了,在這里應該沒有疑問吧?
2.1.9 setFontFeatureSettings(String settings)
用 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?屬性,不過沒關系,這個屬性設置的都是文字的一些次要特性,所以不用著急了解這個方法。當然有興趣的話也可以看一看哈,文檔在這里。
2.1.10 setTextAlign(Paint.Align align)
設置文字的對齊方式。一共有三個值: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);
2.1.11 setTextLocale(Locale locale) / setTextLocales(LocaleList locales)
設置繪制所使用的?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)?來為繪制設置多個語言區域。
2.1.12 setHinting(int mode)
設置是否啟用字體的 hinting (字體微調)。
現在的 Android 設備大多數都是是用的矢量字體。矢量字體的原理是對每個字體給出一個字形的矢量描述,然后使用這一個矢量來對所有的尺寸的字體來生成對應的字形。由于不必為所有字號都設計它們的字體形狀,所以在字號較大的時候,矢量字體也能夠保持字體的圓潤,這是矢量字體的優勢。不過當文字的尺寸過小(比如高度小于 16 像素),有些文字會由于失去過多細節而變得不太好看。 hinting 技術就是為了解決這種問題的:通過向字體中加入 hinting 信息,讓矢量字體在尺寸過小的時候得到針對性的修正,從而提高顯示效果。效果圖盜一張維基百科的:
功能很強,效果很贊。不過在現在( 2017 年),手機屏幕的像素密度已經非常高,幾乎不會再出現字體尺寸小到需要靠 hinting 來修正的情況,所以這個方法其實……沒啥用了。可以忽略。
2.1.13 setElegantTextHeight(boolean elegant)
聲明:這個方法對中國人沒用,不想看的話可以直接跳過,無毒副作用。
設置是否開啟文字的 elegant height 。開啟之后,文字的高度就變優雅了(誤)。下面解釋一下所謂的 elegant height:
在有些語言中,可能會出現一些非常高的字形:
左邊那幾個泰文文字,挺高的吧?但其實它們已經是被壓縮過了的,它們本來比這還要高。
這些比較高的文字,通常都有兩個版本的字體:一個原始版本,一個壓縮了高度的版本。壓縮版本可以保證讓這些「大高個」文字在和普通文字(例如拉丁文字)放在一起的時候看起來不會顯得太奇怪。事實上,Paint?繪制文字時是用的默認版本就是壓縮版本,就像上圖這樣。
不過有的時候,開發者會需要使用它們的原始(優雅)版本。使用?setElegantTextHeight()?就可以切換到原始版本:
paint.setElegantTextHeight(true);
這字得有多高?2 米 26 ?
那么,setElegantTextHeight()?的作用到這里就很清晰了:
其實這個問題我已經在 stackoverflow 回答過一次,原回答在這里。
不過就像前面說的,由于中國人常用的漢語和英語的文字并不會達到這種高度,所以這個方法對于中國人基本上是沒用的。
2.1.14 setSubpixelText(boolean subpixelText)
是否開啟次像素級的抗鋸齒( sub-pixel anti-aliasing )。
次像素級抗鋸齒這個功能解釋起來很麻煩,簡單說就是根據程序所運行的設備的屏幕類型,來進行針對性的次像素級的抗鋸齒計算,從而達到更好的抗鋸齒效果。更詳細的解釋可以看這篇文章。
不過,和前面講的字體 hinting 一樣,由于現在手機屏幕像素密度已經很高,所以默認抗鋸齒效果就已經足夠好了,一般沒必要開啟次像素級抗鋸齒,所以這個方法基本上沒有必要使用。
2.1.15 setLinearText(boolean linearText)
這個方法老實說我從沒用過,也始終沒有搞懂它是什么意思,就不強行裝逼了。把文檔中的解釋照搬過來,各位自己研究吧。
Helper for setFlags(), setting or clearing the LINEARTEXTFLAG 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.
SUBPIXELTEXTFLAG should be used in conjunction with this flag to prevent glyph positions from snapping to whole pixel values as scale factor is adjusted.
以上就是?Paint?的對文字的顯示效果設置類方法。下面介紹它的第二類方法:測量文字尺寸類。
2.2 測量文字尺寸類
不論是文字,還是圖形或?Bitmap,只有知道了尺寸,才能更好地確定應該擺放的位置。由于文字的繪制和圖形或?Bitmap?的繪制比起來,尺寸的計算復雜得多,所以它有一整套的方法來計算文字尺寸。
2.2.1 float getFontSpacing()
獲取推薦的行距。
即推薦的兩行文字的 baseline 的距離。這個值是系統根據文字的字體和字號自動計算的。它的作用是當你要手動繪制多行文字(而不是使用 StaticLayout)的時候,可以在換行的時候給?y?坐標加上這個值來下移文字。
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);
2.2.2 FontMetircs getFontMetrics()
獲取?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?類型的結果。
2.2.3 getTextBounds(String text, int start, int end, Rect bounds)
獲取文字的顯示范圍。
參數里,text?是要測量的文字,start?和?end?分別是文字的起始和結束位置,bounds?是存儲文字顯示范圍的對象,方法在測算完成之后會把結果寫進?bounds。
paint.setElegantTextHeight(true);
它有一個重載方法?getTextBounds(char[] text, int index, int count, Rect bounds),用法非常相似,不再介紹。
2.2.4 float measureText(String text)
測量文字的寬度并返回。
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)?也有幾個重載方法,用法和它大同小異,不再介紹。
2.2.5 getTextWidths(String text, float[] widths)
獲取字符串中每個字符的寬度,并把結果填入參數?widths。
這相當于?measureText()?的一個快捷方法,它的計算等價于對字符串中的每個字符分別調用?measureText()?,并把它們的計算結果分別填入?widths?的不同元素。
getTextWidths()?同樣也有好幾個變種,使用大同小異,不再介紹。
2.2.6 int breakText(String text, boolean measureForwards, float maxWidth, float[] measuredWidth)
這個方法也是用來測量文字寬度的。但和?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()?也有幾個重載方法,使用大同小異,不再介紹。
2.2.7 光標相關
對于?EditText?以及類似的場景,會需要繪制光標。光標的計算很麻煩,不過 API 23 引入了兩個新的方法,有了這兩個方法后,計算光標就方便了很多。
2.2.7.1 getRunAdvance(CharSequence text, int start, int end, int contextStart, int contextEnd, boolean isRtl, int offset)
對于一段文字,計算出某個字符處光標的?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總結
以上是生活随笔為你收集整理的HenCoder Android 开发进阶:自定义 View 1-3 drawText() 文字的绘制的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: pat 1037
- 下一篇: Android 分享功能大全