需求-爲什麼要適配
app首頁是一個不可以滑動的頁面,因此需要高和寬同時適配;內容比較多——日期,抽獎按鈕,步數錶盤,步數柱狀圖,底部的Tab,廣告等;首頁中心是一個圓形錶盤,高度取剩餘高度,寬度取屏幕寬度,並且考慮高寬相等,兩方面適應之後決定圓形錶盤的直徑,導致在不同手機上效果千奇百怪。
適配方案的選擇
使用dp作爲單位,設置空間寬度時使用wrap_content, match_parent, weight等參數,可以滿足大部分業務需求。但是主頁需要精確的適配,不適合。
SamllestWidth適配方案,維護麻煩,而且首頁不是比例的問題,不是隻有寬度適配,需要高度寬度同時適配,只在一個維度上的適配不能滿足。
最終使用的AutoSize滿足一般頁面的適配和首頁的寬度適配,在高度上自行設計同時滿足高度寬度適配。
具體適配方案
1.使用AndroidAutoSize屏幕適配框架
使用dp爲單位,讓屏幕寬度不等於設計圖的都能很好的適配,以寬爲基準進行適配。
可以解決大部分頁面效果不統一的情況。
2.首頁需要寬度高度同時適配
設定寬度基準之後,假設寬度都是360的基礎上,計算首頁中心fragment的高度:
fragment高度=屏幕高度 - 系統導航欄高度 - 系統狀態欄高度 - app廣告高度 - app導航tab高度 - 頭部按鈕高度
根據不同的fragment高度,劃分到5檔,5個區間高度分別爲:
- 大於520dp
- 520 到 456
- 455 到 379
- 378 到 342
- 小於342
由UI分別按照比例設計5套UI,儘可能保證在不同寬高比的屏幕上,視覺效果統一。
代碼上,根據fragment高度,加載5套佈局文件中的一套。
3.ConstraintLayout和Space保證首頁佈局效果
佈局文件的區別體現在:
1.字體大小不同
2.控件大小不同
因爲佈局的高度在一定範圍內而不是確定的值,所以需要控件之間的間隔在一定範圍內可以自適應。
首頁內容比較多,使用ConstaintLayout佈局,減少嵌套層次。
使用Space控件來做組件之間的間隔,Space控件的高度使用權重(layout_constraintVertical_weight)設置,保證5個檔位內的高度範圍內,按照比例分配間隔高度。
Space 經常用於組件之間的縫隙,其
draw()
爲空,減少了繪製渲染的過程。組件之間的距離使用 Space 會提高了繪製效率,特別是對於動態設置間距會很方便高效。
正是因爲
draw()
爲空,對該 view 沒有做任務繪製渲染,所以不能對 Space 設置背景色。
4.缺點
因爲首頁+左滑+右滑,共3個fragment,都是圓盤結構,都需要寬度高度同時適配,所以一共需要3*5=15個layout文件。
5.升級版方案
首頁改版爲可滑動,通過佈局自適應不能滿足自動填充一整屏幕的需求,改爲通過代碼計算高度進行設置。
具體實施——工具類ScreenAutoSizeUtil
將比例寫入map裏面,將其他元素高度設定之後,用代碼通過比例和剩餘總高度計算真實高度,設置到代碼中。節省了佈局文件,可讀性變差,靈活性高。(待補充)
AndriodAutoSize原理
原理同今日頭條適配方案原理相同。
原因
設計稿寬度是固定的,只有一種寬度,而屏幕的真是寬度其實是有很多的。
雖然dp的出現目的在於取代像素,儘量保證屏幕的寬度統一,但是屏幕碎片化嚴重,導致以dp爲單位,寬度還是有很多種,不能統一。
原理
在屏幕繪製的時候,需要把單位換算成px,也就是最終是使用px爲單位進行繪製的。平時我們使用dp爲單位寫佈局之後,最終要使用公式px=dp * density轉換爲px進行繪製。
這套方案,相當於修改了dp的定義——dp代表的是屏幕寬度平均分成【設計圖寬度】的份數之後的1單位長度。
比如屏幕寬度是1080px,用dp爲單位時屏幕寬度是480dp,density=2.25。
要使用這個方案的話,假設我們的設計圖寬度是360,我們修改了dp的定義,dp的新定義是講屏幕平分爲360份之後的單位長度(也就是3px),按照這個dp的數值計算出density應該修改爲3。
我們再使用新的dp去繪製控件的寬度,最終在繪製的時候再用新的density去計算成px:
屏幕寬度 x density=360 x 3 = 1080
在像素這個單位上,寬度沒有問題,還是真實寬度。
簡單說,方案通過在 Acivity#onCreate 中修改 density 的值,強行把所有不同尺寸分辨率的手機的屏幕寬度dp值改成一個統一的值。
可行性
主要回答3個問題:
- 爲什麼density可以修改?
- 爲什麼不會影響其他APP
- 如何做到可以指定Activity不適配、單獨適配的?
1. 爲什麼density可以修改?
先說結論:density不同於dpi,dpi是有物理含義的,表示每英寸的像素數,屏幕的物理寬度確定,像素數量確定,dpi就是確定的;density不是一個固定在系統中的值,而是一個“用於顯示的邏輯密度”(The logical density of the display)在程序啓動之後通過真實物理中的dpi和規則(160爲標準)計算出來的,這個規則是以160爲標準,是一個類似約定俗成的參考標準。density是public成員,因此可以很方便的修改。
閱讀源碼,查看density的值是怎麼來的:
density 是 DisplayMetrics 中的成員變量,DisplayMetrics 實例通過 Resources#getDisplayMetrics 可以獲得:
public DisplayMetrics getDisplayMetrics() {
return mResourcesImpl.getDisplayMetrics();
}
mResourcesImpl是ResourcesImpl實例,查看ResourcesImpl#getDisplayMetrics :
DisplayMetrics getDisplayMetrics() {
if (DEBUG_CONFIG) Slog.v(TAG, "Returning DisplayMetrics: " + mMetrics.widthPixels
+ "x" + mMetrics.heightPixels + " " + mMetrics.density);
return mMetrics;
}
返回的mMetrics是ResourcesImpl的對象,來看它是怎麼創建和賦值的。
private final DisplayMetrics mMetrics = new DisplayMetrics();
……
public ResourcesImpl(@NonNull AssetManager assets, @Nullable DisplayMetrics metrics,
@Nullable Configuration config, @NonNull DisplayAdjustments displayAdjustments) {
mAssets = assets;
mMetrics.setToDefaults();
mDisplayAdjustments = displayAdjustments;
mConfiguration.setToDefaults();
updateConfiguration(config, metrics, displayAdjustments.getCompatibilityInfo());
}
查看DisplayMetrics中density的描述和DisplayMetrics#setToDefaults方法,DisplayMetrics的density是一個“邏輯”密度,無關像素的比例因子。它是通過設備dpi除以160(DENSITY_DEFAULT)計算出來的,就是設備dpi是真實的,而它是考慮比例之後規定出來的。
public class DisplayMetrics {
……略……
/**
* The logical density of the display. This is a scaling factor for the
* Density Independent Pixel unit, where one DIP is one pixel on an
* approximately 160 dpi screen (for example a 240x320, 1.5"x2" screen),
* providing the baseline of the system's display. Thus on a 160dpi screen
* this density value will be 1; on a 120 dpi screen it would be .75; etc.
*
* <p>This value does not exactly follow the real screen size (as given by
* {@link #xdpi} and {@link #ydpi}, but rather is used to scale the size of
* the overall UI in steps based on gross changes in the display dpi. For
* example, a 240x320 screen will have a density of 1 even if its width is
* 1.8", 1.3", etc. However, if the screen resolution is increased to
* 320x480 but the screen size remained 1.5"x2" then the density would be
* increased (probably to 1.5).
*
* @see #DENSITY_DEFAULT
*/
public float density;
……略……
public void setToDefaults() {
widthPixels = 0;
heightPixels = 0;
density = DENSITY_DEVICE / (float) DENSITY_DEFAULT;
densityDpi = DENSITY_DEVICE;
scaledDensity = density;
xdpi = DENSITY_DEVICE;
ydpi = DENSITY_DEVICE;
noncompatWidthPixels = widthPixels;
noncompatHeightPixels = heightPixels;
noncompatDensity = density;
noncompatDensityDpi = densityDpi;
noncompatScaledDensity = scaledDensity;
noncompatXdpi = xdpi;
noncompatYdpi = ydpi;
}
……略……
上述代碼中的DENSITY_DEFAULT的數值就是160。
以上就是density的計算過程,所以density是一個計算出來的值,用途是在使用dp爲單位,計算轉換成px時使用的比例因子,影響dp到px的轉換。對其進行修改,需要保證修改後dp轉換成px不會出錯,也就是響應的使用的dp的值要配套。
2. 爲什麼不會影響其他APP
閱讀源碼,可以發現:
density 是 DisplayMetrics 中的成員變量,DisplayMetrics 是 Resource 的成員變量,Resource 是 Activity 的父類 ContextWrapper 的成員變量,因此 density 對於各個 Activity 來說是自己持有的,Activity 修改 density 隻影響當前 Activity 。
修改的地方選在Activity#onCreate方法裏。
View繪製的時候會獲取DisplayMetrics,用到的也是當前Activity的resource。
3. 如何做到可以指定Activity不適配、單獨適配的?
同2的原因一樣,因爲density是以Activity爲單位的,因此只要在該Activity的onCreate方法中選擇是否修改density,如何修改density就可以對Activity進行是否適配,單獨適配了。
所以框架才能配置哪些頁面進行適配,哪些不進行適配,哪些個性化適配。
AndroidAutoSize源碼分析
代碼結構
├── external
│ ├── ExternalAdaptInfo.java
│ ├── ExternalAdaptManager.java
│── internal
│ ├── CancelAdapt.java
│ ├── CustomAdapt.java
│── unit
│ ├── Subunits.java
│ ├── UnitsManager.java
│── utils
│ ├── AutoSizeUtils.java
│ ├── LogUtils.java
│ ├── Preconditions.java
│ ├── ScreenUtils.java
├── ActivityLifecycleCallbacksImpl.java
├── AutoAdaptStrategy.java
├── AutoSize.java
├── AutoSizeConfig.java
├── DefaultAutoAdaptStrategy.java
├── DisplayMetricsInfo.java
├── FragmentLifecycleCallbacksImpl.java
├── InitProvider.java
自動運行——啓動和初始化
使用者只需要在 AndroidManifest.xml 中填寫一下 meta-data 標籤,其他什麼都不做,AndroidAutoSize 就能自動運行,並在 App 啓動時自動解析。
原理:ContentProvider的onCreate的調用時機介於Application的attachBaseContext和onCreate之間,Provider的onCreate優先於Application的onCreate執行,並且此時的Application已經創建成功,這樣就不需要調用方在Application裏去進行初始化,框架可以自動運行了。
InitProvider#onCreate中,初始化框架:
public boolean onCreate() {
AutoSizeConfig.getInstance()
.setLog(true)
.init((Application) getContext().getApplicationContext())
.setUseDeviceSize(false);
return true;
}
AutoSizeConfig
參數配置類, 給 AndroidAutoSize 配置一些必要的自定義參數
DefaultAutoAdaptStrategy中做了什麼?
如何適配
自定義一個DefaultAutoAdaptStrategy類型,實現AutoAdaptStrategy接口的applyAdapt方法——開始執行屏幕適配邏輯,applyAdapt方法中判斷是否對第三方適配,是否是不適配,是否單獨適配等 。
最後調用的是適配:
//如果 target 實現 CustomAdapt 接口表示該 target 想自定義一些用於適配的參數, 從而改變最終的適配效果
if (target instanceof CustomAdapt) {
LogUtils.d(String.format(Locale.ENGLISH, "%s implemented by %s!", target.getClass().getName(), CustomAdapt.class.getName()));
AutoSize.autoConvertDensityOfCustomAdapt(activity, (CustomAdapt) target);
} else {
LogUtils.d(String.format(Locale.ENGLISH, "%s used the global configuration.", target.getClass().getName()));
AutoSize.autoConvertDensityOfGlobal(activity);
}
ActivityLifecycleCallbacksImpl 實現 Application.ActivityLifecycleCallbacks接口,可用來代替在 BaseActivity 中加入適配代碼的傳統方式,這種方案類似於 AOP, 面向接口, 侵入性低, 方便統一管理, 擴展性強, 並且也支持適配三方庫的Activity。
自定義ActivityLifecycleCallbacksImpl實現Application.ActivityLifecycleCallbacks接口,用於registerActivityLifecycleCallbacks中,在ActivityLifecycleCallbacksImpl寫onActivityCreated中調用applyAdapt,進行適配。
具體適配方法位於AutoSize類。
還提供了一些接口:onAdaptListener(onAdaptBefore,onAdaptAfter)
讓某個頁面取消適配
AutoSize#cancelAdapt中,做到如何取消適配——調用setDensity方法將density設置成原來的density
/**
* 取消適配
*
* @param activity {@link Activity}
*/
public static void cancelAdapt(Activity activity) {
float initXdpi = AutoSizeConfig.getInstance().getInitXdpi();
switch (AutoSizeConfig.getInstance().getUnitsManager().getSupportSubunits()) {
case PT:
initXdpi = initXdpi / 72f;
break;
case MM:
initXdpi = initXdpi / 25.4f;
break;
default:
}
setDensity(activity, AutoSizeConfig.getInstance().getInitDensity()
, AutoSizeConfig.getInstance().getInitDensityDpi()
, AutoSizeConfig.getInstance().getInitScaledDensity()
, initXdpi);
}
DefaultAutoAdaptStrategy#applyAdapt
//如果 target 實現 CancelAdapt 接口表示放棄適配, 所有的適配效果都將失效
if (target instanceof CancelAdapt) {
LogUtils.w(String.format(Locale.ENGLISH, "%s canceled the adaptation!", target.getClass().getName()));
AutoSize.cancelAdapt(activity);
return;
}
問題,density是以Activity爲單位的嗎?
第三方頁面取消適配
原理同上