淺扒Android動態設定字型大小

jarvanmo發表於2017-12-06

說點廢話

Android開發中,TextView類的控制元件應該說是很常用了。一般來說我們是通過android:textSize="20sp"
來設定字型大小,但是很多時候也需要動態設定字型大小,呼叫也很簡單:

textView.setTextSize(textSize);

為了適配各種各樣的型號,我們一般會將字型大小定義到dimens.xml之中:

<dimen name="text_size">16sp</dimen>

然後在java程式碼中設定定義好的字型大小:

float dimen = getResources().getDimension(R.dimen.text_size);
textView.setTextSize(dimen);

滿心歡喜的執行一下,看一效果,結果發現字型奇大無比!!!遠非16sp!!!難道不應該通過getDimension()取值嗎?通過logcat我發現,在Nexus 6p並且<dimen name="text_size">16sp</dimen>下,在通過getDimension(R.dimen.text_size)得到返回值是56.0!
實際上,在java程式碼中取在dimens.xml中定義的值一共有三種:

  • getDimension()
  • getDimensionPixelOffset()
  • getDimensionPixelSize()

看到這三個函式的名稱時,還是會有點不知所云。本著“不求甚解,遍歷式開發”的原則,我把這三種方式都試了一遍,結果發現字型大小沒一個是對的,這就詭異了。難道這裡有平行宇宙?至此,我只能翻出我的英漢大詞典,讓我們去探尋一下docs吧。

getDimension()

    /**
     * Retrieve a dimensional for a particular resource ID.  Unit 
     * conversions are based on the current {@link DisplayMetrics} associated
     * with the resources.
     * 
     * @param id The desired resource identifier, as generated by the aapt
     *           tool. This integer encodes the package, type, and resource
     *           entry. The value 0 is an invalid identifier.
     * 
     * @return Resource dimension value multiplied by the appropriate 
     * metric.
     */
    public float getDimension(@DimenRes int id) throws NotFoundException {
      
    }

通過註釋我們不難發現,getDimension()是根據指定id獲取一個基於當前DisplayMetrics的值。這個值究竟是什麼也沒有說,只知道是float,並且單位轉換是基於當前資源的,但肯定不是畫素,如果是畫素應該是int。

getDimensionPixelSize

   /**
     * Retrieve a dimensional for a particular resource ID for use
     * as a size in raw pixels.  This is the same as
     * {@link #getDimension}, except the returned value is converted to
     * integer pixels for use as a size.  A size conversion involves
     * rounding the base value, and ensuring that a non-zero base value
     * is at least one pixel in size.
     * 
     * @param id The desired resource identifier, as generated by the aapt
     *           tool. This integer encodes the package, type, and resource
     *           entry. The value 0 is an invalid identifier.
     * 
     * @return Resource dimension value multiplied by the appropriate 
     * metric and truncated to integer pixels.
     */
    public int getDimensionPixelSize(@DimenRes int id) throws NotFoundException {
 
    }

getDimensionPixelSize()的功能與getDimension()類似,不同的是將結果轉換為int,並且小數部分四捨五入,這個結果將作為尺寸。getDimensionPixelSize()進行了尺寸轉換,這個轉換實際是上四捨五入的結果,並且保證返回值是一個至少是1畫素的非零數值。

getDimensionPixelOffset()

    /**
     * Retrieve a dimensional for a particular resource ID for use
     * as an offset in raw pixels.  This is the same as
     * {@link #getDimension}, except the returned value is converted to
     * integer pixels for you.  An offset conversion involves simply
     * truncating the base value to an integer.
     * 
     * @param id The desired resource identifier, as generated by the aapt
     *           tool. This integer encodes the package, type, and resource
     *           entry. The value 0 is an invalid identifier.
     * 
     * @return Resource dimension value multiplied by the appropriate 
     * metric and truncated to integer pixels.
     */
    public int getDimensionPixelOffset(@DimenRes int id) throws NotFoundException {
    }

getDimensionPixelOffset()getDimension()功能類似,不同的是將結果轉換為int,這個結果將用作原始畫素的偏移量。偏移轉換(offset conversion,函式命名中的offset是這個意思)的作用之一是將基礎值簡單地截短為整數,注意直接截斷小數位,即取整(其實就是把float強制轉化為int,注意不是四捨五入)。

階段性總結

由此可見,這三個函式返回的都是絕對尺寸,而不是相對尺寸(dpsp等)。如果getDimension()返回結果是30.5f,那麼getDimensionPixelSize()返回結果就是31,getDimensionPixelOffset()返回結果就是30。
至此,應該說getDimensionPixelSize() getDimension() getDimensionPixelOffset()我們已經大致有所瞭解了,但是如果想更深入瞭解一下,就需要深入原始碼以驗證上述解釋。

扒原始碼

深入原始碼,我們可以發現其實這三個函式的實現大同小異,以getDimension()為例:

  public float getDimension(@DimenRes int id) throws NotFoundException {
        final TypedValue value = obtainTempTypedValue();
        try {
            final ResourcesImpl impl = mResourcesImpl;
            impl.getValue(id, value, true);
            if (value.type == TypedValue.TYPE_DIMENSION) {
                return TypedValue.complexToDimension(value.data, impl.getDisplayMetrics());
            }
            throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id)
                    + " type #0x" + Integer.toHexString(value.type) + " is not valid");
        } finally {
            releaseTempTypedValue(value);
        }
    }

