字節跳動的適配方案原理及代碼實現

 字節跳動技術團隊 2018-05-25

每天叫醒你的不是鬧鐘,而是姿勢

在Android開發中,由於Android碎片化嚴重,屏幕分辨率千奇百怪,而想要在各種分辨率的設備上顯示基本一致的效果,適配成本越來越高。雖然Android官方提供了dp單位來適配,但其在各種奇怪分辨率下表現卻不盡如人意,因此下面探索一種簡單且低侵入的適配方式。

傳統dp適配方式的缺點

android中的dp在渲染前會將dp轉爲px,計算公式:

  • px = density * dp;

  • density = dpi / 160;

  • px = dp * (dpi / 160);

 

而dpi是根據屏幕真實的分辨率和尺寸來計算的,每個設備都可能不一樣的。

 

 

屏幕尺寸、分辨率、像素密度三者關係

通常情況下,一部手機的分辨率是寬x高,屏幕大小是以寸爲單位,那麼三者的關係是:

舉個例子:屏幕分辨率爲:1920*1080,屏幕尺寸爲5吋的話,那麼dpi爲440。

 

 

這樣會存在什麼問題呢?

假設我們UI設計圖是按屏幕寬度爲360dp來設計的,那麼在上述設備上,屏幕寬度其實爲1080/(440/160)=392.7dp,也就是屏幕是比設計圖要寬的。這種情況下, 即使使用dp也是無法在不同設備上顯示爲同樣效果的。 同時還存在部分設備屏幕寬度不足360dp,這時就會導致按360dp寬度來開發實際顯示不全的情況。


而且上述屏幕尺寸、分辨率和像素密度的關係,很多設備並沒有按此規則來實現, 因此dpi的值非常亂,沒有規律可循,從而導致使用dp適配效果差強人意。

探索新的適配方式

 

梳理需求

首先來梳理下我們的需求,一般我們設計圖都是以固定的尺寸來設計的。比如以分辨率1920px * 1080px來設計,以density爲3來標註,也就是屏幕其實是640dp * 360dp。如果我們想在所有設備上顯示完全一致,其實是不現實的,因爲屏幕高寬比不是固定的,16:9、4:3甚至其他寬高比層出不窮,寬高比不同,顯示完全一致就不可能了。但是通常下,我們只需要以寬或高一個維度去適配,比如我們Feed是上下滑動的,只需要保證在所有設備中寬的維度上顯示一致即可,再比如一個不支持上下滑動的頁面,那麼需要保證在高這個維度上都顯示一致,尤其不能存在某些設備上顯示不全的情況。同時考慮到現在基本都是以dp爲單位去做的適配,如果新的方案不支持dp,那麼遷移成本也非常高。

 

因此,總結下大致需求如下:

  1. 支持以寬或者高一個維度去適配,保持該維度上和設計圖一致;

  2. 支持dp和sp單位,控制遷移成本到最小。

 

 

找兼容突破口

從dp和px的轉換公式 :px = dp * density 

可以看出,如果設計圖寬爲360dp,想要保證在所有設備計算得出的px值都正好是屏幕寬度的話,我們只能修改 density 的值。

 

通過閱讀源碼,我們可以得知,density 是 DisplayMetrics 中的成員變量,而 DisplayMetrics 實例通過 Resources#getDisplayMetrics 可以獲得,而Resouces通過Activity或者Application的Context獲得。

 

先來熟悉下 DisplayMetrics 中和適配相關的幾個變量:

  • DisplayMetrics#density 就是上述的density

  • DisplayMetrics#densityDpi 就是上述的dpi

  • DisplayMetrics#scaledDensity 字體的縮放因子,正常情況下和density相等,但是調節系統字體大小後會改變這個值

     

那麼是不是所有的dp和px的轉換都是通過 DisplayMetrics 中相關的值來計算的呢?

 

首先來看看佈局文件中dp的轉換,最終都是調用 TypedValue#applyDimension(int unit, float value, DisplayMetrics metrics) 來進行轉換:

 

這裏用到的DisplayMetrics正是從Resources中獲得的。

 

再看看圖片的decode,BitmapFactory#decodeResourceStream方法:

 

可見也是通過 DisplayMetrics 中的值來計算的。

 

當然還有些其他dp轉換的場景,基本都是通過 DisplayMetrics 來計算的,這裏不再詳述。因此,想要滿足上述需求,我們只需要修改 DisplayMetrics 中和 dp 轉換相關的變量即可。

 

 

最終方案

下面假設設計圖寬度是360dp,以寬維度來適配。

 

那麼適配後的 density = 設備真實寬(單位px) / 360,接下來只需要把我們計算好的 density 在系統中修改下即可,代碼實現如下:

 

同時在 Activity#onCreate 方法中調用下。代碼比較簡單,也沒有涉及到系統非公開api的調用,因此理論上不會影響app穩定性。

 

於是修改後上線灰度測試了一版,穩定性符合預期,沒有收到由此帶來的crash,但是收到了很多字體過小的反饋:

 

原因是在上面的適配中,我們忽略了DisplayMetrics#scaledDensity的特殊性,將DisplayMetrics#scaledDensity和DisplayMetrics#density設置爲同樣的值,從而某些用戶在系統中修改了字體大小失效了,但是我們還不能直接用原始的scaledDensity,直接用的話可能導致某些文字超過顯示區域,因此我們可以通過計算之前scaledDensity和density的比獲得現在的scaledDensity,方式如下:

 

但是測試後發現另外一個問題,就是如果在系統設置中切換字體,再返回應用,字體並沒有變化。於是還得監聽下字體切換,調用 Application#registerComponentCallbacks 註冊下 onConfigurationChanged 監聽即可。

 

因此最終方案如下:

 

當然以上代碼只是以設計圖寬360dp去適配的,如果要以高維度適配,可以再擴展下代碼即可。

Showcase

適配前後和設計圖對比:

 

適配後各機型的顯示效果:

 

說明

design_width_in_dp 和 design_height_in_dp 的單位必須是 dp,如果設計師給你的設計圖,只標註了 px 尺寸 ,那請自行根據公式  dp = px / (DPI / 160)  將 px 尺寸轉換爲 dp 尺寸(px 尺寸轉換爲 sp一樣)。

你在 AndroidManifest.xml 中怎麼把設計圖的 px 尺寸轉換爲 dp 尺寸,那在佈局時,每個控件的大小也需要以同樣的方式將設計圖上標註的 px 尺寸轉換爲 dp 尺寸,千萬不要在 AndroidManifest.xml 中填寫的是 dp 尺寸,卻在佈局中繼續填寫設計圖上標註的 px 尺寸(px 尺寸轉換爲 sp一樣)。

 

參考

https://developer.android.com/guide/practices/screens_support.html

https://www.jianshu.com/p/55e0fca23b4f

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