Android屏幕適配方案

前言

android設備各類各樣,手機、pad、電視、車載等不一而足。即便是相同分辨率的手機也可能參數不一致,好比1080P的手機 dpi 通常認爲是480,可是 Google 的Pixel2(1920*1080)的 dpi 是420。此外,android設備的寬高比更是多種多樣。這就致使App適配的工做異常困難。尤爲是你的app要適配各類平臺,好比手機、pad、車載、電視。在這種情形下,你面臨的問題讓你無所適從,由於你根本猜不到設備的參數和尺寸,更別提如何適配。android

相關知識

android度量計算公式git

  • px = density * dp
  • density = dpi / 160
  • px = dp * (dpi / 160)
  • DisplayMetrics.density
  • DisplayMetrics.densityDpi
  • DisplayMetrics.scaledDensity

具體的含義自行搜索,density 的差別致使適配困難;scaledDensity 是字體的縮放因子,scaledDensity 正常狀況下和 density 相等,可是調節系統字體大小後會改變這個值。
查看源碼,能夠得知:DisplayMetrics 實例經過 Resources#getDisplayMetrics能夠得到,而Resouces經過 Activity 或者 Application 的 Context 得到。
dp 和 px 的轉換是經過 DisplayMetrics 中相關的值來計算的,view、bitmap 等元素在計算中的dp轉換也是如此。
佈局文件中 dp 的轉換,最終都是調用 TypedValue#applyDimension(int unit, float value, DisplayMetrics metrics) 來進行轉換。相似的,BitmapFactory#decodeResourceStream 方法也會應用 DisplayMetrics 中的參數計算。github

/**
     * Converts an unpacked complex data value holding a dimension to its final floating 
     * point value. The two parameters <var>unit</var> and <var>value</var>
     * are as in {@link #TYPE_DIMENSION}.
     *  
     * @param unit The unit to convert from.
     * @param value The value to apply the unit to.
     * @param metrics Current display metrics to use in the conversion -- 
     *                supplies display density and scaling information.
     * 
     * @return The complex floating point value multiplied by the appropriate 
     * metrics depending on its unit. 
     */
    public static float applyDimension(int unit, float value,
                                       DisplayMetrics metrics)
    {
        switch (unit) {
        case COMPLEX_UNIT_PX:
            return value;
        case COMPLEX_UNIT_DIP:
            return value * metrics.density;
        case COMPLEX_UNIT_SP:
            return value * metrics.scaledDensity;
        case COMPLEX_UNIT_PT:
            return value * metrics.xdpi * (1.0f/72);
        case COMPLEX_UNIT_IN:
            return value * metrics.xdpi;
        case COMPLEX_UNIT_MM:
            return value * metrics.xdpi * (1.0f/25.4f);
        }
        return 0;
    }
複製代碼
今日頭條方案
原理
  • density = px / dp
適配方案
  • 給定一個寬高大小固定的標準設計圖,支持以寬或高一個維度自適應適配,保持改維度和設計圖一致;
  • 支持dp和sp單位。
實現

修改application和activity的density,系統修改字體時打開App也能對應修改。scaledDensity計算根據系統原來的比值來得到如今修改後的值。api

final float targetScaledDensity = targetDensity * (appDisplayMetrics.scaledDensity / appDisplayMetrics.density);
複製代碼

在 Activity#onCreate 方法中調用下。代碼比較簡單,也沒有涉及到系統非公開api的調用,所以理論上不會影響app穩定性。bash

/**
     * 頭條處理多設備的方案  setCustomDensity(this, getApplication());
     *
     * @param activity
     * @param application
     */
    private void setCustomDensity(Activity activity, final 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;
    }
複製代碼

具體適配效果能夠見參考資料連接,今日頭條團隊在各類手機上的適配效果和後期bug反饋。app

思考一刻

這個方案的解決思路很簡潔,參考資料也詳細的列舉了它的優勢,很是吸引人。可是,最終咱們公司的項目沒有采用這個,而是採用下面的方案。理由很簡單:一次修改,全局改變。後期維護無所適從。假如一處UI出問題了,你打算怎麼改?你無法改,你怎麼改。ide

後續

參考中《騷年你的屏幕適配方式該升級了!-今日頭條適配方案》這篇文章進一步升級了這個思路,它最大的貢獻是對單個 Activity 或 Fragment 能夠取消適配。這個思路能夠解決後期維護問題,我以爲這個方案這個時候就值得推薦和使用了。同時,它還能自定義以寬或者高爲維度進行適配。佈局

AndroidScreenAdaptation 方案
原理
  • 基於設計圖的寬度值(或高度值)和對應的dpi適配,即根據設備的實際寬度(或高度)相對應的縮放view的尺寸。
  • 縮放比率 = value * ((float) actualWidth / (float) designWidth)
