Android自定義View(三) -- drawText()

前面學習了
Android自定義View(一) -- 初識
Android自定義View(二) -- Paint詳解

今天繼續(xù)學習第三篇內(nèi)容drawText(),本文是對第二篇文章中drawText的拓展,進行詳細學習


本文計劃根據(jù)HenCoder系列文章進行學習,所以代碼風格及博文素材可能會摘自其中。


1.Canvas 繪制文字的方式

Canvas 的文字繪制方法有三個:drawText() drawTextRun() 和 drawTextOnPath()。

1.1 drawText(String text, float x, float y, Paint paint)

drawText() 是 Canvas 最基本的繪制文字的方法:給出文字的內(nèi)容和位置, Canvas 按要求去繪制文字。

        paint = new Paint();
        paint.setColor(Color.GRAY);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(5);
        paint.setTextSize(200);
        .
        if (paint == null || TextUtils.isEmpty(this.charSequence)){
            return;
        }
        canvas.drawText(charSequence.toString(),20,200,paint);
drawText

方法的參數(shù)很簡單: text 是文字內(nèi)容,x 和 y 是文字的坐標。但需要注意:這個坐標并不是文字的左上角,而是一個與左下角比較接近的位置。大概在這里:

x.y坐標

如果像繪制其他內(nèi)容一樣,設(shè)置(0,0),文字不會顯示在屏幕左上角,直接顯示在屏幕外,無法觀察到。

附上一張圖,應(yīng)該能更清楚地表達:

image

這是為什么?為什么其它的 Canvas.drawXXX() 方法,都是以左上角作為基準點的,而 drawText() 卻是文字左下方?

先別覺得日了狗,這種設(shè)計其實是有道理的。drawText() 參數(shù)中的 y ,指的是文字的基線( baseline ) 的位置。也就是這條線:

image

眾所周知,不同的語言和文字,每個字符的高度和上下位置都是不一樣的。要讓不同的文字并排顯示的時候整體看起來穩(wěn)當,需要讓它們上下對齊。但這個對齊的方式,不能是簡單的「底部對齊」或「頂部對齊」或「中間對齊」,而應(yīng)該是一種類似于「重心對齊」的方式。就像電線上的小鳥一樣:

image

每只小鳥的最高點和最低點都不一樣,但畫面很平衡

而這個用來讓所有文字互相對齊的基準線,就是基線( baseline )。 drawText() 方法參數(shù)中的 y 值,就是指定的基線的位置。

說完 y 值,再說說 x 值。從前面圖中的標記可以看出來,「Hello HenCoder」繪制出來之后的 x 點并不是字母 "H" 左邊的位置,而是比它的左邊再往左一點點。那么這個「往左的一點點」是什么呢?

它是字母 "H" 的左邊的空隙。絕大多數(shù)的字符,它們的寬度都是要略微大于實際顯示的寬度的。字符的左右兩邊會留出一部分空隙,用于文字之間的間隔,以及文字和邊框的間隔。就像這樣:

image

用豎線標記出邊界后的文字。

所以,明白為什么 x 坐標在 "H" 的左邊再往左一點點的位置,而不是緊貼著 "H" 的左邊線了嗎?就是因為 "H" 的這個留出的空隙。

除了 drawText(text, x, y, paint) 之外, drawText() 還有幾個重載方法,使用方式跟這個都差不多。

1.2 drawTextRun()

這部分對中英文沒有作用,對一些特殊文字有作用,如果需要了解,參考原文中講解HenCoder-drawText

1.3 drawTextOnPath()

沿著一條 Path 來繪制文字。這是一個耍雜技的方法。

path.addRect(200,300,600,800,Path.Direction.CW);
 canvas.drawTextOnPath(charSequence.toString(),path,0,0,paint);
image.png

drawTextOnPath(String text, Path path, float hOffset, float vOffset, Paint paint)
參數(shù)里,需要解釋的只有兩個: hOffset 和 vOffset。它們是文字相對于 Path 的水平偏移量和豎直偏移量,利用它們可以調(diào)整文字的位置。例如你設(shè)置 hOffset 為 5, vOffset 為 10,文字就會右移 5 像素和下移 10 像素。

1.4 StaticLayout

額外講一個 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);

image

到了 View 的邊緣處,文字繼續(xù)向后繪制到看不見的地方,而不是自動換行

  • 不能在換行符 \n 處換行
  String text = "a\nbc\ndefghi\njklm\nnopqrst\nuvwx\nyz";
  ...
  canvas.drawText(text, 50, 100, paint);
image

在換行符 \n 的位置并沒有換行,而只是加了個空格

如果需要繪制多行的文字,你必須自行把文字切斷后分多次使用 drawText() 來繪制,或者——使用 StaticLayout 。

StaticLayout 并不是一個 View 或者 ViewGroup ,而是 android.text.Layout的子類,它是純粹用來繪制文字的。 StaticLayout 支持換行,它既可以為文字設(shè)置寬度上限來讓文字自動換行,也會在 \n 處主動換行。

image.png

StaticLayout 的構(gòu)造方法是 StaticLayout(CharSequence source, TextPaint paint, int width, Layout.Alignment align, float spacingmult, float spacingadd, boolean includepad),其中參數(shù)里:

width 是文字區(qū)域的寬度,文字到達這個寬度后就會自動換行;
align 是文字的對齊方向;
spacingmult 是行間距的倍數(shù),通常情況下填 1 就好;
spacingadd 是行間距的額外增加值,通常情況下填 0 就好;
includeadd 是指是否在文字上下添加額外的空間,來避免某些過高的字符的繪制出現(xiàn)越界。

如果你需要進行多行文字的繪制,并且對文字的排列和樣式?jīng)]有太復(fù)雜的花式要求,那么使用 StaticLayout 就好。

2 Paint 對文字繪制的輔助

Paint 對文字繪制的輔助,有兩類方法:設(shè)置顯示效果的和測量文字尺寸的。

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

image.png

很簡單,不再詳細解釋。

2.1.2 setTypeface(Typeface typeface)

設(shè)置字體。

        paint.setTypeface(Typeface.MONOSPACE);
        canvas.drawText(charSequence.toString(),20,200,paint);
        paint.setTypeface(Typeface.SANS_SERIF);
        canvas.drawText(charSequence.toString(),20,600,paint);
        paint.setTypeface(Typeface.createFromAsset(this.getContext().getAssets(),"Roboto-Regular.ttf"));
        canvas.drawText(charSequence.toString(),20,1000,paint);
image.png

設(shè)置不同的 Typeface 就可以顯示不同的字體。我們中國人談到「字體」,比較熟悉的詞是 font, typeface 和 font 是一個意思,都表示字體。 Typeface 這個類的具體用法,需要了解的話可以直接看文檔,很簡單。

嚴格地說,其實 typeface 和 font 意思不完全一樣。typeface 指的是某套字體(即 font family ),而 font 指的是一個 typeface 具體的某個 weight 和 size 的分支。不過無所謂啦~做人最緊要系開心啦。

2.1.3 setFakeBoldText(boolean fakeBoldText)

是否使用偽粗體。

        canvas.drawText(charSequence.toString(),20,200,paint);
        paint.setFakeBoldText(true);
        canvas.drawText(charSequence.toString(),20,600,paint);
image.png

之所以叫偽粗體( fake bold ),因為它并不是通過選用更高 weight 的字體讓文字變粗,而是通過程序在運行時把文字給「描粗」了。

2.1.4 setStrikeThruText(boolean strikeThruText)

是否加刪除線。

        canvas.drawText(charSequence.toString(),20,200,paint);
        paint.setStrikeThruText(true);
        canvas.drawText(charSequence.toString(),20,600,paint);
image.png

2.1.5 setUnderlineText(boolean underlineText)

是否加下劃線。

        canvas.drawText(charSequence.toString(),20,200,paint);
        paint.setUnderlineText(true);
        canvas.drawText(charSequence.toString(),20,600,paint);
image.png

2.1.6 setTextSkewX(float skewX)

設(shè)置文字橫向錯切角度。其實就是文字傾斜度的啦。

        canvas.drawText(charSequence.toString(),20,200,paint);
        paint.setTextSkewX(-0.4f);
        canvas.drawText(charSequence.toString(),20,600,paint);
image.png

2.1.7 setTextScaleX(float scaleX)

設(shè)置文字橫向放縮。也就是文字變胖變瘦。

        canvas.drawText(charSequence.toString(),20,200,paint);
        paint.setTextScaleX(0.8f);
        canvas.drawText(charSequence.toString(),20,600,paint);
