Android螢幕適配很難嘛?其實也就那麼回事

weixin_34050427發表於2019-01-17

前言

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

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

測試與思考

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

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,如果我按照寬度作為適配標準的話,按照設計圖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(); 
} 

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

最終方案

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

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

//此方法在Application的onCreate方法中呼叫 Density.setDensity(this); 
public static void setDensity(@NonNull Application application) { 
 //獲取application的DisplayMetrics 
 appDisplayMetrics = application.getResources().getDisplayMetrics(); 

 if (appDensity == 0) { 
 //初始化的時候賦值(只在Application裡面初始化的時候會呼叫一次) 
 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() { 
 } 
 }); 
 } 

 //呼叫修改density值的方法(預設以寬度作為基準) 
 setAppOrientation(null, AppUtils.WIDTH); 
} 

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

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

 float targetDensity = 0; 
 try { 
 Double division; 
 //根據帶入引數選擇不同的適配方向 
 if (orientation.equals("height")) { 
 //appDisplayMetrics.heightPixels/667 
 division = Operation.division(appDisplayMetrics.heightPixels, 667); 
 } else { 
 division = Operation.division(appDisplayMetrics.widthPixels, 360); 
 } 
 //由於手機的長寬不盡相同,肯定會有除不盡的情況,有失精度,所以在這裡把所得結果做了一個保留兩位小數的操作 
 DecimalFormat df = new DecimalFormat("0.00"); 
 String s = df.format(division); 
 targetDensity = Float.parseFloat(s); 
 } catch (NumberFormatException e) { 
 e.printStackTrace(); 
 } 

 float targetScaledDensity = targetDensity * (appScaledDensity / appDensity); 
 int targetDensityDpi = (int) (160 * targetDensity); 

 /** 
 * 
 * 最後在這裡將修改過後的值賦給系統引數 
 * 
 * (因為最開始初始化的時候,activity為null,所以只設定application的值就可以了... 
 * 所以在這裡判斷了一下,如果傳有activity的話,再設定Activity的值) 
 */ 
 if (activity != null) { 
 DisplayMetrics activityDisplayMetrics = activity.getResources().getDisplayMetrics(); 
 activityDisplayMetrics.density = targetDensity; 
 activityDisplayMetrics.scaledDensity = targetScaledDensity; 
 activityDisplayMetrics.densityDpi = targetDensityDpi; 
 } else { 
 appDisplayMetrics.density = targetDensity; 
 appDisplayMetrics.scaledDensity = targetScaledDensity; 
 appDisplayMetrics.densityDpi = targetDensityDpi; 
 } 
} 

這是修改之後的所有內容,不懂的地方可以看一下里面的註釋,在裡面我是預設的以寬度來作為基準(這是在Activity中設定的方法,存在於此Activity下的fragment,dialog和PopupWindow都會受到此效果的影響,也就是說,在Activity中設定一次之後,Activity下的其他子View都無需再設定一次)。

使用方法

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

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

15233854-2733c0842c3ebe2b

如果只是適配一個方向的話,只設定這一句就可以了(我在utils裡面設定了預設按照寬度適配,可以根據自己的需求修改預設的適配方向,見下圖)

15233854-75a539e77a7605f6

若app中有某一個頁面需要縱向適配的話:

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

/** 
 * 
 * 如果在一個Activity裡面切換了適配方向的話,需要在destroy裡面將方向設定為預設的方向, 
 * 因為切換方向修改的是Activity的值,但是application的也會覆蓋掉(原因還沒有搞清楚...), 
 * 權衡利弊之後就在onDestroy這個生命週期裡面重新初始化了一下方向(因為用高度作為適配基準的頁面 
 * 少之又少,這樣可以最大程度的減少對程式功能性的影響) 
 */ 
@Override 
protected void onDestroy() { 
 super.onDestroy(); 
 Density.setOrientation(this, AppUtils.WIDTH); 
} 

由於在某一個Activity裡面切換方向之後,我修改掉的是Activity中的值(activityDensity),但是返回再點選其他頁面之後發現其他頁面的適配方向也被修改掉了,於是乎權衡利弊之後我就用了這個相對來說影響最小的辦法:在需要修改適配方向的Activity中的onDetroy生命週期裡面,再手動將方向改成預設。。。(搗鼓了很久實在是想不到更好的辦法了,如果各位看官有其他的好辦法可以給我留言)。

15233854-7f012515dd07569b

最後貼出縱向適配的效果圖,頁面中藍色背景的TextView高度是固定的150dp(只是我自己寫的一個很簡單的頁面,不要嫌醜。。。):

15233854-f12fbc6be90c5507

敲黑板!!!

用此方法寫適配,只需要一個dimens檔案,一個layout檔案就足矣,在xml佈局中直接只用dp就可以了(Android P的劉海屏需要單獨適配layout,全面屏手機可以隱藏的虛擬按鍵似乎也需要單獨適配。。。)

15233854-a4d98c0e7c07db89

結語

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

15233854-9a868df0e5641f97

資料領取

關注我後臺私信回覆【乾貨分享】

領取獲取往期Android高階架構資料、原始碼、筆記、視訊。高階UI、效能優化、架構師課程、NDK、混合式開發(ReactNative+Weex)微信小程式、Flutter全方面的Android進階實踐技術

相關文章