android 今日頭條的螢幕適配理解

筆墨Android發表於2018-10-14

前一段時間無意中看到今日頭條的適配方案,使用到專案中,感覺真的是無比絲滑。所以特意寫一篇文章分享給小夥伴們!

本文知識點:

  • 為什麼要做螢幕適配
  • 今日頭條的適配方案(劃重點)
  • 今日頭條的適配方案的一些問題

1. 為什麼要做螢幕適配

做Android開發的都瞭解,由於Android螢幕碎片化嚴重,雖然Android官方提供了dp為單位的適配方案,但是由於各種千奇百怪的機型,所以變現往往不盡如人意。所以需要進行螢幕適配。說白了就是讓所有機型都進行保持UI的設計原貌!

2. 今日頭條的適配方案

終於到了本文的重點了。為了大家能深刻理解其中的含義,這裡從最基本的開始說起。

2.1 傳統的dp適配的流程

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

px = density * dp;
density = dpi / 160;
px = dp * (dpi / 160);
複製程式碼

而dpi是根據螢幕真實的解析度和尺寸來計算的,每個裝置都可能不一樣的。那麼dpi是怎麼計算的呢?

android 今日頭條的螢幕適配理解

上面圖片說明dpi是怎麼計算得來的。舉個例子,當螢幕解析度為1920 * 1080螢幕尺寸為5寸的手機。計算得來的dpi為440。不信的話可以計算一下!

那麼問題來了?

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

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

3.2 今日頭條的適配方式說明

其實,當我們拿到設計圖的時候,一般都是根據蘋果的6進行設計的,往往在Android中,存在16:9和4:3的一些機型,那麼這些機型中的寬高比不同,如果想完全按照設計圖進行適配是不可能的,也是不現實的。但是如果我們以一個維度,也就是寬這個維度來進行適配的話,如果高度超出了螢幕我們就使用可滑動的控制元件進行展示。這就是今日頭條的適配方案。

因此,採用以寬度為標準去進行適配,保持該維度上和設計圖一致

2.3 今日頭條的適配方案

先科普幾個內容,

  • dp和px的轉換公式為:px = dp * density
  • dp轉換的場景都是通過DisplayMetrics來進行計算的,
  • DisplayMetrics#density 就是上述的density
  • DisplayMetrics#densityDpi 就是上述的dpi
  • DisplayMetrics#scaledDensity 字型的縮放因子,正常情況下和density相等,但是調節系統字型大小後會改變這個值

因為所有關於dp的計算都是通過DisplayMetrics這個類進行的。所以只需要針對這個類進行操作就可以了。

我簡單把DisplayMetrics類分為三個層面,第一個是System(可以理解成初始分配)的,第二個是APP(可以理解成Application)的,第三個是Activity的。當你適配的時候,儘量不要去修改第一個System中的Displaymetris的,因為可能第三方的庫不會按照你的方式去適配,所以這裡只修改後面兩個就可以了。第一個不修改是便於之後的還原!!!

以下是三個層面獲取DisplayMetrics中的程式碼:

// 系統的螢幕尺寸
final DisplayMetrics systemDm = Resources.getSystem().getDisplayMetrics();
// app整體的螢幕尺寸
final DisplayMetrics appDm = Utils.getApp().getResources().getDisplayMetrics();
// activity的螢幕尺寸
final DisplayMetrics activityDm = activity.getResources().getDisplayMetrics();
複製程式碼

接下來我們看看需要怎麼適配,這裡就只以螢幕寬度為基準進行相應的適配了。這裡模擬360dp為基準的適配,當然這個值你是可以修改成任何尺寸的!

  1. 先計算一下螢幕的寬度
//這裡widthPixels代表螢幕的寬度
activityDm.density = activityDm.widthPixels / 360;
複製程式碼
  1. 計算一下字型的density
//這裡通過一個比例確定activity字型的density
activityDm.scaledDensity = activityDm.density * (systemDm.scaledDensity / systemDm.density);
複製程式碼
  1. 計算相應的dpi
//上面有相應的公式
activityDm.densityDpi = (int) (160 * activityDm.density);
複製程式碼
  1. 複製相應的內容
//進行相應的賦值操作
appDm.density = activityDm.density;
appDm.scaledDensity = activityDm.scaledDensity;
appDm.densityDpi = activityDm.densityDpi;
複製程式碼

整體程式碼如下:

