關于UI 稿的適配

1. 可以用今日頭條的適配方式,修改系統的density

1.1 先確定按寬還是高為基準,進行縮放適配。舉例選寬
1.2 代碼中獲取設備的寬的像素值 / 設計稿寬的 dp值 = 目標density
1.3 在Activity#onCreate中將這個density設置給系統DisplayMetrics
1.4 項目xml中的控件,全部設置為設計稿中一樣的大小
1.5 注意處理scaledDensity失效重新設置的相關問題

private static float sNoncompatDensity;
private static float sNoncompatScaledDensity;

private static void setCustomeDensity(@NonNull Activity activity, @NonNull final Application application){

     final DisplayMetrics appDisplayMetrics = application.getResources().getDisplayMetrics();

     if(sNoncompatDensity == 0){
         sNoncompatDensity = appDisplayMetrics.density;
         sNoncompatScaledDensity = appDisplayMetrics.scaleDensity;
         application.registerComponentCallbacks(new ComponentCallbacks(){
             @Override
             public void onConfigurationChanged(Configuration newConfig){
                 if(newConfig != null && newConfig.fontScale > 0){
                     sNoncompatScaledDensity = application.getResources().getDisplayMetrics().scaledDensity;
                 }
             }
             @Override
             public void onLowMemory(){}
         });
     }

     //設計圖360 dp的寬
     final float targetDensity = appDisplayMetrics.widthPixels / 360; 
     final float targetScaledDensity = targetDensity * (sNoncompatScaledDensity / sNoncompatDensity);
     final int targetDensityDpi = targetDensityDpi;

     appDisplayMetrics.density = targetDensity;
     appDisplayMetrics.scaledDensity = targetScaledDensity;
     appDisplayMetrics.densityDpi = targetDensityDpi;

     final DisplayMetrics activityDisplayMetrics = activity.getResources().getDisplayMetrics();
     activityDisplayMetrics.density = targetDensity;
     activityDisplayMetrics.scaledDensity = targetScaledDensity;
     activityDisplayMetrics.densityDpi = targetDensityDpi;
}

2. 基于頭條的原理,不修改DisplayMetrics#density,而是修改DisplayMetrics#xdpi

2.1 xml中使用pt單位
2.2 重寫Activity下 getResources() 函數
2.3 選擇適配基準是設計圖的寬或高
2.4 與今日頭條方案一樣計算出newXdpi,即用屏幕寬度/高度像素 / 設計圖dp數 * 72f
2.5 設置Resources【包含反射設置】

        override fun getResources(): Resources {
            return AdaptScreenUtils.adaptWidth(super.getResources(), 1080)
        }

        override fun getResources(): Resources {
            return AdaptScreenUtils.adaptHeight(super.getResources(), 1920)
        }

        override fun getResources(): Resources {
            return AdaptScreenUtils.closeAdapt(super.getResources())
        }
        #AdaptScreenUtils.java
        @NonNull
        public static Resources adaptWidth(@NonNull final Resources resources, final int designWidth) {
            float newXdpi = (resources.getDisplayMetrics().widthPixels * 72f) / designWidth;
            applyDisplayMetrics(resources, newXdpi);
            return resources;
        }

        @NonNull
        public static Resources adaptHeight(@NonNull final Resources resources, final int designHeight) {
            float newXdpi = (resources.getDisplayMetrics().heightPixels * 72f) / designHeight;
            applyDisplayMetrics(resources, newXdpi);
            return resources;
        }


        public static Resources closeAdapt(@NonNull final Resources resources) {
            float newXdpi = Resources.getSystem().getDisplayMetrics().density * 72f;
            applyDisplayMetrics(resources, newXdpi);
            return resources;
        }

        
        private static void applyDisplayMetrics(@NonNull final Resources resources, final float newXdpi) {
            resources.getDisplayMetrics().xdpi = newXdpi;
            Utils.getApp().getResources().getDisplayMetrics().xdpi = newXdpi;
            applyOtherDisplayMetrics(resources, newXdpi);
        }

        //反射設置Resources中的xdpi
        private static void applyOtherDisplayMetrics(final Resources resources, final float newXdpi) {
            if (sMetricsFields == null) {
                sMetricsFields = new ArrayList<>();
                Class resCls = resources.getClass();
                Field[] declaredFields = resCls.getDeclaredFields();
                while (declaredFields != null && declaredFields.length > 0) {
                    for (Field field : declaredFields) {
                        if (field.getType().isAssignableFrom(DisplayMetrics.class)) {
                            field.setAccessible(true);
                            DisplayMetrics tmpDm = getMetricsFromField(resources, field);
                            if (tmpDm != null) {
                                sMetricsFields.add(field);
                                tmpDm.xdpi = newXdpi;
                            }
                        }
                    }
                    resCls = resCls.getSuperclass();
                    if (resCls != null) {
                        declaredFields = resCls.getDeclaredFields();
                    } else {
                        break;
                    }
                }
            } else {
                applyMetricsFields(resources, newXdpi);
            }
        }

不會失效,這一點是最值得吹牛的,因為不論頭條的適配還是 AndroidAutoSize,都會存在 DisplayMetrics#density 被還原的情況,需要自己重新設置回去,最顯著的就是界面中存在 WebView 的話,由于其初始化的時候會還原 DisplayMetrics#density 的值導致適配失效,當然這點已經有解決方案了,但還會有很多其他情況會還原 DisplayMetrics#density 的值導致適配失效。而我這方案就是為了解決這個痛點,不讓 DisplayMetrics 中的值被還原導致適配失效。

概念解釋

  • px(Pixel像素)
    也稱為圖像元素,是作為圖像構成的基本單元,單個像素的大小并不固定,跟隨屏幕大小和像素數量的關系變化(屏幕越大,像素越低,單個像素越大,反之亦然)。所以在使用像素作為設計單位時,在不同的設備上可能會有縮放或拉伸的情況。

  • Resolution(分辨率)
    是指屏幕的垂直和水平方向的像素數量,如果分辨率是 1920*1080 ,那就是垂直方向有 1920個像素,水平方向有 1080個像素。

  • Dpi(像素密度)
    是指的是在系統軟件上指定的,屏幕上每英寸(1英寸 = 2.54厘米)距離中有多少個像素點。如果屏幕為 320*240,屏幕長 2 英寸寬 1.5英寸,Dpi = 320 / 2 = 240 / 1.5 = 160。它往往是寫在系統出廠配置文件的一個固定值;

  • Density(密度)
    這個是指屏幕上每平方英寸(2.54 ^ 2 平方厘米)中含有的像素點數量。

  • Android dip/dp (設備獨立像素)
    長度單位,同一個單位在不同的設備上有不同的顯示效果,具體效果根據設備的密度有關,詳細的公式請看下面 。

  • ppi(物理:屏幕像素密度)
    物理概念,它是客觀存在的不會改變。dpi是軟件參考了物理像素密度后,人為指定的一個值,這樣保證了某一個區間內的物理像素密度在軟件上都使用同一個值;

  • dp加上自適應布局和weight比例布局能解決90%的適配問題。因為并不是所有的1080P的手機dpi都是480,比如Google 的Pixel2(1920*1080)的dpi是420;

  • 寬高限定符適配:窮舉市面上所有的Android手機的寬高像素值,設定一個基準的分辨率,其他分辨率都根據這個基準分辨率來計算,在不同的尺寸文件夾內部,根據該尺寸編寫對應的dimens文件。但其有一個致命的缺陷,那就是需要精準命中才能適配,App包體積也會變大


重要公式

  • px = density * dp
  • density = Dpi / 160
  • px = (dpi / 160) * dp

dpi=\frac{\sqrt{(寬^2 + 高^2)}}{屏幕尺寸}\frac{【單位px】}{【單位inch】}