image.png

2.1.8 setLetterSpacing(float letterSpacing)

設(shè)置字符間距。默認值是 0。

        canvas.drawText(charSequence.toString(),20,200,paint);
        paint.setLetterSpacing(0.8f);
        canvas.drawText(charSequence.toString(),20,600,paint);
image.png

為什么在默認的字符間距為 0 的情況下,字符和字符之間也沒有緊緊貼著,這個我在前面講 Canvas.drawText() 的 x 參數(shù)的時候已經(jīng)說過了,在這里應(yīng)該沒有疑問吧?

2.1.9 setFontFeatureSettings(String settings)

用 CSS 的 font-feature-settings 的方式來設(shè)置文字。

        canvas.drawText(charSequence.toString(),20,200,paint);
        paint.setFontFeatureSettings("smcp");
        canvas.drawText(charSequence.toString(),20,600,paint);
image.png

CSS 全稱是 Cascading Style Sheets ,是網(wǎng)頁開發(fā)用來設(shè)置頁面各種元素的樣式的。咦,網(wǎng)頁開發(fā)的設(shè)置怎么會出現(xiàn)在 Android 的 API 里?

image

大多數(shù) Android 開發(fā)者都不了解這個 CSS 的 font-feature-settings 屬性,不過沒關(guān)系,這個屬性設(shè)置的都是文字的一些次要特性,所以不用著急了解這個方法。當然有興趣的話也可以看一看哈,文檔在這里。

2.1.10 setTextAlign(Paint.Align align)

設(shè)置文字的對齊方式。一共有三個值:LEFT CETNER 和 RIGHT。默認值為 LEFT。

        canvas.drawText(charSequence.toString(),20,200,paint);
        paint.setTextAlign(Paint.Align.RIGHT);
        canvas.drawText(charSequence.toString(),20,600,paint);

2.1.11 setTextLocale(Locale locale) / setTextLocales(LocaleList locales)

設(shè)置繪制所使用的 Locale。

Locale 直譯是「地域」,其實就是你在系統(tǒng)里設(shè)置的「語言」或「語言區(qū)域」(具體名稱取決于你用的是什么手機),比如「簡體中文(中國)」「English (US)」「English (UK)」。有些同源的語言,在文化發(fā)展過程中對一些相同的字衍生出了不同的寫法(比如中國大陸和日本對于某些漢字的寫法就有細微差別。注意,不是繁體和簡體這種同音同義不同字,而真的是同樣的一個字有兩種寫法)。系統(tǒng)語言不同,同樣的一個字的顯示就有可能不同。你可以試一下把自己手機的語言改成日文,然后打開微信看看聊天記錄,你會明顯發(fā)現(xiàn)文字的顯示發(fā)生了很多細微的變化,這就是由于系統(tǒng)的 Locale 改變所導(dǎo)致的。

Canvas 繪制的時候,默認使用的是系統(tǒng)設(shè)置里的 Locale。而通過 Paint.setTextLocale(Locale locale) 就可以在不改變系統(tǒng)設(shè)置的情況下,直接修改繪制時的 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);  

image

有意思吧?

另外,由于 Android 7.0 ( API v24) 加入了多語言區(qū)域的支持,所以在 API v24 以及更高版本上,還可以使用 setTextLocales(LocaleList locales) 來為繪制設(shè)置多個語言區(qū)域。

2.1.12 setHinting(int mode)

設(shè)置是否啟用字體的 hinting (字體微調(diào))。

現(xiàn)在的 Android 設(shè)備大多數(shù)都是是用的矢量字體。矢量字體的原理是對每個字體給出一個字形的矢量描述,然后使用這一個矢量來對所有的尺寸的字體來生成對應(yīng)的字形。由于不必為所有字號都設(shè)計它們的字體形狀,所以在字號較大的時候,矢量字體也能夠保持字體的圓潤,這是矢量字體的優(yōu)勢。不過當文字的尺寸過?。ū热绺叨刃∮?16 像素),有些文字會由于失去過多細節(jié)而變得不太好看。 hinting 技術(shù)就是為了解決這種問題的:通過向字體中加入 hinting 信息,讓矢量字體在尺寸過小的時候得到針對性的修正,從而提高顯示效果。效果圖盜一張維基百科的:

image.png

功能很強,效果很贊。不過在現(xiàn)在( 2017 年),手機屏幕的像素密度已經(jīng)非常高,幾乎不會再出現(xiàn)字體尺寸小到需要靠 hinting 來修正的情況,所以這個方法其實……沒啥用了??梢院雎?。

2.1.13 setElegantTextHeight(boolean elegant)

聲明:這個方法對中國人沒用,依然跳過,如果想了解,請去原博文了解

2.1.14 setSubpixelText(boolean subpixelText)

是否開啟次像素級的抗鋸齒( sub-pixel anti-aliasing )。

次像素級抗鋸齒這個功能解釋起來很麻煩,簡單說就是根據(jù)程序所運行的設(shè)備的屏幕類型,來進行針對性的次像素級的抗鋸齒計算,從而達到更好的抗鋸齒效果。更詳細的解釋可以看這篇文章。

不過,和前面講的字體 hinting 一樣,由于現(xiàn)在手機屏幕像素密度已經(jīng)很高,所以默認抗鋸齒效果就已經(jīng)足夠好了,一般沒必要開啟次像素級抗鋸齒,所以這個方法基本上沒有必要使用。

2.1.15 setLinearText (boolean linearText)

設(shè)置是否打開線性文本標識,這玩意對大多數(shù)人來說都很奇怪不知道這玩意什么意思。想要明白這東西你要先知道文本在Android中是如何進行存儲和計算的。在Android中文本的繪制需要使用一個bitmap作為單個字符的緩存,既然是緩存必定要使用一定的空間,我們可以通過setLinearText (true)告訴Android我們不需要這樣的文本緩存。

2.2 測量文字尺寸類

不論是文字,還是圖形或 Bitmap,只有知道了尺寸,才能更好地確定應(yīng)該擺放的位置。由于文字的繪制和圖形或 Bitmap 的繪制比起來,尺寸的計算復(fù)雜得多,所以它有一整套的方法來計算文字尺寸。

2.2.1 float getFontSpacing()

獲取推薦的行距。
即推薦的兩行文字的 baseline 的距離。這個值是系統(tǒng)根據(jù)文字的字體和字號自動計算的。它的作用是當你要手動繪制多行文字(而不是使用 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);  
image

2.2.2 FontMetircs getFontMetrics()

獲取 Paint 的 FontMetrics。

FontMetrics 是個相對專業(yè)的工具類,它提供了幾個文字排印方面的數(shù)值:ascent, descent, top, bottom, leading。

image

如圖,圖中有兩行文字,每一行都有 5 條線:top, ascent, baseline, descent, bottom。(leading并沒有畫出來,因為畫不出來,下面會給出解釋)

  • baseline: 上圖中黑色的線。前面已經(jīng)講過了,它的作用是作為文字顯示的基準線。

  • ascent / descent: 上圖中綠色橙色的線,它們的作用是限制普通字符的頂部和底部范圍。
    普通的字符,上不會高過 ascent ,下不會低過 descent ,例如上圖中大部分的字形都顯示在 ascentdescent 兩條線的范圍內(nèi)。具體到 Android 的繪制中, ascent 的值是圖中綠線和 baseline 的相對位移,它的值為負(因為它在 baseline 的上方); descent 的值是圖中橙線和 baseline 相對位移,值為正(因為它在 baseline 的下方)。

  • top / bottom: 上圖中藍色紅色的線,它們的作用是限制所有字形( glyph )的頂部和底部范圍。
    除了普通字符,有些字形的顯示范圍是會超過 ascentdescent 的,而 topbottom 則限制的是所有字形的顯示范圍,包括這些特殊字形。例如上圖的第二行文字里,就有兩個泰文的字形分別超過了 ascentdescent 的限制,但它們都在 topbottom 兩條線的范圍內(nèi)。具體到 Android 的繪制中, top 的值是圖中藍線和 baseline 的相對位移,它的值為負(因為它在 baseline 的上方); bottom 的值是圖中紅線和 baseline 相對位移,值為正(因為它在 baseline 的下方)。

  • leading: 這個詞在上圖中沒有標記出來,因為它并不是指的某條線和 baseline 的相對位移。 leading 指的是行的額外間距,即對于上下相鄰的兩行,上行的 bottom 線和下行的 top 線的距離,也就是上圖中第一行的紅線第二行的藍線的距離(對,就是那個小細縫)。

