1. 程式人生 > >【Android】TextView中不同大小字型如何上下垂直居中?

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

,但是很不幸,怎麼試都不湊效。那麼到底有沒有辦法使用Span讓不同字型大小的垂直居中呢?

答案是:當然可以,得用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將包換一些資訊,例如:baselinePaintFontMetricsInt等資訊。

那我們現在來看看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的asentdesent來達到改變高度的目的呢?答案是可行的。

解決方法

按照上面的分析,我們繼承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);

看看效果:

【參考資料】