例:我們以一個 4.95 英寸 1920 * 1080 的 nexus5 手機設備為例:

  • Dpi:

    1. 條件中的4.95英寸,指的是屏幕對角線的長度,所以我們要求屏幕對角線的像素數,即\sqrt{1920^2 + 1080^2}=2202【勾股定理】
    2. 計算dpi: 2202/4.95 = 445
    3. 得到這個設備的dpi為445【每英寸距離中有445個像素】
  • Density
    上面得到每英寸長度中有440像素,那么density為沒平方英寸中的像素數量,應該為 445^2=198025

  • dip

    1. 先明白一個概念,所有顯示到屏幕上的圖像都是以px為單位
    2. dip是我們開發中使用的長度單位,最后它也要裝換成px
    3. 計算這個設備上1dip等于多少px:
      px = dip * Dpi / 160
      px = 1 * 445 / 160
    4. 通過上面的計算可以看出在此設備上 1dip = 2.78px,那么這是一個真實的故事嗎?
      nonono,其中的關鍵值dpi并不是我們算出來的445
Android系統定義的Dpi

上面計算的 445Dpi 是在 4.95英寸下的 1920*1080 手機,那如果是 4.75英寸下的呢? 4.55 英寸下的呢?。。。可見是很麻煩的,同一個分辨率在不同的屏幕尺寸上 Dpi 如果這么算是不相同的。為了解決這個問題,Android 中內置了幾個默認的 Dpi,在特定的分辨率下自動調用,也可以手動在配置文件中修改。

ldpi mdpi hdpi xhdpi xxhdpi
分辨率 240x320 320x480 480x800 720x1280 1080x1920
系統dpi 120 160 240 320 480
基準比例 0.75 1 1.5 2 3

這是內置的 Dpi ,啥意思? 在 1920*1080 分辨率的手機上 默認就使用 480 的 dpi ,不管的你的尺寸是多大都是這樣,除非廠家手動修改了配置文件,這個我們后面再說。
我們親自嘗試一下

<TextView    
  android:id="@+id/tv"   
  android:layout_width="200dp"    
  android:layout_height="100dp"    
  android:text="Hello World!" />

這是一個 textview,高為 200dp 寬為 100dp 。按照我們之前的公式手動計算:

height = 100 x 445 / 160 = 278.5 px
width  = 200 x 445 / 160 = 556.25 px

我們用下列代碼獲取到控件的實際像素看看是否相同:

layout = (RelativeLayout)findViewById(R.id.la);
//要在控件繪制完成后才能獲取到相關信息,所以這里要監聽繪制狀態
layout.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener()     {    
  public boolean onPreDraw() {        
    Log.d("test", textView.getHeight() + "/" + textView.getWidth());       
    return true;    
  }
});

//輸出的結果為:300/600
//內部計算過程為:
height = 100 x 480 / 160 = 300px
width  = 200 x 480 / 160 = 600px

其中的 160 是基準值不會變的,100200 是我們設置的 dp ,那么這 480是從何而來的?說好的 445 呢?
找到我們手機中的 /system/build.prop 文件,其中有一行是這樣:

ro.sf.lcd_density=480

這就指定了這個機型使用的dpi是多少,還有一種情況是沒有這一行(我在模擬器中發現過),那么應該是根據表格中的分辨率來自動設置。

說到底,因為有dpi這個動態的系數,我們在使用dp的時候才能兼容不同分辨率的設備。
到這里,應該都明白了 dpi 的由來,以及系統 dpi 跟物理 dpi 并不一定相同。
在系統中使用的全部都是系統 dpi,沒有使用物理 dpi,也獲取不到物理 dpi。物理 dpi 主要用于廠家對于手機的參數描述
(也可以看做 ppi )!

設備 1,屏幕寬度為 1080px,480DPI,屏幕總 dp 寬度為 1080 / (480 / 160) = 360dp

來源1
來源2
來源3

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,488評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,034評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,327評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,554評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,337評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,883評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,975評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,114評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,625評論 1 332
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,555評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,737評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,244評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,973評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,615評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,343評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,699評論 2 370

推薦閱讀更多精彩內容