Android螢幕適配很麻煩嗎?不!太簡單了。。。(持續更新)

勞爾發表於2018-06-25

轉載請作明出處:juejin.im/post/5b3094…

前言

作為一個Android開發人員,你還在為了適配各種尺寸的螢幕而苦惱嗎?你還在為了出現一個新的機型而修改著數不盡的dimens和layout嗎?你還在為了UI給的奇葩尺寸的設計圖而絞盡奶汁計算距離嗎?如果你為了這些事情而苦惱,那麼看完這篇文章,希望可以幫你減少開發時間,減緩生命的流逝速度。。。

Android螢幕適配很麻煩嗎?不!太簡單了。。。(持續更新)
不知道大家有沒有看過前一段時間今日頭條技術團隊發表的一篇關於Android螢幕適配的文章:一種極低成本的Android螢幕適配方式。沒有看過的朋友可以先看看了解一下再回來,可以更好的理解。我是無意中點開的這篇文章,但是看過之後眼前一亮-------Android螢幕適配要是真的這麼簡單,那些辛辛苦苦沒日沒夜做適配的前輩們是不是死得太慘了。。。。。。。

測試與思考

不得不說今日頭條的大神們的想法真的非常獨到,成本極其低廉,還特別好用。他們給出的最終方案是這樣的:

private static float sRoncompatDennsity;
private static float sRoncompatScaledDensity;
private void setCustomDensity(@NonNull Activity activity, final @NonNull Application application) {

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

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

            @Override
            public void onLowMemory() {

            }
        });
    }

    //計算寬為360dp 同理可以設定高為640dp的根據實際情況
    final float targetDensity = appDisplayMetrics.widthPixels / 360;
    final float targetScaledDensity = targetDensity * (sRoncompatScaledDensity / sRoncompatDennsity);
    final int targetDensityDpi = (int) (targetDensity * 160);

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

    //activity
    final DisplayMetrics activityDisplayMetrics = activity.getResources().getDisplayMetrics();

    activityDisplayMetrics.density = targetDensity;
    activityDisplayMetrics.densityDpi = targetDensityDpi;
    activityDisplayMetrics.scaledDensity = targetScaledDensity;
}
複製程式碼

看到這篇文章之後我趕緊就寫了一個demo測試了一下,發現了一點小問題。 我們UI給出的設計圖尺寸為1334*720,如果我以寬度作為適配標準的話,按照xhdpi 1dp=2px 的圖進行參照,設計圖寬720px的,螢幕的寬度應為360dp,也就是這樣:

final float targetDensity = appDisplayMetrics.widthPixels / 360;
複製程式碼

這樣做的話寬度適配的比例是沒有任何問的,但是我在想,如果某一個頁面需要以高度來做適配(也就是內容剛好縱向填充全屏)的話,是不是改成這樣就可以了:

final float targetDensity = appDisplayMetrics.heightPixels / 667;
複製程式碼

但是執行之後發現,高度上的差異很大,執行在不同解析度和尺寸的手機上,頁面中的每一部分內容在縱向上的比例不盡相同,沒有達到很好的適配的效果。 思考了許久過後我發現一個問題:我手邊的測試機的寬度是兩個720和兩個1080,而高度有1280,1440,1780和一個全面屏的2160。Android的開原性導致了Android裝置的尺寸的碎片化太嚴重,而通過檢視測試機的尺寸引數會發現,如果用這四個手機來測試的話,寬度可以直接整除,而高度不可以(並且我手邊的測試機的寬度也可以整除,如果有寬度沒法整除的手機呢?)。但是用今日頭條給出的方法,做除法後結果會取整,那會不會是由於用縱向計算出來的density取整影響了精度,從而導致了效果不盡人意呢?

問題修復

發現上述問題之後我就著手去修改,將計算結果取餘後在賦值給targetDensity,經過反覆的測試與實驗,我重新修改了targetDensity的計算方法:

float targetDensity = 0;
try {
    Double division = Operation.division(appDisplayMetrics.heightPixels, 667);
    //由於手機的長寬不盡相同,肯定會有除不盡的情況,有失精度,所以在這裡把所得結果做了一個保留兩位小數的操作
    DecimalFormat df = new DecimalFormat("0.00");
    String s = df.format(division);
    targetDensity = Float.parseFloat(s);
} catch (NumberFormatException e) {
    e.printStackTrace();
}
複製程式碼

但是有熱心的網友給我留言,說如果把系統的語言改成葡萄牙語之後會報異常,頁面完全就是慘不忍睹。。。抱著對外國曆史友人的疑問我繼續去測試,發現了這樣的問題:

