天天看點

自定義View學習3:canvas繪制文字

1canvas繪制文字的方式

canvas的文字繪制方式有三種:drawText(),drawTextRun(),drawTextOnPath()。

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

drawText()是canvas 最基本的繪制方法:給出文字的内容和位置,Canvas按照要求去繪制文字,text是文字内容,x和y是文字的坐标,需要注意的是這個坐标并不是文字的左上角,而是一個與左下角比較接近的位置

drawTexRun()
           

drawTextRun() 是在 API 23 新加入的方法。它和 drawText() 一樣都是繪制文字,但加入了兩項額外的設定——上下文和文字方向——用于輔助一些文字結構比較特殊的語言的繪制。

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

沿着一條path路徑來繪制文字。這個方法可以用來繪制豎向文字,斜向文字等等參數:hOffset 和 vOffset。它們是文字相對于 Path 的水準偏移量和豎直偏移量,利用它們可以調整文字的位置。例如你設定 hOffset 為 5, vOffset 為 10,文字就會右移 5 像素和下移 10 像素。

2.StaticLayout

staticLayouty也是一個使用canvas來繪制文字,不過它并不使用canvas的繪制方法。canvas.drawText隻能繪制單行的文字,而且不能換行。

* 到了view的邊緣,文字繼續向後繪制到看不到的地方為不能自動換行

* 不能在換行符\n處換行

是以使用canvas.drawtext繪制文字,需要自己把文字切斷後,分多次使用drawText()來繪制。

staticLayout并不是view或者viewGroup,而是android.text.Layout的自雷,他是純粹用來繪制文字的,StaticLayout支援換行,它既支援可以為文字設定寬度上限來讓文字自動換行,也會在\n出主動換行。

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 對文字繪制的輔助,有兩類方法:設定顯示效果的和測量文字尺寸的。

1.設定顯示效果

  • setTextSize(float size);//設定text的大小
  • setTypeface(Typeface typeface)//設定字型的樣式設定不同的 Typeface 就可以顯示不同的字型。我們中國人談到「字型」,比較熟悉的詞是 font, typeface 和 font 是一個意思,都表示字型。 Typeface 這個類的具體用法,需要了解的話可以直接看文檔,很簡單。
  • setFakeBlodText(boolean fakeBoldText)//設定僞粗體,之是以叫僞粗體,是因為程式并不是通過weight設定的粗體,而是在程式運作時把文字給描粗了。
  • setStrikeThruText(boolean strikeThruText)//設定删除線
  • setUnderlineText(boolean underlineText)//設定下劃線
  • setTextSkewX(float skewX)//設定文字的傾斜角度
  • setTextScaleX(float scaleX)//設定文字的橫向縮放,也就是文字變胖變瘦
  • setLetterSpacing(float letterSpacing) //設定字元間距,預設是0

2.測試文字尺寸

不論是文字,還是圖形或 Bitmap,隻有知道了尺寸,才能更好地确定應該擺放的位置。由于文字的繪制和圖形或 Bitmap 的繪制比起來,尺寸的計算複雜得多,是以它有一整套的方法來計算文字尺寸。

float getFontSpacing() 
           

擷取推薦的行間距。即推薦的兩行文字的 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);

FontMetircs getFontMetrics()
           

擷取paint的fontMetircs。FontMetrics 是個相對專業的工具類,它提供了幾個文字排印方面的數值:ascent, descent, top, bottom, leading。

自定義View學習3:canvas繪制文字

如圖,圖中有兩行文字,每一行都有 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 類型的結果。

  • getTextBounds(String text, int start, int end, Rect bounds) //擷取文字的顯示範圍。參數裡,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);
               
  • float measureText(String text) //測量文字的寬度并傳回。
    canvas.drawText(text, offsetX, offsetY, paint);
    float textWidth = paint.measureText(text);
    canvas.drawLine(offsetX, offsetY, offsetX + textWidth, offsetY, paint);
               
    如下圖:
    自定義View學習3:canvas繪制文字

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

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

  • getTextBounds: 它測量的是文字的顯示範圍(關鍵詞:顯示)。形象點來說,你這段文字外放置一個可變的矩形,然後把矩形盡可能地縮小,一直小到這個矩形恰好緊緊包裹住文字,那麼這個矩形的範圍,就是這段文字的 bounds。
  • measureText(): 它測量的是文字繪制時所占用的寬度(關鍵詞:占用)。前面已經講過,一個文字在界面中,往往需要占用比他的實際顯示寬度更多一點的寬度,以此來讓文字和文字之間保留一些間距,不會顯得過于擁擠。上面的這幅圖,我并沒有設定 setLetterSpacing() ,這裡的 letter spacing 是預設值 0,但你可以看到,圖中每兩個字母之間都是有空隙的。另外,下方那條用于表示文字寬度的橫線,在左邊超出了第一個字母 H 一段距離的,在右邊也超出了最後一個字母 r(雖然右邊這裡用肉眼不太容易分辨),而就是兩邊的這兩個「超出」,導緻了 measureText() 比 getTextBounds() 測量出的寬度要大一些。
  • 在實際的開發中,測量寬度要用 measureText() 還是 getTextBounds() ,需要根據情況而定。不過你隻要掌握了上面我所說的它們的本質,在選擇的時候就不會為難和疑惑了。
  • getTextWidths(String text, float[] widths) // 擷取字元串中每個字元的寬度,并把結果填入參數 widths。這相當于 measureText() 的一個快捷方法,它的計算等價于對字元串中的每個字元分别調用 measureText() ,并把它們的計算結果分别填入 widths 的不同元素。
  • int breakText(String text, boolean measureForwards, float maxWidth, float[] measuredWidth)//這個方法也是用來測量文字寬度的。但和 measureText() 的差別是, breakText() 是在給出寬度上限的前提下測量文字的寬度。如果文字的寬度超出了上限,那麼在臨近超限的位置截斷文字。

    學習來源:http://hencoder.com/ui-1-3/