Android Dimension轉換算法原理分析

最近在做深度主題,要實現類似小米那種在主題包中設置dimension值,然後在系統中替換原值的功能。

特地研究了一下Android系統中dimension類型的值的存儲方式以及相關的轉換算法。

?

在Android中,我們可以在values文件夾中定義各種資源,其中有一種就是dimension。

dimension是一個包含單位(dp、dip、sp、pt、px、mm、in)的尺寸,可以用於定義視圖的寬度、字號等。如下圖所示。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <dimen name="textview_height">25dp</dimen>
    <dimen name="textview_width">150dp</dimen>
    <dimen name="ball_radius">30dp</dimen>
    <dimen name="font_size">16sp</dimen>
</resources>
<TextView
    android:layout_height="@dimen/textview_height"
    android:layout_width="@dimen/textview_width"
    android:textSize="@dimen/font_size"/>

而在代碼中,我們可以通過getDimension方法獲取到資源文件中定義的dimension值。

Resources res = getResources();
float fontSize = res.getDimension(R.dimen.font_size);

從上圖可以發現,不論之前在資源文件中定義的dimension是什麼單位,在代碼中均轉換成了float類型的數值。

但兩個dimension值,如果數值部分相同,但單位不同,顯然轉換後的float值應該不同。

爲了證明以上結論,我做了一個實驗。

首先,定義一組dimension,數值部分相同,單位不同。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <dimen name="tdp">10dp</dimen>
    <dimen name="tdip">10dip</dimen>
    <dimen name="tsp">10sp</dimen>
    <dimen name="tpt">10pt</dimen>
    <dimen name="tpx">10px</dimen>
    <dimen name="tmm">10mm</dimen>
    <dimen name="tin">10in</dimen>
</resources>

然後,在代碼中分別使用getDimension與getValue方法獲取這些dimension的值。

Resources r = getResources();
TypedValue tv = new TypedValue();
r.getValue(R.dimen.tdp, tv, true);
System.out.println("10dp="+r.getDimension(R.dimen.tdp)+" data="+Integer.toBinaryString(tv.data));
r.getValue(R.dimen.tdip, tv, true);
System.out.println("10dip="+r.getDimension(R.dimen.tdip)+" data="+Integer.toBinaryString(tv.data));
r.getValue(R.dimen.tsp, tv, true);
System.out.println("10sp="+r.getDimension(R.dimen.tsp)+" data="+Integer.toBinaryString(tv.data));
r.getValue(R.dimen.tpt, tv, true);
System.out.println("10pt="+r.getDimension(R.dimen.tpt)+" data="+Integer.toBinaryString(tv.data));
r.getValue(R.dimen.tpx, tv, true);
System.out.println("10px="+r.getDimension(R.dimen.tpx)+" data="+Integer.toBinaryString(tv.data));
r.getValue(R.dimen.tmm, tv, true);
System.out.println("10mm="+r.getDimension(R.dimen.tmm)+" data="+Integer.toBinaryString(tv.data));
r.getValue(R.dimen.tin, tv, true);
System.out.println("10in="+r.getDimension(R.dimen.tin)+" data="+Integer.toBinaryString(tv.data));

最後,可以從下圖看到輸出的結果。

從上圖可以看出,不同單位情況下,即使數值相同,轉換成的float值也是千差萬別。

但從後面的data值中,我們卻發現,對於不同的單位,其最高位大部分是相同的,僅僅是後面幾位有所區別。我們可以大膽地猜想:“dimension在系統中是以數值+單位的形式存儲的。”

分析了Android的Resoureces類的getDimension方法的源碼後我們發現,對於不同的dimension,在使用getValue獲取到對應的int值之後,會通過TypedValue的complexToDimension方法將其轉換爲float。
complexToDimension方法主要是調用applyDimension方法,將getValue獲取到的int值,拆成單位和數值兩部分,然後根據單位的不同,對數值進行處理。
其中, (data>>COMPLEX_UNIT_SHIFT)&COMPLEX_UNIT_MASK的作用是將該int值與上0xf,以獲取其最低4位,這4位是單位。而complexToFloat則是使用該int值的最高24位當作數值,4-7位作爲radix,進行計算,轉成float。
最後,在applyDimension中根據單位的不同,將float乘上不同的係數。如dip/dp需乘上屏幕係數,sp需乘上字號的縮放係數,pt、in、mm等也是根據相應的算法進行換算。(從COMPLEX_UNIT_PX直接返回float可以看出,該方法是將數值轉成像素數

至此,我們之前的猜想大致上是正確的,只是需要加上一個radix部分。即“dimension在系統中是以數值+4位radix+4位單位的形式存儲的”。

因此,爲了實現將形如“10dip”的dimension轉成getValue返回的int值,我們需要進行以下處理(參考com.androi.layoutlib.bridge.impl.ResourceHelper的parseFloatAttribute方法)。

首先,將dimension字符串拆成數值與單位兩部分,並將數值轉成浮點數。

從上文可以知道,dimension的數值部分在實際存儲時,系統只提供了24位存儲空間。再考慮到一個dimension是有符號的,可正可負。故最高位表示正負。因此,真正的數值只有23位進行存儲。

所以,接下來,對於負數要先轉成對應的正數,並乘上223加0.5(四捨五入)後轉成long。(即僅保留整數和小數部分各23位)

接着,對轉換後的long值進行判斷。

若最低23位爲0,即小數部分爲0,標記radix爲TypedValue.COMPLEX_RADIX_23p0,shift爲23,即只保留整數部分。

若最高41位爲0,即整數部分爲0(java中long以8個字節存儲),標記radix爲TypedValue.COMPLEX_RADIX_0p23,shift爲0,即只保留小數部分。

若最高33爲0,即整數部分最多隻有8位有效,標記radix爲TypedValue.COMPLEX_RADIX_8p15,shift爲8,即只保留整數部分8位,小數部分15位。

若最高25位爲0,即整數部分最多隻有16位有效,標記radix爲TypedValue.COMPLEX_RADIX_16p7,shift爲16,即只保留整數部分16位,小數部分7位。

若以上都不符合,說明小數部分不爲0,整數有效部分超過16位,則標記radix爲TypedValue.COMPLEX_RADIX_23p0,shift爲23,即只保留整數部分。

然後,根據shift將轉換後的long值進行右移,取最低24位,轉爲int。若原值爲負數,將右移的數轉成負數後再取最低24位。

最後,將最終數值左移8位,最低4-7位或上radix,0-3位或上單位。


上圖給出了123113.51213dp的處理流程,按照算法,該dimension值會被轉換成31516929存儲在系統中。爲了驗證算法的正確性,筆者做了以下實驗。

首先,在資源文件中添加一個值爲123113.51213dp的dimension。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <dimen name="tdp">123113.51213dp</dimen>
</resources>

?然後,在代碼中獲取該值。

Resources r = getResources();
TypedValue tv = new TypedValue();
r.getValue(R.dimen.tdp, tv, true);
System.out.println("123113.51213dp="+r.getDimension(R.dimen.tdp)+" data="+(tv.data));

?最後,查看輸出結果,完全符合!

至此,dimension的處理算法分析完畢。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章