自定義View drawText()繪制文字

1.Canvas 繪制文字方式

Canvas 繪制文字的方式:drawText() drawTextRun() drawTextOnPath()

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

注:如果你從(0,0)點(diǎn)開(kāi)始繪制Text,文字不會(huì)顯示在View左上角,會(huì)顯示在View的左上方。


image.png

這張圖,電線上的小鳥(niǎo),這里,電線就類似于文字的基線。


image.png

盜圖:Hencoder

canvas.drawText()中,參數(shù)y,是指的文字的基線(baseLine)。參數(shù)x,也不是文字最左面的點(diǎn),x的位置是文字起點(diǎn)靠左一點(diǎn)(這里是因?yàn)閷?duì)于絕大多數(shù)字符,他們的寬度都要略微大于實(shí)際顯示的寬度,字符的左右兩邊都會(huì)留出一部分間隙,用于文字間的間隔,因此我們?cè)O(shè)定繪制文字起點(diǎn)的時(shí)候 ,會(huì)發(fā)現(xiàn)實(shí)際文字繪制時(shí)候會(huì)靠右一點(diǎn))。

1.2 drawTextRun() (這個(gè)在中國(guó)應(yīng)該用不到)

類 似于drawText() 但是增加了兩個(gè)設(shè)置----上下文---文字方向

drawTextRun(CharSequence text, int start, int end, int contextStart, int contextEnd, float x, float y, boolean isRtl, Paint paint)

參數(shù):
text:要繪制的文字
start:從那個(gè)字開(kāi)始繪制
end:繪制到哪個(gè)字結(jié)束
contextStart:上下文的起始位置。contextStart 需要小于等于 start
contextEnd:上下文的結(jié)束位置。contextEnd 需要大于等于 end
x:文字左邊的坐標(biāo)
y:文字的基線坐標(biāo)
isRtl:是否是 RTL(Right-To-Left,從右向左)

1.3 drawTextOnPath()

沿著一條Path來(lái)繪制文字。
例:


image.png
paint.setTextSize(60);
paint.setColor(Color.RED);
mRectF = new RectF(100,100,800,500);
mPath.addOval(mRectF,Path.Direction.CW);
    
canvas.drawTextOnPath(text,mPath,1,0,paint);

drawTextOnPath(String text, Path path, float hOffset, float vOffset, Paint paint)

參數(shù)
hOffset:文字相對(duì)于 Path 的水平偏移量
vOffset:文字相對(duì)于 Path 的豎直偏移量
利用它們可以調(diào)整文字的位置。例如你設(shè)置 hOffset 為 5, vOffset 為 10,文字就會(huì)右移 5 像素和下移 10 像素。

1.4 staticLayout

staticLayout 是用canvas來(lái)繪制,但是不是canvas的方法。staticLayout一般用于繪制多行textView。如果你需要進(jìn)行多行文字的繪制,并且對(duì)文字的排列和樣式?jīng)]有太復(fù)雜的花式要求,那么使用staticLayout最好。

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

例:


image.png
    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(); 

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)越界。

2.Paint 對(duì)文字繪制的輔助

2.1 設(shè)置顯示效果類

2.1.1 setTextSize(float textSize)

設(shè)置文字大小

2.1.2 setTypeface(Typeface typeface)

設(shè)置字體,字體可以是系統(tǒng)所有的字體,也可以是自己的字體。官方文檔

這里的Typeface跟font是一個(gè)意思,都表示字體,但是,typerface指的是某套字體(font family),而font值指的是typeface具體的某個(gè)weight和size的分支。

2.1.3 setFakeBoldText(boolean fakeBoldText)

是否使用偽粗體。


image.png

這種粗體叫做偽粗體,因?yàn)樗皇窃O(shè)置更高的weight的字體讓文字變粗,他是通過(guò)程序在運(yùn)行時(shí)候?qū)⒆煮w描粗。

2.1.4 setStrikeThurText(boolean strikeThruText)

是否添加刪除線


image.png
2.1.5 setUnderlineText(boolean underlineText)

是否添加下劃線


image.png

#######2.1.6 setTextSkewX(float skewX)
設(shè)置文字的錯(cuò)切角度,通俗的說(shuō),就是設(shè)置文字傾斜


image.png
2.1.7 setTextScaleX(float scaleX)

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


image.png
    paint.setTextScaleX(0.5f);
    canvas.drawText(text, 50, 100, paint);
    paint.setTextScaleX(1.0f);
    canvas.drawText(text, 50, 150, paint);
    paint.setTextScaleX(1.5f);
    canvas.drawText(text, 50, 200, paint);
2.1.8 setLetterSpacing(float letterSpacing)

設(shè)置字符間距。默認(rèn)為0。
注:setLetterSpacing為字符間距,setTextScaleX為文字橫向?qū)挾取?/p>

2.1.9 setFontFeatureSettrings(String settings)

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

2.1.10 setTextAlign(Paint.Align align)

設(shè)置文字對(duì)其方式(LEFFT,CETNER,RIGHT 左中右,默認(rèn)為左 既LEFT)。


image.png
2.1.11setTextLocale(Locale locale)/setTextLocales(LocaleList locales)

設(shè)置繪制所使用的Locale,就是設(shè)置不同地域的語(yǔ)言(是漢語(yǔ)還是英語(yǔ))

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ù)就是為了解決這種問(wèn)題的:通過(guò)向字體中加入 hinting 信息,讓矢量字體在尺寸過(guò)小的時(shí)候得到針對(duì)性的修正,從而提高顯示效果。效果圖盜一張維基百科的:


image.png

對(duì)于現(xiàn)在的手機(jī)(屏幕密度非常高),幾乎不會(huì)出現(xiàn)字體尺寸小道需要修改hinting來(lái)修正的情況,所以這個(gè)方法,現(xiàn)在幾乎不會(huì)用到。

2.1.13 setElegantTextHeight(boolean elegant)

設(shè)置是否開(kāi)啟文字的elegant height。
這個(gè)方法適用于有較高文字的語(yǔ)言。
把「大高個(gè)」文字的高度恢復(fù)為原始高度;
增大每行文字的上下邊界,來(lái)容納被加高了的文字。
中文不需要!?。?/p>

2.1.14 setSubpixelText(boolean subpixeText)

是否開(kāi)啟像素級(jí)抗鋸齒
詳細(xì)介紹

2.1.15 setLinearText(boolean linearText)

2.2 測(cè)量文字尺寸類

文字也有他自己的尺寸。
########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)下移文字。

2.2.2 FontMetrics getFontMetrice()

獲取Paint的FontMetrics

盜圖:henCoder


image.png

如圖,圖中有兩行文字,每一行都有 5 條線:top, ascent, baseline, descent, bottom。

? baseline: 上圖中黑色的線。它的作用是作為文字顯示的基準(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è)詞在上圖中沒(méi)有標(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" 。

FontMetrics 提供的就是 Paint 根據(jù)當(dāng)前字體和字號(hào),得出的這些值的推薦值。它把這些值以變量的形式存儲(chǔ),供開(kāi)發(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ì)算得出。

實(shí)際 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é)果。

推薦:計(jì)算基線

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

獲取文字的顯示范圍,text為要測(cè)量的文字,start為文字起始的位置,end為文字介素的位子,bounds存儲(chǔ)文字顯示范圍的對(duì)象,在測(cè)量結(jié)束以后會(huì)將結(jié)果寫(xiě)入bounds.

(他還有一個(gè)重載方法 getTextBounds(char[] text, int index, int count, Rect 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);
image.png
2.2.4 float measure(String text)

測(cè)量文字寬度并返回

注:measure(String text) 測(cè)量后的寬度比getTextBounds()測(cè)量后的值大一點(diǎn)
? 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ò)于擁擠。上面的這幅圖,我并沒(méi)有設(shè)置 setLetterSpacing() ,這里的 letter spacing 是默認(rèn)值 0,但你可以看到,圖中每?jī)蓚€(gè)字母之間都是有空隙的。另外,下方那條用于表示文字寬度的橫線,在左邊超出了第一個(gè)字母 H 一段距離的,在右邊也超出了最后一個(gè)字母 r(雖然右邊這里用肉眼不太容易分辨),而就是兩邊的這兩個(gè)「超出」,導(dǎo)致了 measureText() 比 getTextBounds() 測(cè)量出的寬度要大一些。

#######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 的不同元素。

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

這個(gè)方法也是用來(lái)測(cè)量文字寬度的。但和 measureText() 的區(qū)別是, breakText() 是在給出寬度上限的前提下測(cè)量文字的寬度。如果文字的寬度超出了上限,那么在臨近超限的位置截?cái)辔淖帧?br> 注:是在給出的寬度范圍下測(cè)量文字寬度,如果文字寬度超出上限,在臨近超限位置截?cái)辔淖帧?br> breakText的返回值是截取的文字個(gè)數(shù)。常用語(yǔ)多行文字的折行計(jì)算。

2.2.7 光標(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); 
image.png

其實(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 ????"
        
        ...
        
        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.png

如上圖,???? 雖然占了 4 個(gè)字符(\uD83C\uDDE8\uD83C\uDDF3),但當(dāng) offset 是表情中間處時(shí), getRunAdvance() 得出的結(jié)果并不會(huì)在表情的中間處。

2.2.7.2 getOffsetForAdvance(CharSequence text, int start, int end, int contextStart, int contextEnd, boolean isRtl, float advance)
給出一個(gè)位置的像素值,計(jì)算出文字中最接近這個(gè)位置的字符偏移量(即第幾個(gè)字符最接近這個(gè)坐標(biāo))。

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

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

#######2.2.8 hasGlyph(String string)
檢查指定的字符串中是否是一個(gè)單獨(dú)的字形 (glyph)。最簡(jiǎn)單的情況是,string 只有一個(gè)字母(比如 a)。

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

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

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