Android螢幕適配很麻煩嗎?不!太簡單了。。。(持續更新)
仔細看!!!小數點竟然是中文逗號(目前發現的語言當中,葡語和印尼語是這樣,其他語言未經測試)!!!沒辦法,只能繼續修改了,後來經過這位熱心網友提醒,我將這部分程式碼修改成:

float targetDensity = appDisplayMetrics.heightPixels / 667f;
複製程式碼

這樣一來就完全沒有問題了,也不需要做保留兩位小數處理了(發現問題的朋友們,原諒我學藝不精。。。也感謝這位熱心網友的指正)

繼續測試後發現,高度上的適配結果讓人非常滿意。可是還有一個問題,我們一般來說做適配都是以手機的寬度為基準,但是一個app裡面避免不了偶爾一兩個頁面是按照高度為基準(就是內容縱向填充全屏的頁面)做適配的。但是上述方法只能保證一個方向,那我就讓它可以自由的切換適配的基準方向不就好了。

最終方案

繼續修改之後我得到了最終的方案,修改過後這個類中的所有內容如下:

private static float appDensity;
private static float appScaledDensity;
private static DisplayMetrics appDisplayMetrics;
private static int barHeight;

public static void setDensity(@NonNull Application application) {
    //獲取application的DisplayMetrics
    appDisplayMetrics = application.getResources().getDisplayMetrics();
    //獲取狀態列高度
    barHeight = AppUtils.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的話,直接用下面那個方法就好了)
public static void setDefault(Activity activity) {
    setAppOrientation(activity, AppUtils.WIDTH);
}

//此方法用於在某一個Activity裡面更改適配的方向
public static void setOrientation(Activity activity, String orientation) {
    setAppOrientation(activity, orientation);
}

/**
 * targetDensity
 * targetScaledDensity
 * targetDensityDpi
 * 這三個引數是統一修改過後的值
 * <p>
 * orientation:方向值,傳入width或height
 */
private static void setAppOrientation(@Nullable Activity activity, String orientation) {

    float targetDensity;

    if (orientation.equals("height")) {
        targetDensity = (appDisplayMetrics.heightPixels - barHeight) / 667f;
    } else {
        targetDensity = appDisplayMetrics.widthPixels / 360f;
    }

    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;
}
複製程式碼

在這個類的初始化方法裡面我是預設的以寬度來作為基準(這是在Activity中設定的方法,存在於此Activity下的fragment,dialog和PopupWindow都會受到此效果的影響,也就是說,在Activity中設定一次之後,Activity下的其他子View都無需再設定一次)。

使用方法

自己建立一個類,將最終方案裡面的程式碼複製貼上就可以使用了

使用方法:在Application的onCreate()方法中:

Android螢幕適配很麻煩嗎?不!太簡單了。。。(持續更新)
在BaseActivity中:

Android螢幕適配很麻煩嗎?不!太簡單了。。。(持續更新)
如果只是適配一個方向的話,只設定這兩句就可以了(我在utils裡面設定了預設按照寬度適配,可以根據自己的需求修改預設的適配方向,見下圖)

Android螢幕適配很麻煩嗎?不!太簡單了。。。(持續更新)
若app中有某一個頁面需要縱向適配的話:

/**
 *
 * 由於是個人封裝,此方法需要寫在onCreate()中的setContentView()方法前面,切換方向的效果才會生效
 */
@Override
public void setOrientation() {
    Density.setOrientation(this, AppUtils.HEIGHT);
}
複製程式碼

Android螢幕適配很麻煩嗎?不!太簡單了。。。(持續更新)
最後貼出適配的效果圖(顏色只是為了看的直觀一點。。。)

橫向:

Android螢幕適配很麻煩嗎?不!太簡單了。。。(持續更新)
縱向:
Android螢幕適配很麻煩嗎?不!太簡單了。。。(持續更新)

敲黑板!!!

使用此方法,只需要一個dimens檔案,一個layout檔案就足矣,在xml佈局中直接只用dp就可以了(Android P的劉海屏需要單獨適配layout)

結語

由於是自己寫的demo,還沒有大面積測試,要是各位看官有條件大範圍測試的話,出現什麼問題可以反饋給我,我們可以一起討論該如何修改,共同進步。 這是我入行以來寫的第一篇文章,有寫的不好的地方歡迎指正,以後還會繼續努力多寫文章的,好的東西需要分享。

這裡是github地址,demo裡面還有BaseRecyclerViewAdapterHelperQMUI的使用方法

留個聯絡方式,方便溝通 QQ:850701071

這篇文章如果幫到你的話,點個喜歡再走唄

相關文章