HenCoder Android 开发进阶:自定义 View 1-3 drawText() 文字的绘制
這期是 HenCoder 自定義繪制的第三期:文字的繪制。
之前的內(nèi)容在這里:?
HenCoder Android 開發(fā)進(jìn)階 自定義 View 1-1 繪制基礎(chǔ)?
HenCoder Android 開發(fā)進(jìn)階 自定義 View 1-2 Paint 詳解
如果你沒聽說(shuō)過(guò) HenCoder,可以先看看這個(gè):?
HenCoder:給高級(jí) Android 工程師的進(jìn)階手冊(cè)
簡(jiǎn)介
上期的 Paint 詳解里已經(jīng)說(shuō)過(guò),文字的繪制所能控制的內(nèi)容太多太細(xì),必須拆成單獨(dú)的一期專門來(lái)講。今天這期,就是來(lái)把這些細(xì)節(jié)講清楚的。
需要說(shuō)明的有兩點(diǎn):
下面進(jìn)入正題。
1 Canvas 繪制文字的方式
Canvas?的文字繪制方法有三個(gè):drawText()?drawTextRun()?和?drawTextOnPath()。
1.1 drawText(String text, float x, float y, Paint paint)
drawText()?是?Canvas?最基本的繪制文字的方法:給出文字的內(nèi)容和位置,?Canvas?按要求去繪制文字。
String text = "Hello HenCoder";...canvas.drawText(text, 200, 100, paint);
方法的參數(shù)很簡(jiǎn)單:?text?是文字內(nèi)容,x?和?y?是文字的坐標(biāo)。但需要注意:這個(gè)坐標(biāo)并不是文字的左上角,而是一個(gè)與左下角比較接近的位置。大概在這里:
而如果你像繪制其他內(nèi)容一樣,在繪制文字的時(shí)候把坐標(biāo)填成 (0, 0),文字并不會(huì)顯示在 View 的左上角,而是會(huì)幾乎完全顯示在 View 的上方,到了 View 外部看不到的位置:
canvas.drawText(text, 0, 0, paint);
↑ 這里沒有貼錯(cuò)圖哦
再附上一張圖,應(yīng)該能更清楚地表達(dá):
這是為什么?為什么其它的?Canvas.drawXXX()?方法,都是以左上角作為基準(zhǔn)點(diǎn)的,而?drawText()?卻是文字左下方?
先別覺得日了狗,這種設(shè)計(jì)其實(shí)是有道理的。drawText()?參數(shù)中的?y?,指的是文字的基線( baseline )?的位置。也就是這條線:
眾所周知,不同的語(yǔ)言和文字,每個(gè)字符的高度和上下位置都是不一樣的。要讓不同的文字并排顯示的時(shí)候整體看起來(lái)穩(wěn)當(dāng),需要讓它們上下對(duì)齊。但這個(gè)對(duì)齊的方式,不能是簡(jiǎn)單的「底部對(duì)齊」或「頂部對(duì)齊」或「中間對(duì)齊」,而應(yīng)該是一種類似于「重心對(duì)齊」的方式。就像電線上的小鳥一樣:
每只小鳥的最高點(diǎn)和最低點(diǎn)都不一樣,但畫面很平衡
而這個(gè)用來(lái)讓所有文字互相對(duì)齊的基準(zhǔn)線,就是基線( baseline )。?drawText()?方法參數(shù)中的?y?值,就是指定的基線的位置。
說(shuō)完?y?值,再說(shuō)說(shuō)?x?值。從前面圖中的標(biāo)記可以看出來(lái),「Hello HenCoder」繪制出來(lái)之后的?x?點(diǎn)并不是字母 "H" 左邊的位置,而是比它的左邊再往左一點(diǎn)點(diǎn)。那么這個(gè)「往左的一點(diǎn)點(diǎn)」是什么呢?
它是字母 "H" 的左邊的空隙。絕大多數(shù)的字符,它們的寬度都是要略微大于實(shí)際顯示的寬度的。字符的左右兩邊會(huì)留出一部分空隙,用于文字之間的間隔,以及文字和邊框的間隔。就像這樣:
用豎線標(biāo)記出邊界后的文字。
所以,明白為什么?x?坐標(biāo)在 "H" 的左邊再往左一點(diǎn)點(diǎn)的位置,而不是緊貼著 "H" 的左邊線了嗎?就是因?yàn)?"H" 的這個(gè)留出的空隙。
除了?drawText(text, x, y, paint)?之外,?drawText()?還有幾個(gè)重載方法,使用方式跟這個(gè)都差不多,我就不說(shuō)了,你自己看吧。
1.2 drawTextRun()
聲明:這個(gè)方法對(duì)中國(guó)人沒用。所以如果你有興趣,可以繼續(xù)看;而如果你想省時(shí)間,直接跳過(guò)這個(gè)方法看后面的就好了,沒有任何毒副作用。
drawTextRun()?是在 API 23 新加入的方法。它和?drawText()?一樣都是繪制文字,但加入了兩項(xiàng)額外的設(shè)置——上下文和文字方向——用于輔助一些文字結(jié)構(gòu)比較特殊的語(yǔ)言的繪制。
-
額外設(shè)置一:上下文。
有些語(yǔ)言的文字,字符的形狀會(huì)互相之間影響:一個(gè)字你單獨(dú)寫是一個(gè)樣,和別的字放在一起寫又是另外一個(gè)樣。不過(guò)由于我們最熟悉的語(yǔ)言——漢語(yǔ)和英語(yǔ)——都沒有這種情況,所以只靠說(shuō)可能不太好理解,我就用圖說(shuō)明一下吧。
以阿拉伯文為例。阿拉伯文里的「????(阿拉伯)」是一個(gè)四字詞,它的中間兩個(gè)字符「??」在這個(gè)詞里的樣子,和單獨(dú)寫的時(shí)候的樣子是不同的。也就是說(shuō),當(dāng)這四個(gè)字寫在一起的時(shí)候,中間兩個(gè)字由于受到兩邊的字的影響,形狀被改變了。看圖吧:
上面第二行和第三行的文字是完全一樣的倆字,你敢信?
哇塞,是不是特別神奇?
不過(guò)我們就不用管它為什么這么神奇了,也不用替阿拉伯人操心這么復(fù)雜的文字他們使用起來(lái)會(huì)不會(huì)很痛苦,人家都已經(jīng)用了幾百上千年了。我還說(shuō)回到?drawTextRun()。?drawTextRun()?除了文字的內(nèi)容和位置之外,還可以設(shè)置文字的上下文(也就是要繪制的文字的左邊和右邊是什么文字,雖然這些文字并不會(huì)被繪制出來(lái)),從而讓同樣的文字可以按需表現(xiàn)出不同的顯示效果。
-
額外設(shè)置二:文字方向。
除了上下文,?drawTextRun()?還可以設(shè)置文字的方向,即文字是從左到右還是從右到左排列的。
介紹完這兩類額外設(shè)置,來(lái)看一下具體的方法吧:
drawTextRun(CharSequence text, int start, int end, int contextStart, int contextEnd, float x, float y, boolean isRtl, Paint paint)
參數(shù):?
text:要繪制的文字?
start:從那個(gè)字開始繪制?
end:繪制到哪個(gè)字結(jié)束?
contextStart:上下文的起始位置。contextStart?需要小于等于?start?
contextEnd:上下文的結(jié)束位置。contextEnd?需要大于等于?end?
x:文字左邊的坐標(biāo)?
y:文字的基線坐標(biāo)?
isRtl:是否是 RTL(Right-To-Left,從右向左)
要實(shí)現(xiàn)上面圖中的「同樣的字有不同的顯示」效果,調(diào)節(jié)?contextStart?和?contextEnd?就可以了,至于具體的實(shí)現(xiàn),你有興趣的話就自己試試吧。
這就是?drawTextRun()?,一個(gè)增加了「上下文」和「RTL」支持的增強(qiáng)版本的?drawText()?。不過(guò)就像剛才說(shuō)過(guò)的,這個(gè)方法對(duì)中國(guó)人其實(shí)沒什么用……
1.3 drawTextOnPath()
沿著一條?Path?來(lái)繪制文字。這是一個(gè)耍雜技的方法。
canvas.drawPath(path, paint); // 把 Path 也繪制出來(lái),理解起來(lái)更方便 canvas.drawTextOnPath("Hello HeCoder", path, 0, 0, paint);
吁,拐角處的文字怎么那么難看?
所以記住一條原則:?drawTextOnPath()?使用的?Path?,拐彎處全用圓角,別用尖角。
具體的方法很簡(jiǎn)單:
drawTextOnPath(String text, Path path, float hOffset, float vOffset, Paint paint)
參數(shù)里,需要解釋的只有兩個(gè):?hOffset?和?vOffset。它們是文字相對(duì)于?Path?的水平偏移量和豎直偏移量,利用它們可以調(diào)整文字的位置。例如你設(shè)置?hOffset?為 5,?vOffset?為 10,文字就會(huì)右移 5 像素和下移 10 像素。
1.4 StaticLayout
額外講一個(gè)?StaticLayout。這個(gè)也是使用?Canvas?來(lái)進(jìn)行文字的繪制,不過(guò)并不是使用?Canvas?的方法。
Canvas.drawText()?只能繪制單行的文字,而不能換行。它:
- 不能在 View 的邊緣自動(dòng)折行
到了 View 的邊緣處,文字繼續(xù)向后繪制到看不見的地方,而不是自動(dòng)換行
- 不能在換行符?\n?處換行
在換行符?\n?的位置并沒有換行,而只是加了個(gè)空格
如果需要繪制多行的文字,你必須自行把文字切斷后分多次使用?drawText()?來(lái)繪制,或者——使用?StaticLayout?。
StaticLayout?并不是一個(gè)?View?或者?ViewGroup?,而是?android.text.Layout?的子類,它是純粹用來(lái)繪制文字的。?StaticLayout?支持換行,它既可以為文字設(shè)置寬度上限來(lái)讓文字自動(dòng)換行,也會(huì)在?\n處主動(dòng)換行。
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();上面代碼中出現(xiàn)的?Canvas.save()?Canvas.translate()?Canvas.restore()?配合起來(lái)可以對(duì)繪制的內(nèi)容進(jìn)行移動(dòng)。它們的具體用法我會(huì)在下期講,這期你就先依葫蘆畫瓢照搬著用吧。
StaticLayout?的構(gòu)造方法是 StaticLayout(CharSequence source, TextPaint paint, int width, Layout.Alignment align, float spacingmult, float spacingadd, boolean includepad),其中參數(shù)里:
width?是文字區(qū)域的寬度,文字到達(dá)這個(gè)寬度后就會(huì)自動(dòng)換行;?
align?是文字的對(duì)齊方向;?
spacingmult?是行間距的倍數(shù),通常情況下填 1 就好;?
spacingadd?是行間距的額外增加值,通常情況下填 0 就好;?
includeadd?是指是否在文字上下添加額外的空間,來(lái)避免某些過(guò)高的字符的繪制出現(xiàn)越界。
如果你需要進(jìn)行多行文字的繪制,并且對(duì)文字的排列和樣式?jīng)]有太復(fù)雜的花式要求,那么使用?StaticLayout?就好。
2 Paint 對(duì)文字繪制的輔助
Paint?對(duì)文字繪制的輔助,有兩類方法:設(shè)置顯示效果的和測(cè)量文字尺寸的。
2.1 設(shè)置顯示效果類
2.1.1 setTextSize(float textSize)
設(shè)置文字大小。
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);
很簡(jiǎn)單,不再詳細(xì)解釋。
2.1.2 setTypeface(Typeface typeface)
設(shè)置字體。
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);
設(shè)置不同的?Typeface?就可以顯示不同的字體。我們中國(guó)人談到「字體」,比較熟悉的詞是 font, typeface 和 font 是一個(gè)意思,都表示字體。?Typeface?這個(gè)類的具體用法,需要了解的話可以直接看文檔,很簡(jiǎn)單。
嚴(yán)格地說(shuō),其實(shí) typeface 和 font 意思不完全一樣。typeface 指的是某套字體(即 font family ),而 font 指的是一個(gè) typeface 具體的某個(gè) weight 和 size 的分支。不過(guò)無(wú)所謂啦~做人最緊要系開心啦。
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 ),因?yàn)樗⒉皇峭ㄟ^(guò)選用更高 weight 的字體讓文字變粗,而是通過(guò)程序在運(yùn)行時(shí)把文字給「描粗」了。
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)
設(shè)置文字橫向錯(cuò)切角度。其實(shí)就是文字傾斜度的啦。
paint.setTextSkewX(-0.5f); canvas.drawText(text, 100, 150, paint);
2.1.7 setTextScaleX(float scaleX)
設(shè)置文字橫向放縮。也就是文字變胖變瘦。
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)
設(shè)置字符間距。默認(rèn)值是 0。
paint.setLetterSpacing(0.2f); canvas.drawText(text, 100, 150, paint);
為什么在默認(rèn)的字符間距為 0 的情況下,字符和字符之間也沒有緊緊貼著,這個(gè)我在前面講?Canvas.drawText()?的?x?參數(shù)的時(shí)候已經(jīng)說(shuō)過(guò)了,在這里應(yīng)該沒有疑問吧?
2.1.9 setFontFeatureSettings(String settings)
用 CSS 的?font-feature-settings?的方式來(lái)設(shè)置文字。
paint.setFontFeatureSettings("smcp"); // 設(shè)置 "small caps" canvas.drawText("Hello HenCoder", 100, 150, paint);
CSS 全稱是 Cascading Style Sheets ,是網(wǎng)頁(yè)開發(fā)用來(lái)設(shè)置頁(yè)面各種元素的樣式的。咦,網(wǎng)頁(yè)開發(fā)的設(shè)置怎么會(huì)出現(xiàn)在 Android 的 API 里?
大多數(shù) Android 開發(fā)者都不了解這個(gè) CSS 的?font-feature-settings?屬性,不過(guò)沒關(guān)系,這個(gè)屬性設(shè)置的都是文字的一些次要特性,所以不用著急了解這個(gè)方法。當(dāng)然有興趣的話也可以看一看哈,文檔在這里。
2.1.10 setTextAlign(Paint.Align align)
設(shè)置文字的對(duì)齊方式。一共有三個(gè)值:LEFT?CETNER?和?RIGHT。默認(rèn)值為?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)
設(shè)置繪制所使用的?Locale。
Locale?直譯是「地域」,其實(shí)就是你在系統(tǒng)里設(shè)置的「語(yǔ)言」或「語(yǔ)言區(qū)域」(具體名稱取決于你用的是什么手機(jī)),比如「簡(jiǎn)體中文(中國(guó))」「English (US)」「English (UK)」。有些同源的語(yǔ)言,在文化發(fā)展過(guò)程中對(duì)一些相同的字衍生出了不同的寫法(比如中國(guó)大陸和日本對(duì)于某些漢字的寫法就有細(xì)微差別。注意,不是繁體和簡(jiǎn)體這種同音同義不同字,而真的是同樣的一個(gè)字有兩種寫法)。系統(tǒng)語(yǔ)言不同,同樣的一個(gè)字的顯示就有可能不同。你可以試一下把自己手機(jī)的語(yǔ)言改成日文,然后打開微信看看聊天記錄,你會(huì)明顯發(fā)現(xiàn)文字的顯示發(fā)生了很多細(xì)微的變化,這就是由于系統(tǒng)的?Locale?改變所導(dǎo)致的。
Canvas?繪制的時(shí)候,默認(rèn)使用的是系統(tǒng)設(shè)置里的?Locale。而通過(guò)?Paint.setTextLocale(Locale locale)?就可以在不改變系統(tǒng)設(shè)置的情況下,直接修改繪制時(shí)的?Locale。
paint.setTextLocale(Locale.CHINA); // 簡(jiǎn)體中文 canvas.drawText(text, 150, 150, paint); paint.setTextLocale(Locale.TAIWAN); // 繁體中文 canvas.drawText(text, 150, 150 + textHeight, paint); paint.setTextLocale(Locale.JAPAN); // 日語(yǔ) canvas.drawText(text, 150, 150 + textHeight * 2, paint);
有意思吧?
另外,由于 Android 7.0 ( API v24) 加入了多語(yǔ)言區(qū)域的支持,所以在 API v24 以及更高版本上,還可以使用?setTextLocales(LocaleList locales)?來(lái)為繪制設(shè)置多個(gè)語(yǔ)言區(qū)域。
2.1.12 setHinting(int mode)
設(shè)置是否啟用字體的 hinting (字體微調(diào))。
現(xiàn)在的 Android 設(shè)備大多數(shù)都是是用的矢量字體。矢量字體的原理是對(duì)每個(gè)字體給出一個(gè)字形的矢量描述,然后使用這一個(gè)矢量來(lái)對(duì)所有的尺寸的字體來(lái)生成對(duì)應(yīng)的字形。由于不必為所有字號(hào)都設(shè)計(jì)它們的字體形狀,所以在字號(hào)較大的時(shí)候,矢量字體也能夠保持字體的圓潤(rùn),這是矢量字體的優(yōu)勢(shì)。不過(guò)當(dāng)文字的尺寸過(guò)小(比如高度小于 16 像素),有些文字會(huì)由于失去過(guò)多細(xì)節(jié)而變得不太好看。 hinting 技術(shù)就是為了解決這種問題的:通過(guò)向字體中加入 hinting 信息,讓矢量字體在尺寸過(guò)小的時(shí)候得到針對(duì)性的修正,從而提高顯示效果。效果圖盜一張維基百科的:
功能很強(qiáng),效果很贊。不過(guò)在現(xiàn)在( 2017 年),手機(jī)屏幕的像素密度已經(jīng)非常高,幾乎不會(huì)再出現(xiàn)字體尺寸小到需要靠 hinting 來(lái)修正的情況,所以這個(gè)方法其實(shí)……沒啥用了。可以忽略。
2.1.13 setElegantTextHeight(boolean elegant)
聲明:這個(gè)方法對(duì)中國(guó)人沒用,不想看的話可以直接跳過(guò),無(wú)毒副作用。
設(shè)置是否開啟文字的 elegant height 。開啟之后,文字的高度就變優(yōu)雅了(誤)。下面解釋一下所謂的 elegant height:
在有些語(yǔ)言中,可能會(huì)出現(xiàn)一些非常高的字形:
左邊那幾個(gè)泰文文字,挺高的吧?但其實(shí)它們已經(jīng)是被壓縮過(guò)了的,它們本來(lái)比這還要高。
這些比較高的文字,通常都有兩個(gè)版本的字體:一個(gè)原始版本,一個(gè)壓縮了高度的版本。壓縮版本可以保證讓這些「大高個(gè)」文字在和普通文字(例如拉丁文字)放在一起的時(shí)候看起來(lái)不會(huì)顯得太奇怪。事實(shí)上,Paint?繪制文字時(shí)是用的默認(rèn)版本就是壓縮版本,就像上圖這樣。
不過(guò)有的時(shí)候,開發(fā)者會(huì)需要使用它們的原始(優(yōu)雅)版本。使用?setElegantTextHeight()?就可以切換到原始版本:
paint.setElegantTextHeight(true);
這字得有多高?2 米 26 ?
那么,setElegantTextHeight()?的作用到這里就很清晰了:
其實(shí)這個(gè)問題我已經(jīng)在 stackoverflow 回答過(guò)一次,原回答在這里。
不過(guò)就像前面說(shuō)的,由于中國(guó)人常用的漢語(yǔ)和英語(yǔ)的文字并不會(huì)達(dá)到這種高度,所以這個(gè)方法對(duì)于中國(guó)人基本上是沒用的。
2.1.14 setSubpixelText(boolean subpixelText)
是否開啟次像素級(jí)的抗鋸齒( sub-pixel anti-aliasing )。
次像素級(jí)抗鋸齒這個(gè)功能解釋起來(lái)很麻煩,簡(jiǎn)單說(shuō)就是根據(jù)程序所運(yùn)行的設(shè)備的屏幕類型,來(lái)進(jìn)行針對(duì)性的次像素級(jí)的抗鋸齒計(jì)算,從而達(dá)到更好的抗鋸齒效果。更詳細(xì)的解釋可以看這篇文章。
不過(guò),和前面講的字體 hinting 一樣,由于現(xiàn)在手機(jī)屏幕像素密度已經(jīng)很高,所以默認(rèn)抗鋸齒效果就已經(jīng)足夠好了,一般沒必要開啟次像素級(jí)抗鋸齒,所以這個(gè)方法基本上沒有必要使用。
2.1.15 setLinearText(boolean linearText)
這個(gè)方法老實(shí)說(shuō)我從沒用過(guò),也始終沒有搞懂它是什么意思,就不強(qiáng)行裝逼了。把文檔中的解釋照搬過(guò)來(lái),各位自己研究吧。
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?的對(duì)文字的顯示效果設(shè)置類方法。下面介紹它的第二類方法:測(cè)量文字尺寸類。
2.2 測(cè)量文字尺寸類
不論是文字,還是圖形或?Bitmap,只有知道了尺寸,才能更好地確定應(yīng)該擺放的位置。由于文字的繪制和圖形或?Bitmap?的繪制比起來(lái),尺寸的計(jì)算復(fù)雜得多,所以它有一整套的方法來(lái)計(jì)算文字尺寸。
2.2.1 float getFontSpacing()
獲取推薦的行距。
即推薦的兩行文字的 baseline 的距離。這個(gè)值是系統(tǒng)根據(jù)文字的字體和字號(hào)自動(dòng)計(jì)算的。它的作用是當(dāng)你要手動(dòng)繪制多行文字(而不是使用 StaticLayout)的時(shí)候,可以在換行的時(shí)候給?y?坐標(biāo)加上這個(gè)值來(lái)下移文字。
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?是個(gè)相對(duì)專業(yè)的工具類,它提供了幾個(gè)文字排印方面的數(shù)值:ascent,?descent,?top,?bottom,?leading。
如圖,圖中有兩行文字,每一行都有 5 條線:top,?ascent,?baseline,?descent,?bottom。(leading并沒有畫出來(lái),因?yàn)楫嫴怀鰜?lái),下面會(huì)給出解釋)
-
baseline: 上圖中黑色的線。前面已經(jīng)講過(guò)了,它的作用是作為文字顯示的基準(zhǔn)線。
-
ascent?/?descent: 上圖中綠色和橙色的線,它們的作用是限制普通字符的頂部和底部范圍。?
普通的字符,上不會(huì)高過(guò)?ascent?,下不會(huì)低過(guò)?descent?,例如上圖中大部分的字形都顯示在?ascent?和?descent?兩條線的范圍內(nèi)。具體到 Android 的繪制中,?ascent?的值是圖中綠線和?baseline?的相對(duì)位移,它的值為負(fù)(因?yàn)樗?baseline?的上方);?descent?的值是圖中橙線和?baseline?相對(duì)位移,值為正(因?yàn)樗?baseline?的下方)。 -
top?/?bottom: 上圖中藍(lán)色和紅色的線,它們的作用是限制所有字形( glyph )的頂部和底部范圍。?
除了普通字符,有些字形的顯示范圍是會(huì)超過(guò)?ascent?和?descent?的,而?top?和?bottom?則限制的是所有字形的顯示范圍,包括這些特殊字形。例如上圖的第二行文字里,就有兩個(gè)泰文的字形分別超過(guò)了?ascent?和?descent?的限制,但它們都在?top?和?bottom?兩條線的范圍內(nèi)。具體到 Android 的繪制中,?top?的值是圖中藍(lán)線和?baseline?的相對(duì)位移,它的值為負(fù)(因?yàn)樗?baseline?的上方);?bottom?的值是圖中紅線和?baseline?相對(duì)位移,值為正(因?yàn)樗?baseline?的下方)。 -
leading: 這個(gè)詞在上圖中沒有標(biāo)記出來(lái),因?yàn)樗⒉皇侵傅哪硹l線和?baseline?的相對(duì)位移。?leading?指的是行的額外間距,即對(duì)于上下相鄰的兩行,上行的?bottom?線和下行的?top?線的距離,也就是上圖中第一行的紅線和第二行的藍(lán)線的距離(對(duì),就是那個(gè)小細(xì)縫)。
leading?這個(gè)詞的本意其實(shí)并不是行的額外間距,而是行距,即兩個(gè)相鄰行的?baseline?之間的距離。不過(guò)對(duì)于很多非專業(yè)領(lǐng)域,leading?的意思被改變了,被大家當(dāng)做行的額外間距來(lái)用;而 Android 里的?leading?,同樣也是行的額外間距的意思。
另外,leading?在這里應(yīng)該讀作 "ledding" 而不是 "leeding" 哦。原因就不說(shuō)了,我這越扯越遠(yuǎn)沒邊了。
FontMetrics?提供的就是?Paint?根據(jù)當(dāng)前字體和字號(hào),得出的這些值的推薦值。它把這些值以變量的形式存儲(chǔ),供開發(fā)者需要時(shí)使用。
- FontMetrics.ascent:float 類型。
- FontMetrics.descent:float 類型。
- FontMetrics.top:float 類型。
- FontMetrics.bottom:float 類型。
- FontMetrics.leading:float 類型。
另外,ascent?和?descent?這兩個(gè)值還可以通過(guò)?Paint.ascent()?和?Paint.descent()?來(lái)快捷獲取。
FontMetrics 和 getFontSpacing():
從定義可以看出,上圖中兩行文字的 font spacing (即相鄰兩行的?baseline?的距離) 可以通過(guò)?bottom - top + leading?(top?的值為負(fù),前面剛說(shuō)過(guò),記得吧?)來(lái)計(jì)算得出。
但你真的運(yùn)行一下會(huì)發(fā)現(xiàn),?bottom - top + leading?的結(jié)果是要大于?getFontSpacing()?的返回值的。
兩個(gè)方法計(jì)算得出的 font spacing 竟然不一樣?
這并不是 bug,而是因?yàn)?getFontSpacing()?的結(jié)果并不是通過(guò)?FontMetrics?的標(biāo)準(zhǔn)值計(jì)算出來(lái)的,而是另外計(jì)算出來(lái)的一個(gè)值,它能夠做到在兩行文字不顯得擁擠的前提下縮短行距,以此來(lái)得到更好的顯示效果。所以如果你要對(duì)文字手動(dòng)換行繪制,多數(shù)時(shí)候應(yīng)該選取?getFontSpacing()?來(lái)得到行距,不但使用更簡(jiǎn)單,顯示效果也會(huì)更好。
getFontMetrics()?的返回值是?FontMetrics?類型。它還有一個(gè)重載方法?getFontMetrics(FontMetrics fontMetrics)?,計(jì)算結(jié)果會(huì)直接填進(jìn)傳入的?FontMetrics?對(duì)象,而不是重新創(chuàng)建一個(gè)對(duì)象。這種用法在需要頻繁獲取?FontMetrics?的時(shí)候性能會(huì)好些。
另外,這兩個(gè)方法還有一對(duì)同樣結(jié)構(gòu)的對(duì)應(yīng)的方法?getFontMetricsInt()?和?getFontMetricsInt(FontMetricsInt fontMetrics)?,用于獲取?FontMetricsInt?類型的結(jié)果。
2.2.3 getTextBounds(String text, int start, int end, Rect bounds)
獲取文字的顯示范圍。
參數(shù)里,text?是要測(cè)量的文字,start?和?end?分別是文字的起始和結(jié)束位置,bounds?是存儲(chǔ)文字顯示范圍的對(duì)象,方法在測(cè)算完成之后會(huì)把結(jié)果寫進(jìn)?bounds。
paint.setElegantTextHeight(true);
它有一個(gè)重載方法?getTextBounds(char[] text, int index, int count, Rect bounds),用法非常相似,不再介紹。
2.2.4 float measureText(String text)
測(cè)量文字的寬度并返回。
canvas.drawText(text, offsetX, offsetY, paint); float textWidth = paint.measureText(text); canvas.drawLine(offsetX, offsetY, offsetX + textWidth, offsetY, paint);
咦,前面有了?getTextBounds(),這里怎么又有一個(gè)?measureText()?
如果你用代碼分別使用?getTextBounds()?和?measureText()?來(lái)測(cè)量文字的寬度,你會(huì)發(fā)現(xiàn)?measureText()?測(cè)出來(lái)的寬度總是比?getTextBounds()?大一點(diǎn)點(diǎn)。這是因?yàn)檫@兩個(gè)方法其實(shí)測(cè)量的是兩個(gè)不一樣的東西。
-
getTextBounds: 它測(cè)量的是文字的顯示范圍(關(guān)鍵詞:顯示)。形象點(diǎn)來(lái)說(shuō),你這段文字外放置一個(gè)可變的矩形,然后把矩形盡可能地縮小,一直小到這個(gè)矩形恰好緊緊包裹住文字,那么這個(gè)矩形的范圍,就是這段文字的 bounds。
-
measureText(): 它測(cè)量的是文字繪制時(shí)所占用的寬度(關(guān)鍵詞:占用)。前面已經(jīng)講過(guò),一個(gè)文字在界面中,往往需要占用比他的實(shí)際顯示寬度更多一點(diǎn)的寬度,以此來(lái)讓文字和文字之間保留一些間距,不會(huì)顯得過(guò)于擁擠。上面的這幅圖,我并沒有設(shè)置?setLetterSpacing()?,這里的 letter spacing 是默認(rèn)值 0,但你可以看到,圖中每?jī)蓚€(gè)字母之間都是有空隙的。另外,下方那條用于表示文字寬度的橫線,在左邊超出了第一個(gè)字母?H?一段距離的,在右邊也超出了最后一個(gè)字母?r(雖然右邊這里用肉眼不太容易分辨),而就是兩邊的這兩個(gè)「超出」,導(dǎo)致了?measureText()?比?getTextBounds()?測(cè)量出的寬度要大一些。
在實(shí)際的開發(fā)中,測(cè)量寬度要用?measureText()?還是?getTextBounds()?,需要根據(jù)情況而定。不過(guò)你只要掌握了上面我所說(shuō)的它們的本質(zhì),在選擇的時(shí)候就不會(huì)為難和疑惑了。
measureText(String text)?也有幾個(gè)重載方法,用法和它大同小異,不再介紹。
2.2.5 getTextWidths(String text, float[] widths)
獲取字符串中每個(gè)字符的寬度,并把結(jié)果填入?yún)?shù)?widths。
這相當(dāng)于?measureText()?的一個(gè)快捷方法,它的計(jì)算等價(jià)于對(duì)字符串中的每個(gè)字符分別調(diào)用?measureText()?,并把它們的計(jì)算結(jié)果分別填入?widths?的不同元素。
getTextWidths()?同樣也有好幾個(gè)變種,使用大同小異,不再介紹。
2.2.6 int breakText(String text, boolean measureForwards, float maxWidth, float[] measuredWidth)
這個(gè)方法也是用來(lái)測(cè)量文字寬度的。但和?measureText()?的區(qū)別是,?breakText()?是在給出寬度上限的前提下測(cè)量文字的寬度。如果文字的寬度超出了上限,那么在臨近超限的位置截?cái)辔淖帧?/span>
int measuredCount; float[] measuredWidth = {0};// 寬度上限 300 (不夠用,截?cái)?#xff09; measuredCount = paint.breakText(text, 0, text.length(), true, 300, measuredWidth); canvas.drawText(text, 0, measuredCount, 150, 150, paint);// 寬度上限 400 (不夠用,截?cái)?#xff09; 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()?的返回值是截取的文字個(gè)數(shù)(如果寬度沒有超限,則是文字的總個(gè)數(shù))。參數(shù)中,?text?是要測(cè)量的文字;measureForwards?表示文字的測(cè)量方向,true?表示由左往右測(cè)量;maxWidth?是給出的寬度上限;measuredWidth?是用于接受數(shù)據(jù),而不是用于提供數(shù)據(jù)的:方法測(cè)量完成后會(huì)把截取的文字寬度(如果寬度沒有超限,則為文字總寬度)賦值給?measuredWidth[0]。
這個(gè)方法可以用于多行文字的折行計(jì)算。
breakText()?也有幾個(gè)重載方法,使用大同小異,不再介紹。
2.2.7 光標(biāo)相關(guān)
對(duì)于?EditText?以及類似的場(chǎng)景,會(huì)需要繪制光標(biāo)。光標(biāo)的計(jì)算很麻煩,不過(guò) API 23 引入了兩個(gè)新的方法,有了這兩個(gè)方法后,計(jì)算光標(biāo)就方便了很多。
2.2.7.1 getRunAdvance(CharSequence text, int start, int end, int contextStart, int contextEnd, boolean isRtl, int offset)
對(duì)于一段文字,計(jì)算出某個(gè)字符處光標(biāo)的?x?坐標(biāo)。?start?end?是文字的起始和結(jié)束坐標(biāo);contextStart?contextEnd?是上下文的起始和結(jié)束坐標(biāo);isRtl?是文字的方向;offset?是字?jǐn)?shù)的偏移,即計(jì)算第幾個(gè)字符處的光標(biāo)。
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);
其實(shí),說(shuō)是測(cè)量光標(biāo)位置的,本質(zhì)上這也是一個(gè)測(cè)量文字寬度的方法。上面這個(gè)例子中,start?和?contextStart?都是 0,?end?contextEnd?和?offset?都等于?text.length()。在這種情況下,它是等價(jià)于?measureText(text)?的,即完整測(cè)量一段文字的寬度。而對(duì)于更復(fù)雜的需求,getRunAdvance()?能做的事就比?measureText()?多了。
// 包含特殊符號(hào)的繪制(如 emoji 表情) String text = "Hello HenCoder \uD83C\uDDE8\uD83C\uDDF3" // "Hello HenCoder總結(jié)
以上是生活随笔為你收集整理的HenCoder Android 开发进阶:自定义 View 1-3 drawText() 文字的绘制的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: pat 1037
- 下一篇: Android 分享功能大全