1. 程式人生 > >今日頭條Android螢幕適配方式

今日頭條Android螢幕適配方式

一種極低成本的Android螢幕適配方式

今日頭條技術團隊 5月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

適配前後和設計圖對比:

 

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

 

參考https://mp.weixin.qq.com/s/d9QCoBP6kV9VSWvVldVVwA

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