TypedValue是動態型別資料的容器,其主要用於盛放Resources的值。上述程式碼第7行就是根據id獲取TypedValue的值,getDimension()getDimensionPixelOffset()getDimensionPixelSize()函式體唯一的不同就是第7行:

  • getDimension()呼叫的是TypedValuecomplexToDimension()方法
  • getDimensionPixelSize呼叫的是TypedValuecomplexToDimensionPixelSize()方法
  • getDimensionPixelOffset呼叫的是TypedValuecomplexToDimensionPixelOffset()方法

順藤摸瓜,我們繼續深入ypedValue,檢視complexToDimension()complexToDimensionPixelSize()complexToDimensionPixelOffset()函式的區別,會發現這三個函式體內容依舊大同小異,以complexToDimension()為例:

    public static float complexToDimension(int data, DisplayMetrics metrics) {
        return applyDimension(
            (data>>COMPLEX_UNIT_SHIFT)&COMPLEX_UNIT_MASK,
            complexToFloat(data),
            metrics);
    }

complexToDimensionPixelOffset()complexToDimension()不同的是將結果進行了強轉,實際上相當直接截斷小數部分;
complexToDimensionPixelSize()是將結果進行四捨五入,並取整。這裡的四捨五入實際上就是把結果加上0.5f然後進行強轉(有興趣瞭解原理的可以留言)。

applyDimension()

各位看官,原始碼已經看到了這裡,是否已感覺很無趣?但applyDimension()的實現已經脫光了在等著你呢:

public static float applyDimension(int unit, float value,DisplayMetrics metrics)  {
        switch (unit) {
        case COMPLEX_UNIT_PX:
            return value;
        case COMPLEX_UNIT_DIP:
            return value * metrics.density;
        case COMPLEX_UNIT_SP:
            return value * metrics.scaledDensity;
        case COMPLEX_UNIT_PT:
            return value * metrics.xdpi * (1.0f/72);
        case COMPLEX_UNIT_IN:
            return value * metrics.xdpi;
        case COMPLEX_UNIT_MM:
            return value * metrics.xdpi * (1.0f/25.4f);
        }
        return 0;
    }

在上述程式碼中,我們發現在applyDimension()中根據單位的不同,將float乘上不同的係數。如dip/dp需乘上螢幕係數,sp則需乘上字號的縮放係數,pt、in、mm等也是根據相應的演算法進行換算(從COMPLEX_UNIT_PX直接返回float可以看出,該方法是將數值轉成畫素數)。

再次總結

通過上述探索,我們不難發現,在Adroid並沒有在java程式碼中直接獲取dimens.xml中定義的dp(dip)/sp的值的API,只有getDimension()getDimensionPixelOffset()getDimensionPixelSize()這個三個方法來獲取絕對尺寸。但有時候我們確實需要動態獲取dimen.xml中的值,併為TextView設定字型大小。而這種方法直接應用在textView.setTextSize(dimen);都是有問題的。那我們將從TextView入手,尋找一個正確的姿勢來設定字型大小。

setTextSize()

首先把程式碼端上來:

  public void setTextSize(float size) {
        setTextSize(TypedValue.COMPLEX_UNIT_SP, size);
    }

原來setTextSize(float)呼叫了他的過載方法setTextSize(int,float),並且第一個引數傳的預設值是TypedValue.COMPLEX_UNIT_SP,眼熟嗎,沒錯就是之前提到的。那麼,我們繼續看看一下setTextSize(int,float)做了什麼:

   public void setTextSize(int unit, float size) {
        if (!isAutoSizeEnabled()) {
            setTextSizeInternal(unit, size, true /* shouldRequestLayout */);
        }
    }

很顯然是呼叫了setTextSizeInternal(unit, size, true /* shouldRequestLayout */);。看到這累不,不過看都看了就再看看唄,說不定比蒼老師好看:

    private void setTextSizeInternal(int unit, float size, boolean shouldRequestLayout) {
        Context c = getContext();
        Resources r;

        if (c == null) {
            r = Resources.getSystem();
        } else {
            r = c.getResources();
        }

        setRawTextSize(TypedValue.applyDimension(unit, size, r.getDisplayMetrics()),shouldRequestLayout);
    }

高能!!!TypedValue.applyDimension(unit, size, r.getDisplayMetrics())是不是很眼熟???還記得applyDimension()是怎麼處理資料的嗎?

  • 我們發現在applyDimension()中根據單位的不同,將float乘上不同的係數。如dip/dp需乘上螢幕係數,sp則需乘上字號的縮放係數,pt、in、mm等也是根據相應的演算法進行換算(從COMPLEX_UNIT_PX直接返回float可以看出,該方法是將數值轉成畫素數)

綜上,setTextSize(float)給傳的值的單位其實是SP,但通過getDimension()取的值卻不是這樣的。為了證實預設單位是SP,各位看官可以直接傳個16,看看和16sp是不是一樣的。所以問題是不得到了解決?

結論

Android中並不提供直接從dimens.xml獲取dp/sp數值的方法,通過getDimensionPixelSize() getDimension() getDimensionPixelOffset()獲取的值是經過處理的。所以正確地動態設定TextView字型大小的姿勢應該是:

int dimen = getResources().getDimensionPixelSize(R.dimen.text_size);
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX,dimen);


相關文章