Pacer首頁適配方案

需求-爲什麼要適配

app首頁是一個不可以滑動的頁面,因此需要高和寬同時適配;內容比較多——日期,抽獎按鈕,步數錶盤,步數柱狀圖,底部的Tab,廣告等;首頁中心是一個圓形錶盤,高度取剩餘高度,寬度取屏幕寬度,並且考慮高寬相等,兩方面適應之後決定圓形錶盤的直徑,導致在不同手機上效果千奇百怪。

適配方案的選擇

使用dp作爲單位,設置空間寬度時使用wrap_content, match_parent, weight等參數,可以滿足大部分業務需求。但是主頁需要精確的適配,不適合。

SamllestWidth適配方案,維護麻煩,而且首頁不是比例的問題,不是隻有寬度適配,需要高度寬度同時適配,只在一個維度上的適配不能滿足。

最終使用的AutoSize滿足一般頁面的適配和首頁的寬度適配,在高度上自行設計同時滿足高度寬度適配。

具體適配方案

1.使用AndroidAutoSize屏幕適配框架

使用dp爲單位,讓屏幕寬度不等於設計圖的都能很好的適配,以寬爲基準進行適配。

可以解決大部分頁面效果不統一的情況。

2.首頁需要寬度高度同時適配

設定寬度基準之後,假設寬度都是360的基礎上,計算首頁中心fragment的高度:

fragment高度=屏幕高度 - 系統導航欄高度 - 系統狀態欄高度 - app廣告高度 - app導航tab高度 - 頭部按鈕高度

根據不同的fragment高度,劃分到5檔,5個區間高度分別爲:

  1. 大於520dp
  2. 520 到 456
  3. 455 到 379
  4. 378 到 342
  5. 小於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個問題:

  1. 爲什麼density可以修改?
  2. 爲什麼不會影響其他APP
  3. 如何做到可以指定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爲單位的嗎?

第三方頁面取消適配

原理同上

參考文章

使用ContentProvider初始化你的Library

騷年你的屏幕適配方式該升級了!-今日頭條適配方案

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章