前一段時間無意中看到今日頭條的適配方案,使用到專案中,感覺真的是無比絲滑。所以特意寫一篇文章分享給小夥伴們!
本文知識點:
- 為什麼要做螢幕適配
- 今日頭條的適配方案(劃重點)
- 今日頭條的適配方案的一些問題
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是怎麼計算的呢?
上面圖片說明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為基準的適配,當然這個值你是可以修改成任何尺寸的!
- 先計算一下螢幕的寬度
//這裡widthPixels代表螢幕的寬度
activityDm.density = activityDm.widthPixels / 360;
複製程式碼
- 計算一下字型的density
//這裡通過一個比例確定activity字型的density
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;
複製程式碼
整體程式碼如下:
/**
* 適配的主要程式碼
*
* @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();
}
複製程式碼