leading 這個詞的本意其實并不是行的額外間距,而是行距,即兩個相鄰行的 baseline 之間的距離。不過對于很多非專業(yè)領(lǐng)域,leading 的意思被改變了,被大家當做行的額外間距來用;而 Android 里的 leading ,同樣也是行的額外間距的意思。

另外,leading 在這里應(yīng)該讀作 "ledding" 而不是 "leeding" 哦。原因就不說了,我這越扯越遠沒邊了。
FontMetrics 提供的就是 Paint 根據(jù)當前字體和字號,得出的這些值的推薦值。它把這些值以變量的形式存儲,供開發(fā)者需要時使用。

FontMetrics 提供的就是 Paint 根據(jù)當前字體和字號,得出的這些值的推薦值。它把這些值以變量的形式存儲,供開發(fā)者需要時使用。

  • 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 的值為負,前面剛說過,記得吧?)來計算得出。

但你真的運行一下會發(fā)現(xiàn), bottom - top + leading 的結(jié)果是要大于 >getFontSpacing() 的返回值的。

兩個方法計算得出的 font spacing 竟然不一樣?

這并不是 bug,而是因為 getFontSpacing() 的結(jié)果并不是通過 FontMetrics 的標準值計算出來的,而是另外計算出來的一個值,它能夠做到在兩行文字不顯得擁擠的前提下縮短行距,以此來得到更好的顯示效果。所以如果你要對文字手動換行繪制,多數(shù)時候應(yīng)該選取 getFontSpacing() 來得到行距,不但使用更簡單,顯示效果也會更好。

getFontMetrics() 的返回值是 FontMetrics 類型。它還有一個重載方法 getFontMetrics(FontMetrics fontMetrics) ,計算結(jié)果會直接填進傳入的 FontMetrics 對象,而不是重新創(chuàng)建一個對象。這種用法在需要頻繁獲取 FontMetrics 的時候性能會好些。

另外,這兩個方法還有一對同樣結(jié)構(gòu)的對應(yīng)的方法 getFontMetricsInt() 和 getFontMetricsInt(FontMetricsInt fontMetrics) ,用于獲取 FontMetricsInt 類型的結(jié)果。

2.2.3 getTextBounds(String text, int start, int end, Rect bounds)

獲取文字的顯示范圍。

參數(shù)里,text 是要測量的文字,start 和 end 分別是文字的起始和結(jié)束位置,bounds 是存儲文字顯示范圍的對象,方法在測算完成之后會把結(jié)果寫進 bounds。

        canvas.drawText(charSequence.toString(),20,200,paint);
        paint.getTextBounds(charSequence.toString(),0,charSequence.length(),bounds);
        bounds.left += 20;
        bounds.right += 20;
        bounds.top += 200;
        bounds.bottom += 200;
        canvas.drawRect(bounds,paint);
image.png

2.2.4 float measureText(String text)

測量文字的寬度并返回。

        canvas.drawText(charSequence.toString(),20,200,paint);
        float textWidth = paint.measureText(charSequence.toString());
        canvas.drawLine(20,200, textWidth += 20,200,paint);
image.png

咦,前面有了 getTextBounds(),這里怎么又有一個 measureText()?

如果你用代碼分別使用 getTextBounds() 和 measureText() 來測量文字的寬度,你會發(fā)現(xiàn) measureText() 測出來的寬度總是比 getTextBounds() 大一點點。這是因為這兩個方法其實測量的是兩個不一樣的東西。

  • getTextBounds: 它測量的是文字的顯示范圍(關(guān)鍵詞:顯示)。形象點來說,你這段文字外放置一個可變的矩形,然后把矩形盡可能地縮小,一直小到這個矩形恰好緊緊包裹住文字,那么這個矩形的范圍,就是這段文字的 bounds。

  • measureText(): 它測量的是文字繪制時所占用的寬度(關(guān)鍵詞:占用)。前面已經(jīng)講過,一個文字在界面中,往往需要占用比他的實際顯示寬度更多一點的寬度,以此來讓文字和文字之間保留一些間距,不會顯得過于擁擠。上面的這幅圖,我并沒有設(shè)置 setLetterSpacing() ,這里的 letter spacing 是默認值 0,但你可以看到,圖中每兩個字母之間都是有空隙的。另外,下方那條用于表示文字寬度的橫線,在左邊超出了第一個字母 P 一段距離的,在右邊也超出了最后一個數(shù)字1),而就是兩邊的這兩個「超出」,導(dǎo)致了 measureText() 比 getTextBounds() 測量出的寬度要大一些。

