【Android】TextView中不同大小字型如何上下垂直居中?
前言
在客戶端開發中,我們往往需要對一個TextView的文字的部分內容進行特殊化處理,比如加粗、改變顏色、加連結、下劃線等。iOS為我們提供了AttributedString
,而Android則提供了SpannableString
。
在Android的android.text.style包下為我們提供了各種各樣的span(可以參考這篇文章),例如:
AbsoluteSizeSpan(int size) —— 設定字型大小,引數是絕對數值,相當於Word中的字型大小
RelativeSizeSpan(float proportion) —— 設定字型大小,引數是相對於預設字型大小的倍數,比如預設字型大小是x, 那麼設定後的字型大小就是x*proportion,這個用起來比較靈活,proportion>1就是放大(zoom in), proportion<1就是縮小(zoom out)
BackgroundColorSpan(int color) —— 背景著色,引數是顏色數值,可以直接使用android.graphics.Color裡面定義的常量,或是用Color.rgb(int, int, int)
ForegroundColorSpan(int color) —— 前景著色,也就是字的著色,引數與背景著色一致
問題
網上已經有著很多使用這些span的教程了,所以沒必要在這裡繼續探討這些基礎使用了。但是,如果使用了AbsoluteSizeSpan(int size)
在同一個TextView中定義了不同字型大小,就會預設顯示成底部對齊的方式:
說到這裡,第一反應肯定是tv.setGravity(Gravity.CENTER_VERTICAL)
答案是:當然可以,得用ReplacementSpan
分析
為何是ReplacementSpan?
它是系統提供給我們的一個抽象類。通過名字我們可以知道其實用於是用於替換。指示我們可以把文字的某一部分替換成我們想要的內容。這也許是我們想要的。
Relpacement
的定義很簡單:
public abstract class ReplacementSpan extends MetricAffectingSpan {
public abstract int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm);
public abstract void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint);
public void updateMeasureState(TextPaint p) { }
public void updateDrawState(TextPaint ds) { }
}
我們在繼承它的時候,需要實現兩個方法getSize()
和draw()
。通過方法名,我們也許能夠知道其作用:getSize()
用於確定span的大小(實際上只是一個寬度),draw()
用於繪製我們想要的內容。
但是問題來了,這些方法的傳參是什麼?為何getSize()
只返回了一個int值?
瞭解了這兩個問題,就基本弄懂了自定義span。來回答這兩個問題前,我們首先要明確的一件事情是:span是用於SpannableString
中,並且最終被用於TextView
中。所以在定義span時,我們的大小、繪製內容都應該依賴於使用時的環境。我們假設自定義span使用的環境為A,那麼A將包換一些資訊,例如:baseline
、Paint
、FontMetricsInt
等資訊。
那我們現在來看看getSize()
方法。getSize()
的返回值是int,其實這個值指的是自定義span的寬度,那它的高度呢?其實高度是已知的,那就是外界環境A帶來的字的高度。但我某些情況我們希望改變span的高度,我們該怎麼做呢? 如果對Android上字型繪製有一定了解的同學會知道,一個字的高度取決於繪製這個子的Paint.FontMetricsInt
什麼是 Paint.FontMetrics
它表示繪製字型時的度量標準。google的官方api文件對它的欄位說明如下:
Type | Fields |
---|---|
public float | ascent - The recommended distance above the baseline for singled spaced text. |
public float | bottom - The maximum distance below the baseline for the lowest glyph in the font at a given text size. |
public float | descent - The recommended distance below the baseline for singled spaced text. |
public float | leading - The recommended additional space to add between lines of text. |
public float | top - The maximum distance above the baseline for the tallest glyph in the font at a given text size. |
其中:
- ascent : 字型最上端到基線的距離,為負值。
- descent:字型最下端到基線的距離,為正值。
如上圖,中間那條線(Baseline)就是基線,基線到上面那條線的距離就是ascent
,基線到下面那條線的距離就是descent
。
回到我們的主題, 我們發現getSize()
方法的引數中有Paint.FontMetricsInt
,那我們是否就可以通過改變傳入的Paint.FontMetricsInt的asent
和desent
來達到改變高度的目的呢?答案是可行的。
解決方法
按照上面的分析,我們繼承ReplacementSpan
自定義一個Span
/**
* 使TextView中不同大小字型垂直居中
*/
public class CustomVerticalCenterSpan extends ReplacementSpan {
private int fontSizeSp; //字型大小sp
public CustomVerticalCenterSpan(int fontSizeSp){
this.fontSizeSp = fontSizeSp;
}
@Override
public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
text = text.subSequence(start, end);
Paint p = getCustomTextPaint(paint);
return (int) p.measureText(text.toString());
}
@Override
public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
text = text.subSequence(start, end);
Paint p = getCustomTextPaint(paint);
Paint.FontMetricsInt fm = p.getFontMetricsInt();
canvas.drawText(text.toString(), x, y - ((y + fm.descent + y + fm.ascent) / 2 - (bottom + top) / 2), p); //此處重新計算y座標,使字型居中
}
private TextPaint getCustomTextPaint(Paint srcPaint) {
TextPaint paint = new TextPaint(srcPaint);
paint.setTextSize(ViewUtils.getSpPixel(mContext, fontSizeSp)); //設定字型大小, sp轉換為px
return paint;
}
}
解釋下形參:
- x:要繪製的image的左邊框到textview左邊框的距離。
- y:要替換的文字的基線(Baseline)的縱座標。
- top:替換行的最頂部位置。
- bottom:替換行的最底部位置。注意,textview中兩行之間的行間距是屬於上一行的,所以這裡bottom是指行間隔的底部位置。
- paint:畫筆,包含了要繪製字型的度量資訊。
所以就有:
y + fm.descent
:得到字型的descent
線座標;
y + fm.ascent
:得到字型的ascent
線座標;
(y + fm.descent + y + fm.ascent) / 2
也就是字型中間線的縱座標
((y + fm.descent + y + fm.ascent) / 2 - (bottom + top) / 2)
就是字型需要向上調整的距離
使用方式
SpannableString ss = new SpannableString(disStr + unitString);
ss.setSpan(new AbsoluteSizeSpan(40, true), 0, disStr.length(), Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
//垂直居中顯示文字
ss.setSpan(new CustomVerticalCenterSpan(23), disStr.length(), ss.length(), Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
看看效果: