今日頭條屏幕適配方案落地研究

前言

如今給你們推薦一種極低版本的 Android 屏幕適配方案,就是今日頭條適配方案,「極低成本」這四個字正是今日頭條的適配文章標題。android

衆所周知,安卓的屏幕碎片化極其嚴重,適配一直是從事安卓開發人員十分頭疼的事情。前期,因爲公司支持的平板款式單一,只須要作幾款平板的適配便可,選用了 smalledtWidth(最小寬度)適配,可是這個方案在增長新屏幕時且原 dimens 文件沒法很好適配時,就須要增長新屏幕的最小寬度 dimens 文件了,比較麻煩並且會增長項目大小(雖然只是幾個文件),並且這種屏幕適配極度依賴設備的屏幕密度,叫density。爲了講解更清楚,這裏須要引入幾個公式:面試

px = density * dp
dp : 安卓開發人員經常掛在嘴上的長度單位
px : 設計人員眼中的長度單位
density = dpi / 160
所以, px = dp * (dpi/160)
dpi : 根據屏幕真實分辨率和尺寸計算得出
舉個例子:屏幕分辨率爲 1920 *1080,屏幕尺寸爲5寸(屏幕斜邊長度cm/0.3937), 則  dpi = √(寬度²+ 高度²)/屏幕尺寸

所以,屏幕密度相當重要,屏幕密度怎麼來的?廠商寫入一個 system/build.prop 文件,有時還會寫錯,就咱們一款華爲平板,獲取的屏幕密度是2,可是手工測量並按公式獲得實際屏幕密度是1.56。致使咱們的適配方案在那款平板就失效了。性能優化

本人一直在尋找能夠一勞永逸的屏幕適配方案,今日頭條是選定基準分辨率,基於設備屏幕分辨率計算出新的屏幕密度進行適配,保證全部設備的顯示效果一致,完美避開上面那款設備的問題。推薦給你們。架構

各平板數據比較

首先,我詳細記錄了公司主流設備的參數,新方案確定要對主流設備都能完美適配,這纔是入門門檻。
app

能夠看到橫向是幾種設備,豎向是一些參數,其中中英文混雜,這是爲何呢?這是我故意的,中文是設備原始參數,英文是根據今日頭條方案原理計算的。由於,今日頭條的目的是全部設備的顯示效果一致。可是設備的分辨率是不一樣的,怎麼顯示一致呢?簡單述之,就是縮放,按寬度縮放的。可能有人會有疑問,縮放後的效果圖放不下,顯示不完整怎麼辦?框架

咱們看看上面的數據,能夠看到按照三星6.0基準進行縮放,效果圖在三星4.1這款設備寬度上的顯示,是按768乘以new density ,也就是 1.04166 進行放大,不用按計算器了,就是800px,完美適配。那麼高度呢,1024 也乘以 new density,發現是1066px,比實際高度像素值 1280px 小,不會出現顯示不全的現象。可能有人會問了,這不是多出來了麼,會不會留空白啊?對,好問題,因此合格的開發在豎向佈局上增長自適應權重,以應對這種狀況。固然,橫向也須要考慮自適應權重。ide

同理,可得知效果圖在華爲8.0設備的寬度像素是 1600px, 也比實際設備寬度 1852px 小,也能顯示徹底。佈局

爲何看起來更小了?(頭條方案跟最小寬度方案比較)

對的,跟原先的比起來,是更小了,包括圖片更小,文字更小。這是爲何呢?且聽我細細道來… …性能

你們都知道,安卓有 mdpi、hdpi、xhdpi後綴的文件,具體使用有 drawable-mdpi、drawable-hdpi,或者mipmap-mdpi、mipmap-hdpi, 又或者 values-mdpi、values-hdpi, 這些都是安卓自帶的屏幕適配方案,只是不太好用嗎,常常出問題。那麼,這些文件都是怎麼使用的呢,這又涉及到了屏幕密度這個屬性,關聯以下:學習

image.png

  1. 平板A 三星平板5100 的屏幕密度是1.33125,大於mdpi,小於hdpi,向上取整,因此屬於hdpi
  2. 平板B 三星平板P355C 的屏幕密度是1,屬於mdpi
  3. ldpi:mdpi:hdpi:xhdpi:xxhdpi:xxxdpi = 0.75:1:1.5:2:3:4 = 3:4:6:8:12:16
  4. 上述比值乘以12,就是 36:48:72:144:192,恰好就是icon尺寸
  5. 咱們會看到,最小寬度適配方案,values-hdpi 的值是 values-mdpi 的值乘以 0.8

0.8 的參數

  1. 寬高100dp的正方形圖片,平板A會顯示100px,平板B會乘以1.5,顯示成150px,致使偏大
  2. 因爲平板B的屏幕密度是 1.33125, 最好 顯示成 100* 1.33125
  3. 1.33125/ 1.5 = 0.8875 約爲 0.8

sw600dp-dpi

  1. sw : small width,就是最小寬度是600dp,
  2. px -> dp : dp = px / density
  3. 平板A: 800 /1.33125 = 600.93
  4. 平板B: 768/1 = 768

上述兩個平板,一個是600dp,一個是768dp,都是大於600dp,平板A使用sw600dp-hdpi,平板B使用sw600dp-mdpi

最後稱述

平板A、B 同時顯示一個 100px 的圖片:

  1. 按最小寬度適配:100 1.5 0.8 = 120 ,圖片會顯示成 120px
  2. 按今日頭條適配: 100 * 1.04166 = 104.166,圖片會顯示成 104.166 px
  3. 因此今日頭條方案顯示的圖片就更小了。

那麼,哪一個更好呢?咱們再來看看一個極端,顯示一個 平板B 的填滿寬度的圖片, 768px:

  1. 按最小寬度適配:768px 1.5 0.8 = 921.6px ,圖片會顯示成 921.6px, 遠遠超出平板A的尺寸,此時開發人員須要手動干預
  2. 按今日頭條適配: 768px * 1.04166 = 799.99488,圖片能夠當作顯示成 800 px
  3. 優勢很明顯,佈局更簡單

嚴謹的你,可能會問了,那顯示超過768px呢?
很差意思,咱們的基準就是 768,不會超過他了。

smallesWidth 方案遷移

咱們原項目使用的是 smallestWidth 方案,經試驗遷移代價很低,經研究有以下兩個方案。

  1. 刪除全部適配 smallestWidth 的dimens 文件夾,只保留dp 值是1:1 的 dimens 文件便可;
  2. 不想刪除亦可,將全部的 dimens 文件都覆蓋成 dp 值是1:1 的 dimens 文件便可

優缺點

優勢

  1. 使用成本很是低,操做很是簡單,使用該方案無需增長dimens 文件,修改代碼,完虐其餘屏幕適配方案
  2. 侵入性很是低,切換幾乎瞬間完成,試錯成本接近爲0
  3. 修改的 density 是全局的,一次修改,終生受益。
  4. 不會有任何性能的損耗
  5. 今日頭條 大廠保證

缺點

一、 第三方佈局庫, 未按項目效果圖佈局,全局修改 density 致使修改第三方佈局,形成顯示界面問題
二、與 smallestwith 適配方案不兼容,切換回來比較麻煩

issue

一個 Bitmap 的density 問題

在某處,開啓今日頭條適配方案,全局修改屏幕密度,獲取 ImageView 的 Bitmap 的寬高,發現獲取的寬高和實際的寬高(佈局出來觀察)不一致。經查閱源碼,發現 Bitmap 也有一個 density, 懷疑未被修改。

隨決定,修改 sDefaultDensity 值,查閱代碼,發現 sDefaultDensity 是靜態私有,因而召喚反射大法

[圖片上傳失敗...(image-543f05-1559720957691)]

測試 Ok, 收工。

附錄(適配核心代碼)

  • initAppDensity 方法 Application 調用,記錄默認屏幕密度
  • setDefault 和 setOrientation 方法 Activity 調用,設置新的屏幕密度
  • resetAppOrientation 方法,恢復屏幕密度
// * ================================================
  // * 本框架核心原理來自於 <a href="https://mp.weixin.qq.com/s/d9QCoBP6kV9VSWvVldVVwA">今日頭條官方適配方案</a>
  // * <p>
  // * 本框架源碼的註釋都很詳細, 歡迎閱讀學習
  // * <p>
  // * 任何方案都不可能完美, 在成本和收益中作出取捨, 選擇出最適合本身的方案便可, 在沒有更好的方案出來以前, 只有繼續忍耐它的不完美, 或者本身做出改變
  // * 既然選擇, 就不要抱怨, 感謝 今日頭條技術團隊 和 張鴻洋 等人對 Android 屏幕適配領域的的貢獻
  // * <p>
  // * ================================================
 // */
  private static final int WIDTH = 1;
  private static final float DEFAULT_WIDTH = 768f; //默認寬度
  private static final float DEFAULT_HEIGHT = 1024f; //默認高度
  private static float appDensity;
  /**
  * 字體的縮放因子,正常狀況下和density相等,可是調節系統字體大小後會改變這個值
  */
  private static float appScaledDensity;
  /**
  * 狀態欄高度
  */
  private static int barHeight;
  private static DisplayMetrics appDisplayMetrics;
  private static float densityScale = 1.0f;

  /**
  * application 層調用,存儲默認屏幕密度
  *
  * @param application application
  */
    public static void initAppDensity(@NonNull final Application application) {
    //獲取application的DisplayMetrics
    appDisplayMetrics = application.getResources().getDisplayMetrics();
    //獲取狀態欄高度
    barHeight = getStatusBarHeight(application);
    if (appDensity == 0) {
   //初始化的時候賦值
    appDensity = appDisplayMetrics.density;
    appScaledDensity = appDisplayMetrics.scaledDensity;
       //添加字體變化的監聽
       application.registerComponentCallbacks(new ComponentCallbacks() {
       @Override
       public void onConfigurationChanged(Configuration newConfig) {
         //字體改變後,將appScaledDensity從新賦值
         if (newConfig != null && newConfig.fontScale > 0) {
                       appScaledDensity = application.getResources().getDisplayMetrics().scaledDensity;
          }
       }

       @Override
       public void onLowMemory() {
       }
     });
   }
 }

 /**
 * 此方法在BaseActivity中作初始化(若是不封裝BaseActivity的話,直接用下面那個方法就行了)
 *
 * @param activity activity
 */
 public static void setDefault(Activity activity) {
  setAppOrientation(activity, WIDTH);
 }

 /**
 * 好比頁面是上下滑動的,只須要保證在全部設備中寬的維度上顯示一致便可,
 * 再好比一個不支持上下滑動的頁面,那麼須要保證在高這個維度上都顯示一致
 *
 * @param activity activity
 * @param orientation WIDTH HEIGHT
 */
 public static void setOrientation(Activity activity, int orientation) {
   setAppOrientation(activity, orientation);
 }

 /**
 * 重設屏幕密度
 *
 * @param activity activity
 * @param orientation WIDTH 寬,HEIGHT 高
 */
 private static void setAppOrientation(@NonNull Activity activity, int orientation) {

    float targetDensity;

    if (orientation == HEIGHT) {
        targetDensity = (appDisplayMetrics.heightPixels - barHeight) / DEFAULT_HEIGHT;
    } else {
        targetDensity = appDisplayMetrics.widthPixels / DEFAULT_WIDTH;
    }

    float targetScaledDensity = targetDensity * (appScaledDensity / appDensity);
    int targetDensityDpi = (int) (160 * targetDensity);
    // 最後在這裏將修改事後的值賦給系統參數,只修改Activity的density值
    DisplayMetrics activityDisplayMetrics = activity.getResources().getDisplayMetrics();
    activityDisplayMetrics.density = targetDensity;
    activityDisplayMetrics.scaledDensity = targetScaledDensity;
    activityDisplayMetrics.densityDpi = targetDensityDpi;

    densityScale = appDensity / targetDensity;
    setBitmapDefaultDensity(activityDisplayMetrics.densityDpi);
  }

 /**
 * 重置屏幕密度
 *
 * @param activity activity
 */
 public static void resetAppOrientation(@NonNull Activity activity) {
    DisplayMetrics activityDisplayMetrics = activity.getResources().getDisplayMetrics();
    activityDisplayMetrics.density = appDensity;
    activityDisplayMetrics.scaledDensity = appScaledDensity;
    activityDisplayMetrics.densityDpi = (int) (appDensity * 160);

    densityScale = 1.0f;
    setBitmapDefaultDensity(activityDisplayMetrics.densityDpi);
 }

 /**
 * 獲取狀態欄高度
 *
 * @param context context
 * @return 狀態欄高度
 */
 private static int getStatusBarHeight(Context context) {
   int result = 0;
   int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
   if (resourceId > 0) {
   result = context.getResources().getDimensionPixelSize(resourceId);
    }
   return result;
 }

 /**
 * 設置 Bitmap 的默認屏幕密度
 * 因爲 Bitmap 的屏幕密度是讀取配置的,致使修改未被啓用
 * 全部,放射方式強行修改
 * @param defaultDensity 屏幕密度
 */
 private static void setBitmapDefaultDensity(int defaultDensity) {
    //獲取單個變量的值
   Class clazz;
   try {
      clazz = Class.forName("android.graphics.Bitmap");
      Field field = clazz.getDeclaredField("sDefaultDensity");
      field.setAccessible(true);
      field.set(null, defaultDensity);
      field.setAccessible(false);
   } catch (ClassNotFoundException e) {
   } catch (NoSuchFieldException e) {
   } catch (IllegalAccessException e) {
      e.printStackTrace();
   }
 }

 /**
 * 屏幕密度縮放係數
 *
 * @return 屏幕密度縮放係數
 */
 public static float getDensityScale() {
    return densityScale;
 }
更多資料分享歡迎Android工程師朋友們加入安卓開發技術進階互助:856328774免費提供安卓開發架構的資料(包括Fultter、高級UI、性能優化、架構師課程、 NDK、Kotlin、混合式開發(ReactNative+Weex)和一線互聯網公司關於Android面試的題目彙總。
相關文章
相關標籤/搜索