在實際的開發(fā)中,測量寬度要用 measureText() 還是 getTextBounds() ,需要根據(jù)情況而定。不過你只要掌握了上面我所說的它們的本質(zhì),在選擇的時候就不會為難和疑惑了。

measureText(String text) 也有幾個重載方法,用法和它大同小異,不再介紹。

2.2.5 getTextWidths(String text, float[] widths)

獲取字符串中每個字符的寬度,并把結(jié)果填入?yún)?shù) widths。

這相當于 measureText() 的一個快捷方法,它的計算等價于對字符串中的每個字符分別調(diào)用 measureText() ,并把它們的計算結(jié)果分別填入 widths 的不同元素。

getTextWidths() 同樣也有好幾個變種,使用大同小異,不再介紹。

2.2.6 int breakText(String text, boolean measureForwards, float maxWidth, float[] measuredWidth)

這個方法也是用來測量文字寬度的。但和 measureText() 的區(qū)別是, 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);  

image

breakText() 的返回值是截取的文字個數(shù)(如果寬度沒有超限,則是文字的總個數(shù))。參數(shù)中, text 是要測量的文字;measureForwards 表示文字的測量方向,true 表示由左往右測量;maxWidth 是給出的寬度上限;measuredWidth 是用于接受數(shù)據(jù),而不是用于提供數(shù)據(jù)的:方法測量完成后會把截取的文字寬度(如果寬度沒有超限,則為文字總寬度)賦值給 measuredWidth[0]

這個方法可以用于多行文字的折行計算。

breakText() 也有幾個重載方法,使用大同小異,不再介紹。

2.2.7 光標相關(guān)

對于 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 是文字的起始和結(jié)束坐標;contextStart contextEnd 是上下文的起始和結(jié)束坐標;isRtl 是文字的方向;offset 是字數(shù)的偏移,即計算第幾個字符處的光標。

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

image

其實,說是測量光標位置的,本質(zhì)上這也是一個測量文字寬度的方法。上面這個例子中,startcontextStart 都是 0, end contextEndoffset 都等于 text.length()。在這種情況下,它是等價于 measureText(text) 的,即完整測量一段文字的寬度。而對于更復(fù)雜的需求,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);

...

image

如上圖,???? 雖然占了 4 個字符(\uD83C\uDDE8\uD83C\uDDF3),但當 offset 是表情中間處時, getRunAdvance() 得出的結(jié)果并不會在表情的中間處。為什么?因為這是用來計算光標的方法啊,光標當然不能出現(xiàn)在符號中間啦。

2.2.7.2 getOffsetForAdvance(CharSequence text, int start, int end, int contextStart, int contextEnd, boolean isRtl, float advance)

給出一個位置的像素值,計算出文字中最接近這個位置的字符偏移量(即第幾個字符最接近這個坐標)。

方法的參數(shù)很簡單: text 是要測量的文字;start end 是文字的起始和結(jié)束坐標;contextStart``contextEnd 是上下文的起始和結(jié)束坐標;isRtl 是文字方向;advance 是給出的位置的像素值。填入?yún)?shù),對應(yīng)的字符偏移量將作為返回值返回。

getOffsetForAdvance() 配合上 getRunAdvance() 一起使用,就可以實現(xiàn)「獲取用戶點擊處的文字坐標」的需求。

2.2.8 hasGlyph(String string)

檢查指定的字符串中是否是一個單獨的字形 (glyph)。最簡單的情況是,string 只有一個字母(比如 a)。

image

以上這些內(nèi)容,就是文字繪制的相關(guān)知識。它們有的常用,有的不常用,有的甚至可以說是在某些情況下沒用,不過你把它們?nèi)扛愣?,在實際的開發(fā)中,就知道哪些事情可以做到,哪些事情做不到,以及應(yīng)該怎么做了。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容