/**
 * 適配的主要程式碼
 *
 * @param activity        上下文
 * @param sizeInPx        你要適配的相應尺寸
 * @param isVerticalSlide 水平還是垂直為參考
 */
 private static void adaptScreen(final Activity activity,
                                    final int sizeInPx,
                                    final boolean isVerticalSlide) {
        // 系統的螢幕尺寸
        final DisplayMetrics systemDm = Resources.getSystem().getDisplayMetrics();
        // app整體的螢幕尺寸
        final DisplayMetrics appDm = Utils.getApp().getResources().getDisplayMetrics();
        // activity的螢幕尺寸
        final DisplayMetrics activityDm = activity.getResources().getDisplayMetrics();
        if (isVerticalSlide) {
            activityDm.density = activityDm.widthPixels / (float) sizeInPx;
            Log.e(TAG, "adaptScreen: "+activityDm.widthPixels );
        } else {
            activityDm.density = activityDm.heightPixels / (float) sizeInPx;
        }
        // 字型的縮放因子,這個是通過一個比例計算得來的!
        activityDm.scaledDensity = activityDm.density * (systemDm.scaledDensity / systemDm.density);
        // 計算得到相應的dpi
        activityDm.densityDpi = (int) (160 * activityDm.density);

        //進行相應的賦值操作
        appDm.density = activityDm.density;
        appDm.scaledDensity = activityDm.scaledDensity;
        appDm.densityDpi = activityDm.densityDpi;
    }
複製程式碼

因為上面涉及到橫豎屏的問題,所以這裡有個if判斷。上面是主要程式碼。

3 今日頭條的適配方案的一些問題

3.1 適配之後Toast的問題?

進行上面的適配之後,Toast會變得很小。其實也不難理解,因為你修改了APP的density,所以整個圖片的介面都會發生相應的變化也就很好理解了。那麼怎麼解決呢?其實就想上面說的,使用System的density對App和Activity進行還原。怎麼說呢?其實就是在show()方法之前還原,在之後在進行適配。

怎麼取消呢?看下面的程式碼。

    public static void cancelAdaptScreen(final Activity activity) {
        final DisplayMetrics systemDm = Resources.getSystem().getDisplayMetrics();
        final DisplayMetrics appDm = Utils.getApp().getResources().getDisplayMetrics();
        final DisplayMetrics activityDm = activity.getResources().getDisplayMetrics();
        activityDm.density = systemDm.density;
        activityDm.scaledDensity = systemDm.scaledDensity;
        activityDm.densityDpi = systemDm.densityDpi;

        appDm.density = systemDm.density;
        appDm.scaledDensity = systemDm.scaledDensity;
        appDm.densityDpi = systemDm.densityDpi;
    }
複製程式碼

其實就是使用System的density把APP和Activity的density修改回來就可以了!

然後在show()方法之後使用下面方法重新對介面進行適配!

    public static void restoreAdaptScreen(Activity activity, boolean isVerticalSlide, int sizeInPx) {
        final DisplayMetrics systemDm = Resources.getSystem().getDisplayMetrics();
        final DisplayMetrics appDm = Utils.getApp().getResources().getDisplayMetrics();
        final DisplayMetrics activityDm = activity.getResources().getDisplayMetrics();
        if (isVerticalSlide) {
            activityDm.density = activityDm.widthPixels / (float) sizeInPx;
        } else {
            activityDm.density = activityDm.heightPixels / (float) sizeInPx;
        }
        activityDm.scaledDensity = activityDm.density * (systemDm.scaledDensity / systemDm.density);
        activityDm.densityDpi = (int) (160 * activityDm.density);

        appDm.density = activityDm.density;
        appDm.scaledDensity = activityDm.scaledDensity;
        appDm.densityDpi = activityDm.densityDpi;
    }
複製程式碼

呼叫程式碼就變成了這個樣子

//取消適配
ScreenUtils.cancelAdaptScreen(this);
//彈出Toast
Toast.makeText(this, "點選了第一個內容", Toast.LENGTH_SHORT).show();
//重新適配
ScreenUtils.restoreVerticalAdaptScreen(this, 720);
複製程式碼

像什麼Toast、dialog什麼的都會出現上面的情況,所以解決辦法是一樣的

3.2 webview載入後發現density復原

由於 WebView 初始化的時候會還原 density 的值導致適配失效,繼承 WebView,重寫如下方法:

@Override
public void setOverScrollMode(int mode) {
    super.setOverScrollMode(mode);
    ScreenUtils.restoreAdaptScreen();
}
複製程式碼

特別感謝: blankj的Android 螢幕適配從未如斯簡單

相關文章