canvas 種繪制文字一向是個難題蟆湖,關(guān)于文字大小,輪廓玻粪,寬高的問題更是涉及到好幾個到API隅津,尤其是我們要繪制多行文字時更是如此,老實說我看到這幾個到API后都是懵的劲室,找些好些資料伦仍,然后自己測試不同文字打印數(shù)據(jù)才搞明白,大家跟著我一起來吧很洋,希望能把下面這幾個到API講明白充蓝,也讓大家不在想我一樣糊度~
FontMetrics 文字模型
也可以叫矩陣 FontMetrics,我們給 paint 畫筆設(shè)置完文字大小就可以獲取到這個文字矩陣 對象蹲缠,文字繪制的基礎(chǔ)全部依賴于此棺克,我們首先搞懂這個 FontMetrics 才是王道喵喵喵
FontMetrics 模型圖:這是我能找到的最好的圖了
這2張圖一起看才行哦~
FontMetrics 文字繪制模型涉及到5個概念:
- top
- bottom
- ascent
- descent
- baseline
咱們來一個個的說,說的明白了线定,之后就好做了,就不會再迷糊了
-
baseline
baseline 也叫基準(zhǔn)線确买,是文字繪制的基準(zhǔn)斤讥,文字以文字中心內(nèi)容為核心以baseline為起點進(jìn)行居中對齊,典型的就是帶圈的小寫字母了湾趾,大家從上面的圖中能看出端倪芭商。文字剩下的部分,有的向上占據(jù)空間搀缠,比如 i铛楣,有的向下占據(jù)空間,比如 j 艺普。canvas 繪制文字時就是以baseline 的值作為文字 Y坐標(biāo)的基準(zhǔn)
-
ascent 和 descent
ascent 和 descent 是成對來說的簸州,ascent 叫文字的上坡度鉴竭,descent 叫文字的下坡度,繪制文字據(jù)對不會超出 ascent - descent 的范圍岸浑,上標(biāo)搏存,下標(biāo),音標(biāo)除外
文字占據(jù) ascent - descent 空間的情況分3種:
-
a矢洲,I
典型小寫字母璧眠,只會占據(jù) baseline - ascent 的部分空間 -
A
典型大寫字母,會占據(jù) baseline - ascent 的全部空間读虏,也就是撐滿從 baseline - ascent -
j责静,g
向下占據(jù)空間的典型字母,會像 a 一樣占據(jù)部分 baseline - ascent 的空間盖桥,但是向下繪制的部分會占據(jù)部分 baseline - descent 的空間泰演,向下最多的如 j 是會占據(jù)全部 baseline - descent 的空間 -
我
中文基本會填滿 ascent - descent 的空間,但是又不會 100% 填滿葱轩,上下會多少流出一些空隙
-
top - bottom
top-ascent 叫上標(biāo)睦焕,bottom-descent 叫下標(biāo),一般繪制文字不會占這塊的空間靴拱,但是上標(biāo)垃喊,下標(biāo),音標(biāo)會占用這塊空間袜炕,好比上圖中的羅馬字符本谜。top-ascent 和 bottom-descent 上下2塊的空間除了繪制特殊部分,基本是作為文字上下分割空間存在的
-
FontMetrics 的坐標(biāo)值
我們從畫筆 paint 可以獲取的矩陣中的值偎窘,baseline 處為0乌助,向上為負(fù)數(shù),向下為正數(shù)陌知,這里要清楚
基本上 FontMetrics 文字矩陣就是這樣了他托,大家明白這幾條是干啥的就好了~
繪制文字涉及到的 API
這個是重點了啦,大伙都是在這里迷糊的仆葡,下面所列 API 功能相近赏参,容易混
// canvas 繪制文字
public void drawText (String text, float x, float y, Paint paint)
// 獲取文字矩陣和其中的參數(shù)
val fontMetrics = paint.fontMetrics
val top = fontMetrics.top.toInt()
val bottom = fontMetrics.bottom.toInt()
val ascent = fontMetrics.ascent.toInt()
val descent = fontMetrics.descent.toInt()
val leading = fontMetrics.leading.toInt()
// 獲取文字占據(jù)的大小
paint.getTextBounds(text.toString(), 0, text.length, rect)
// 測量文字的寬度
val measureWidth = paint.measureText(text.toString())
// 獲取指定寬度下可以繪制的字符數(shù)
var text3: String = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
val breakText = mPaint.breakText(text3, true, 600f, null)
// textview 獲取行高
text_name.lineHeight
// paint 畫筆獲取行高
paint.getFontMetrics(fontMetrics)
paint.getFontMetricsInt(paint.getFontMetricsInt())
paint.getFontSpacing()
// textview 設(shè)置行間距
android:lineSpacingExtra="10px"
android:lineSpacingMultiplier="1.5"
// StaticLayout 繪制多行文字輔助類,可以實現(xiàn)在指定寬度限制下繪制多行文字,文字可以實動實現(xiàn)換行
var staticLayout = StaticLayout(text, mPaint, width, Layout.Alignment.ALIGN_NORMAL, 0f, 0f, false)
staticLayout.draw(canvas)
這幾個 API ok 了沿盅,之后自定義 view 繪制文字部分基本平趟兒~
getTextBounds 獲取的是哪塊尺寸
好多人對 getTextBounds 這個方法有誤解把篓,認(rèn)為 getTextBounds 獲取的就是文字的大小,其實不然 getTextBounds 獲取的是文字的輪廓腰涧。
什么是輪廓韧掩,就是真實大小,不是 FontMetrics 某段尺寸窖铡,是真實的文字占據(jù)多大就是多少疗锐。前提說過不同類型的字符占據(jù)的 FontMetrics 區(qū)域是不一樣的坊谁,那么 getTextBounds 返回的大小也是不一樣的。
我們來測試幾個不同的字符窒悔,看看相關(guān)的數(shù)據(jù)都是怎么樣的:
這里選2個字符:a 呜袁、 A ,很明顯能看出 getTextBounds 獲取的文字大小是不同的
所以我們使用 getTextBounds 要注意場合简珠,不同的字符獲取到的此存是不一樣的阶界,有其是我們要是用 getTextBounds 計算行高的時候獲取的數(shù)據(jù)肯定是不對的。
Textsize 設(shè)置的是哪部分的值
texeview 的 setTextSize() 方法會指到 paint 的 setTextSize() 方法聋庵,最終會調(diào)用本地 c 的方法膘融,從代碼上我們看不出我們給文字設(shè)置完大小指的是 FontMetrics 的哪里,那么我們從結(jié)果觸發(fā)了
我們打印幾個不同文字尺寸的 FontMetrics 看看:
從結(jié)果上看:textsize 的大小 = baseline 到 top 和 ascent 的中間祭玉。我說之前我量的文字大小總是小一點呢氧映,所以大伙在量文字大小之后加一點大小才準(zhǔn)確
獲取文字寬度
FontMetrics 里面沒有設(shè)計文字寬度,這個很正常脱货,大家想啊岛都,F(xiàn)ontMetrics 是單個字符標(biāo)準(zhǔn),實際文字字?jǐn)?shù)不固定振峻,F(xiàn)ontMetrics 當(dāng)然表示不了了
那我們怎么獲取文字寬度呢臼疫,有2個 Paint 的 API 可供選擇:
- measureText
- getTextBounds
Paint 的這2個方法都可以獲取指定數(shù)量字符的寬度,getTextBounds 獲取的是文字輪廓扣孟,measureText 是專門計算文字寬度的烫堤,從結(jié)果上來看 measureText 獲取的結(jié)果比 getTextBounds 要多 1-2 個像素,關(guān)于這點我找到一個圖:
圖中紅線是 getTextBounds 獲取的部分凤价,粉線是 measureText 獲取的部分鸽斟,自然 measureText 獲取的數(shù)據(jù)要多一些,這算是留邊吧利诺,實際上我看所有人都推薦用 measureText
textview 中的行高
為啥我會寫 textview 呢富蓄,是因為cavans 繪制文字時我不知道應(yīng)該以哪個作為標(biāo)準(zhǔn)行高,是 top - bottom立轧,還是 ascent - descent 格粪,所以去 textview 那瞧瞧
獲取行高的 API 有4個:
// textview 獲取行高
text_name.lineHeight
// paint 畫筆獲取行高
paint.getFontMetrics(fontMetrics)
paint.getFontMetricsInt(paint.getFontMetricsInt())
paint.getFontSpacing()
paint 的3個方法,getFontMetricsInt 獲取的數(shù)據(jù)最準(zhǔn)確氛改,textview 的 getLineHeight 內(nèi)部調(diào)的也是 getFontMetricsInt 這個方法
textview getLineHeight 有這個獲取行高的 API ,我跟進(jìn)去比伏,源碼里面太復(fù)雜胜卤,沒看出來是獲取的 FontMetrics 的哪塊值。
然后我還是從結(jié)果入手赁项,打印了幾次結(jié)果搞清楚了葛躏,先說下結(jié)果:
-
textview 即使在 padding = 0時澈段,依然在上留了 top - ascent ,在下留了 descent - bottom 大小的邊距
打印文字2個不同大小時的矩陣信息和 view 大小舰攒,從結(jié)果能看出來 view 的 height - LineHeight = ( top - ascent)+ (descent - bottom)基本相等败富,這里用的是絕對值。
-
textview 的 LineHeight = ascent - bottom 的距離
上面的圖就不行了摩窃,還是相同的文字大小兽叮,我們從單行增加到 3行:
文字大小是 20px 時,view 的高是 76猾愿,view 默認(rèn)的邊距是 28-24=4 鹦聪,實際內(nèi)容高度是 72 ,除以3 = 24 蒂秘,正好是 LineHeight 的大小
文字大小我們再換成 28px泽本,view 的高是 104,view 默認(rèn)的邊距是 38-33=5 姻僧,實際內(nèi)容高度是 99 规丽,除以3 = 33 ,正好是 LineHeight 的大小
為啥不包含 ascent - top 這塊上標(biāo)的高度呢撇贺,我估計是上標(biāo)一般也用不到赌莺,而且看數(shù)值上標(biāo)大小也不小了,既然用不到就不要了显熏,下標(biāo) descent - bottom 的大小不大雄嚣,正好作為每行默認(rèn)的行間距
有其得到啟發(fā),我們 cavans 繪制文字時還是學(xué)習(xí) textview 以 ascent - bottom 的大小為行高是最合適的
textview 的行間距
textview 有2個參數(shù)可以設(shè)置行間距:
android:lineSpacingExtra="10px"
android:lineSpacingMultiplier="1.5"
- lineSpacingExtra 設(shè)置的行間距的絕度數(shù)值喘蟆,默認(rèn) = 0
- lineSpacingMultiplier 設(shè)置的是行間距的倍數(shù)缓升,默認(rèn) = 1.0
上文提到的 textview 的行高計算方式都是在沒有設(shè)置行間距時的算法,那么這2個值我們要是設(shè)置之后行間距怎么算呢蕴轨,公式就是下面這個啦:
LineHeight = LineHeight(原來的行間距) * lineSpacingMultiplier 倍數(shù)+ lineSpacingExtra
那么我們不用 textview 用 canvas 繪制文字時港谊,行間距就根據(jù)我們自己的習(xí)慣來吧纷捞,因為推薦使用 textview 的行高計算方式岖常,所以我們繪制出來的文字默認(rèn)是帶一些行間距的,如果需要我們可以加一個行間距的偏移量進(jìn)來即可
canvas 單行文字居中
終于到重頭戲了揣云,就是我們用 canvas 畫文字時最常見的居中問題棘脐,先來單行的斜筐,再來多行的。
看圖例蛀缝,我畫了一個示意圖顷链,紅線是 view 中心,黃線是文字的 baseline 基線屈梁,在文字居中時嗤练,基線的情況就是這樣子的榛了,我們算的就是 baseline 基線相對 view 中心線的偏移量
要算這個值我們是離不開 FontMetrics 的
首先別忘了 FontMetrics 中 baseline 為0,top 和 ascent 的值都是負(fù)數(shù)煞抬,這里我們?nèi)?ascent - descent 的距離為行高霜大,因為文字不會超過這個范圍的
因為文字要居中,所以 view 的中心線也就是文字的中心線革答,也就是行高的一半 = ( - ascent + descent )/ 2 战坤。中心線到 baseline 距離 = ascent 的高度 - 行高的一半 = - ascent - ( - ascent + descent )/ 2
最后計算下得到最后的公式:
-ascent / 2 - descent / 2
代碼如下:
class MyTextview : View {
var mPaint: TextPaint = TextPaint()
constructor(context: Context?) : super(context)
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes)
init {
mPaint.textSize = 58f
mPaint.isAntiAlias = true
mPaint.strokeWidth = 1f
}
override fun onDraw(canvas: Canvas?) {
canvas?.drawColor(Color.BLUE)
var text: String = "AAAgggg我我哦我我我我我"
var drawTextX: Float = 0f
var drawTextY: Float = 0f
// 拿到文字的寬度
val textWidth = getTextWidth(text, mPaint)
// baseline x 坐標(biāo)
drawTextX = (width / 2 - textWidth / 2)
// baseline y 坐標(biāo) = view 的中心 + baseline 的偏移量
drawTextY = (height / 2 + calculateBaselineOffsetY(mPaint))
mPaint.color = Color.WHITE
canvas?.drawText(text, 0, text.length, drawTextX, drawTextY, mPaint)
// 繪制2條參考線
mPaint.color = Color.RED
canvas?.drawLine(0f, (height / 2).toFloat(), width.toFloat(), (height / 2).toFloat(), mPaint)
mPaint.color = Color.YELLOW
canvas?.drawLine(0f, drawTextY, width.toFloat(), drawTextY, mPaint)
}
// 拿到文字的寬度
fun getTextWidth(text: String, paint: Paint): Float {
if (TextUtils.isEmpty(text)) {
return 0f
}
return paint.measureText(text)
}
// 計算 baseline 的相對文字中心的偏移量
fun calculateBaselineOffsetY(paint: Paint): Float {
val fontMetrics = paint.fontMetrics
val ascent = fontMetrics.ascent
val descent = fontMetrics.descent
return -ascent / 2 - descent / 2
}
}
canvas 多行文字居中
多行和單行計算起來沒什么太大區(qū)別,這里我來個簡單點的蝗碎,之后會寫一個模擬 textview 的例子湖笨,那里會復(fù)雜一些
這里繪制3行同樣的文字,每行起點的 X 坐標(biāo)是相同的蹦骑。我使用 ascent - bottom 的距離做為一行的高度慈省,這樣第二行第三行繪制時 Y 坐標(biāo)就是在前一行的基礎(chǔ)上加上個行高就行了
那么重點就是計算第一行的繪制坐標(biāo)了,X 坐標(biāo)不用說了眠菇,Y 坐標(biāo)的計算也不難边败,這里我們知道了一行的高度,一共有幾行捎废,那么我們就可以計算出文字總得高度
var textlist = arrayListOf<String>("AAAgggg我我哦我我我我我", "AAAgggg我我哦我我我我我", "AAAgggg我我哦我我我我我")
var totalHeight = getLineHeight(mPaint) * textlist.size
fun getLineHeight(paint: Paint): Float {
val fontMetrics = paint.fontMetrics
val ascent = fontMetrics.ascent
val bottom = fontMetrics.bottom
return Math.abs(ascent) + Math.abs(bottom)
}
然后我們可以計算出文字頂部的 Y 坐標(biāo)笑窜,那么所有文字居中時 Y 坐標(biāo)就出來了
var centerY = height / 2 - getLineHeight(mPaint) * textlist.size / 2
然后我們再算第一行文字 baseline 的偏移量也就好算了,baseline 距離每行最頂部有 ascent 的距離登疗,這里我們再加上一個 ascent 的偏移量就是繪制第一行文字 Y 的起始坐標(biāo)
var fristTextStartY = height / 2 - getLineHeight(mPaint) * textlist.size / 2 + getFontMetricsAscent(mPaint)
這個不難寫排截,因為這里有幾行,每行文字分割辐益,每行文字 X 軸起始坐標(biāo)都不用考慮断傲,只算 Y 的值,也是盡量簡單點智政,好理解嘛认罩,之后有個例子會復(fù)雜一些,把上述參數(shù)考慮進(jìn)去
代碼如下:
class MyTextview3 : View {
var mPaint: TextPaint = TextPaint()
constructor(context: Context?) : super(context)
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes)
init {
mPaint.textSize = 50f
mPaint.isAntiAlias = true
mPaint.strokeWidth = 1f
}
override fun onDraw(canvas: Canvas?) {
canvas?.drawColor(Color.BLUE)
var textlist = arrayListOf<String>("AAAgggg我我哦我我我我我", "AAAgggg我我哦我我我我我", "AAAgggg我我哦我我我我我")
var drawTextX: Float = 0f
var drawTextY: Float = 0f
// 計算第一行文字繪制的起始位置
drawTextX = (width / 2 - getTextWidth(textlist[0], mPaint) / 2)
drawTextY = (height / 2 - getLineHeight(mPaint) * textlist.size / 2 + getFontMetricsAscent(mPaint))
// 繪制 view 中心線
mPaint.color = Color.RED
canvas?.drawLine(0f, (height / 2).toFloat(), width.toFloat(), (height / 2).toFloat(), mPaint)
// 繪制文字
mPaint.color = Color.WHITE
for ((index, text) in textlist.withIndex()) {
// 繪制每行文字時续捂,添加 Y 軸向下偏移量 = 自身行數(shù)-1的行高垦垂,第一行是0,所以沒有偏移量牙瓢,或者大家在循環(huán)里自己一行一行的填也可以
canvas?.drawText(text, 0, text.length, drawTextX, drawTextY + index * getLineHeight(mPaint), mPaint)
}
}
fun getTextWidth(text: String, paint: Paint): Float {
if (TextUtils.isEmpty(text)) {
return 0f
}
return paint.measureText(text)
}
fun getLineHeight(paint: Paint): Float {
val fontMetrics = paint.fontMetrics
val ascent = fontMetrics.ascent
val bottom = fontMetrics.bottom
return Math.abs(ascent) + Math.abs(bottom)
}
fun getFontMetricsAscent(paint: Paint): Float {
return Math.abs( paint.fontMetrics.ascent )
}
}
breakText
var text3: String = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
val breakText = mPaint.breakText(text3, true, 600f, null)
這個方法讓我們設(shè)置一個最大寬度在不超過這個寬度的范圍內(nèi)返回實際測量值否則停止測量劫拗,參數(shù)很多但是都很好理解,text表示我們的字符串矾克,measureForwards表示向前還是向后測量杨幼,maxWidth表示一個給定的最大寬度在這個寬度內(nèi)能測量出幾個字符,measuredWidth為一個可選項聂渊,可以為空差购,不為空時返回真實的測量值
這個方法在一些結(jié)合文本處理的應(yīng)用里比較常用,比如文本閱讀器的翻頁效果汉嗽,我們需要在翻頁的時候動態(tài)折斷或生成一行字符串欲逃,這就派上用場了~~~
StaticLayout
系統(tǒng)中有一個StaticLayout方法,可以在設(shè)置寬度饼暑,當(dāng)前行文本超過此寬度后稳析,進(jìn)行自動換行,提供ALIGN_CENTER(居中)弓叛、ALIGN_NORMAL(標(biāo)準(zhǔn))彰居、ALIGN_OPPOSITE(與標(biāo)準(zhǔn)相反)三種對齊方式。
StaticLayout 的使用不難撰筷,看下面的 API 介紹就 OK ~
var text = "AAAAAAA"
var width: Int = 150
var staticLayout = StaticLayout(text, mPaint, width, Layout.Alignment.ALIGN_NORMAL, 0f, 0f, false)
staticLayout.draw(canvas)
// 可以獲取文字在指定寬度限制下所需空間
staticLayout.width
staticLayout.height
// 若是使用 StaticLayout 繪制文字居中陈惰,可以通過移動 canvas 實現(xiàn)文字位置的變化
canvas?.save()
canvas?.translate(staticLayout.getWidth() / 2.toFloat(), staticLayout.getHeight() / 2.toFloat());
staticLayout.draw(canvas)
canvas?.restore()