適配方案
  • 給定一個寬高大小固定的標準設計圖,支持以寬或高一個維度自適應適配,保持改維度和設計圖一致;
  • 支持dp和sp單位。
實現

遍歷 ViewGroup 獲取全部子 View 的尺寸參數,從新計算 View 的WidthHeightFont、Padding、LayoutMargin。字體

/**
 * Only adapter width/height/padding/margin
 * Created by zhangyuwan0 on 2018/3/21.
 */

public class SimpleConversion implements IConversion {

    @Override
    public void transform(View view, AbsLoadViewHelper loadViewHelper) {
        if (view.getLayoutParams() != null) {
            loadViewHelper.loadWidthHeightFont(view);
            loadViewHelper.loadPadding(view);
            loadViewHelper.loadLayoutMargin(view);
        }
    }

}
複製代碼

Activity、Fragment、自定義 View 等加載view後,手動調用LoadViewHelper#loadView 方法重計算一遍全部view。本質的轉化方法是計算縮放因子。ui

private float calculateValue(float value) {
        if ("px".equals(unit)) {
            return value * ((float) actualWidth / (float) designWidth);
        } else if ("dp".equals(unit)) {
            int dip = dp2pxUtils.px2dip(actualDensity, value);
            value = ((float) designDpi / 160) * dip;
            return value * ((float) actualWidth / (float) designWidth);

        }
        return 0;
    }
複製代碼
項目實際反饋
  • 簡單衡量頭條和AndroidScreenAdaptation的優缺點後,咱們最終選擇這個方案。緣由:雖然全部佈局都須要手動調用 ScreenAdapterTools # getInstance() # loadView(view) 方法,工做量大;可是,優勢也是這個。任何 View 的適配都是能夠調整和修改的,並且不會影響其它佈局。
/**
     * Created by guokun on 2018/7/21.
     * Description: 標準寬高640x360(16:9) density = 1.0 dpi = 160
     * 1. 高度低於設計高度,以高度做標準縮放;
     * 2. 高度高於設計高度,可是高度:寬度 < 9:16,以高度做標準縮放;
     * 3. 其他以寬度做標準縮放;
     * @param
     * @return
     */
    public float calculateValue(float value) {
        if ("px".equals(unit)) {
            return value * ((float) actualWidth / (float) designWidth);
        } else if ("dp".equals(unit)) {
            int dip = dp2pxUtils.px2dip(actualDensity, value);
            value = ((float) designDpi / 160) * dip;

            if (actualHeight < designHeight || actualWidth * designHeight / designWidth > actualHeight) {
                return value * ((float) actualHeight / (float) designHeight);
            }
            return value * ((float) actualWidth / (float) designWidth);
        }
        return 0;
    }
複製代碼
  • 自定義 View 基本不支持。每一個自定義 View 你須要查看源碼調用 calcualteValue 從新計算參數。幸運的是項目自定義 View 不是不少和複雜。最致命的是:wrapcontent 不適配,全部的 View 必須給定尺寸;SeekBarProgress 不支持,手動反射調用方法處理適配問題。
@Override
    public void transform(View view, AbsLoadViewHelper loadViewHelper) {
        /**Created by guokun on 2018/7/28.
         * Description: MyLinearLayout_h381特殊處理
         * 1. MyLinearLayout鍵盤的高度大於標準高度360;
         * 2. 這裏UI標準圖設計bug,未加上20dp 鍵盤top;
         * */
        if (view.getTag() != null && (Integer)view.getTag() == MyLinearLayout_h381.getCustomHeight(view.getContext())) {
            int defaultDesign = loadViewHelper.getDesignHeight();
            loadViewHelper.setDesignHeight((Integer) view.getTag());
            if (view.getLayoutParams() != null) {
                loadViewHelper.loadWidthHeightFont(view);
                loadViewHelper.loadPadding(view);
                loadViewHelper.loadLayoutMargin(view);
            }
            loadViewHelper.setDesignHeight(defaultDesign);
        }else {
            if (view.getLayoutParams() != null) {
                loadViewHelper.loadWidthHeightFont(view);
                loadViewHelper.loadPadding(view);
                loadViewHelper.loadLayoutMargin(view);
            }
        }
    }
複製代碼
思考一刻

放棄這個項目吧,不值得。光是缺點,你都改不過來。

大總結

android 適配一直是個懸而未決的大難題。Google 提供的思路對於國內複雜的設備環境和小團隊而言,代價很高。綜合項目實際場景再權衡各類方案纔是解決之道,由於這些方案自己並非很大的工程。

參考資料

推薦Android兩種屏幕適配方案
Android 目前穩定高效的UI適配方案
騷年你的屏幕適配方式該升級了!-今日頭條適配方案

相關文章
相關標籤/搜索