最近在做深度主題,要實現類似小米那種在主題包中設置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的處理算法分析完畢。