Android屏幕適配(網易雲音樂方案)

簡單高效無侵入式Android屏幕適配

最近在學習網易的Android課程,網易的老師提供了網易雲音樂的屏幕適配解決方案,主要有兩種,17年前是採用自定義縮放佈局,17年後是採用的是工具類發方案,現在這兩種方案在網易雲音樂中是同時存在的。互不影響。在對比dimen適配、density適配、百分比佈局適配等各種適配方案之後,網易雲音樂的這兩種方案在我們的項目中都是非常簡潔高效的。

屏幕適配的相關概念
像素(px)

通常所說的像素,就是CCD/CMOS上光電感應元件的數量,一個感光元件經過感光,光電信號轉換,A/D轉換等步驟以後,在輸出的照片上就形成一個點,我們如果把影像放大數倍,會發現這些連續色調其實是由許多色彩相近的小方點所組成,這些小方點就是構成影像的最小單位“像素”(Pixel)。

分辨率

手機在橫向、縱向上的像素點數總和,一般描述成寬高 ,即橫向像素點個數乘以縱向像素點個數。

屏幕尺寸(inch)

手機對角線的物理尺寸,單位 英寸(inch),一英寸大約2.54cm,常見的尺寸有4.7寸、5寸、5.5寸、6寸。

屏幕像素密度(dpi)

每英寸長度上像素點個數。

例如每英寸內有160個像素點,則其像素密度爲160dpi。

公式: 像素密度=像素/尺寸 (dpi=px/in)

標準屏幕像素密度(mdpi)

每英寸長度上還有160個像素點,即稱爲標準屏幕像素密度(mdpi)。

像素密度等級

手機真實像素密度與標準屏幕像素密度(160dpi)的比值。官方給出的0.75、1、1.5、2、3、4,即對應120dpi、160dpi、240dpi、320dpi、480dpi、640dpi

密度無關像素(dp)

density-independent pixel,叫dp或dip,與終端上的實際物理像素點無關。可以保證在不同屏幕像素密度的設備上顯示相同的效果,是安卓特有的長度單位。

獨立比例像素(sp)

scale-independent pixel,叫sp或sip,字體大小專用單位,可根據字體大小首選項進行縮放;

推薦使用12sp、14sp、18sp、22sp作爲字體大小,不推薦使用奇數和小數,容易造成精度丟失,12sp以下字體太小。

尺寸、像素、像素密度關係

在這裏插入圖片描述

幾種屏幕適配方案的對比
dimen適配

適配交由系統根據手機分辨率自動讀取不同的配置來完成,開發者無需手動處理任何細節。但壞處也很明顯,由於對於各種分辨率,爲了保證能最大精度的適配,我們要寫一大堆的dimen文件,當然,直接用工具生成即可,主要問題是增大了安裝包的大小。

density適配

通過動態修改手機的density來實現,但這種方式的缺陷是,有些廠商的手機不允許修改density的操作,再者,修改density一般是對寬度進行適配,而高度的適配則需要單獨處理,否則有可能出現垂直方向顯示不全的問題,這跟設備的屏幕比例有關,雖然可以通過加個ScrollView解決,但總體來說還是比較繁瑣,效果也只是差強人意。

百分比佈局適配

谷歌官方有提供了這套解決方案,在GitHub上可以找到,並且還有開發者對其進行了進一步封裝完善。由於UI小姐姐給我們的效果圖一般都是直接用像素(px)做單位來設計和標註的,所以如果要使用百分比佈局,那對於大部分的元素我們都需要手動計算其橫縱佔比,這無疑增加了UI和開發之間溝通和實現成本。另外,我們所有的佈局文件中用到的RelativeLayout、FrameLayout、LinearLayout等這些都必須替換成對應的百分比佈局容器,對於自定義View也不太友好。

網易雲音樂的兩種適配方式
1.自定義縮放佈局

這種方式是通過繼承佈局控件,重寫onMeasure()方法,在此方法中通過當前屏幕分辨率與設計分辨率的橫縱縮放比,對子控件的width、height、padding、margin等屬性進行相應的縮放。以RelativeLayout爲例:

public class UIRelativeLayout  extends RelativeLayout {

    private boolean flag=true;
    public static final float STANDARD_WIDTH=1080f;
    public static final float STANDARD_HEIGHT=1920f;
    
    public UIRelativeLayout(Context context) {
        super(context);
    }

    public UIRelativeLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public UIRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (flag) {
        flag = false;
        float scaleX = getHorizontalScaleValue();
        float scaleY = getVerticalScaleValue();
        int childCount = this.getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = this.getChildAt(i);
            LayoutParams layoutParams = (LayoutParams) child.getLayoutParams();
            layoutParams.width=(int) (layoutParams.width * scaleX);
            layoutParams.height = (int) (layoutParams.height * scaleY);
            layoutParams.leftMargin = (int) (layoutParams.leftMargin * scaleX);
            layoutParams.rightMargin = (int) (layoutParams.rightMargin * scaleX);
            layoutParams.topMargin = (int) (layoutParams.topMargin * scaleY);
            layoutParams.bottomMargin = (int) (layoutParams.bottomMargin * scaleY);
         }
        }
    }
    
    public float getHorizontalScaleValue(){
        return  ((float)(displayMetricsWidth)) / STANDARD_WIDTH;
    }
    public float getVerticalScaleValue(){
        return ((float)(displayMetricsHeight))/(STANDARD_HEIGHT-systemBarHeight);
    }
}

自定義好佈局文件之後,我們只需要在xml中是用我們自定義的這個類即可。這裏需要注意的是,子控件的所有屬性值必須使用像素作爲單位。

2.UI工具類

這種方案其實是將自定義縮放佈局的onMeasure()中的計算部分單獨封裝,不再交由佈局來處理,而是封裝成單獨的工具類,對外提供各種佈局的適配接口。

工具類UIUtils.java

public class UIUtils {

    private static UIUtils instance;

    //標準寬高   以UI圖爲準
    public static final float STANDARD_WIDTH = 1080f;
    public static final float STANDARD_HEIGHT = 1920f;

    public static float displayMetricsWidth;
    public static float displayMetricsHeight;
    public static float systemBarHeight;

    public static UIUtils getInstance(Context context){
        if (instance == null){
            instance = new UIUtils(context);
        }
        return instance;
    }

    public static UIUtils getInstance(){
        if (instance == null){
            throw new RuntimeException("UiUtil應該先調用含有構造方法進行初始化");
        }
        return instance;
    }

    private UIUtils(Context context){

        //計算縮放係數
        WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        DisplayMetrics displayMetrics = new DisplayMetrics();
        if (displayMetricsWidth == 0.0f || displayMetricsHeight == 0.0f){

            //獲取設備的真實寬高
            windowManager.getDefaultDisplay().getMetrics(displayMetrics);
            systemBarHeight = getSystemBarHeight(context);

            //橫屏
            if (displayMetrics.widthPixels > displayMetrics.heightPixels){
                displayMetricsWidth = (float)(displayMetrics.heightPixels);
                displayMetricsHeight = (float)(displayMetrics.widthPixels-systemBarHeight);
            }else {
                //豎屏
                displayMetricsWidth = (float)(displayMetrics.widthPixels);
                displayMetricsHeight = (float)(displayMetrics.heightPixels-systemBarHeight);
            }
        }
    }

    /**
     * 計算狀態欄高度
     * @param context context
     * @return 高度
     */
    private int getSystemBarHeight(Context context){
        return getValue(context,"com.android.internal.R$dimen","system_bar_height",48);
    }

    private int getValue(Context context, String dimeClass, String system_bar_height, int defaultValue) {
//        com.android.internal.R$dimen    system_bar_height   狀態欄的高度
        try {
            Class<?> clz=Class.forName(dimeClass);
            Object object = clz.newInstance();
            Field field=clz.getField(system_bar_height);
            int id=Integer.parseInt(field.get(object).toString());
            return context.getResources().getDimensionPixelSize(id);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return defaultValue;
    }

    /**
     * 獲取適配後的寬度
     * 例如傳入100寬度,單位爲像素,計算其在當前設備上應顯示的像素寬度
     */
    public int getWidth(int width) {
        return Math.round((float)width * displayMetricsWidth / STANDARD_WIDTH);
    }

    /**
     * 獲取適配後的高度
     */
    public int getHeight(int height) {
        return Math.round((float)height * displayMetricsHeight / (STANDARD_HEIGHT-systemBarHeight));
    }

}

在這裏我們需要把狀態欄高度考慮進去,這一點對於劉海屏適配是很重要,如果需要,還可以將底部的虛擬按鍵高度也考慮進去。

上述工具類只是完成了縮放係數相關的計算工作,我們還需要一個工具來來爲各種佈局提供適配接口。

各種佈局接口類ViewCalculateUtil.java

public class ViewCalculateUtil {

    /**
     * RelativeLayout
     * 根據屏幕的大小設置view的高度,間距
     */
    public static void setViewLayoutParam(View view, int width, int height, int topMargin, int bottomMargin, int lefMargin, int rightMargin)
    {
        RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) view.getLayoutParams();

        if (layoutParams != null)
        {
            if (width != RelativeLayout.LayoutParams.MATCH_PARENT && width != RelativeLayout.LayoutParams.WRAP_CONTENT && width != RelativeLayout.LayoutParams.FILL_PARENT)
            {
                layoutParams.width = UIUtils.getInstance().getWidth(width);
            }
            else
            {
                layoutParams.width = width;
            }
            if (height != RelativeLayout.LayoutParams.MATCH_PARENT && height != RelativeLayout.LayoutParams.WRAP_CONTENT && height != RelativeLayout.LayoutParams.FILL_PARENT)
            {
                layoutParams.height = UIUtils.getInstance( ).getHeight(height);
            }
            else
            {
                layoutParams.height = height;
            }

            layoutParams.topMargin = UIUtils.getInstance( ).getHeight(topMargin);
            layoutParams.bottomMargin = UIUtils.getInstance( ).getHeight(bottomMargin);
            layoutParams.leftMargin = UIUtils.getInstance( ).getWidth(lefMargin);
            layoutParams.rightMargin = UIUtils.getInstance( ).getWidth(rightMargin);
            view.setLayoutParams(layoutParams);
        }

    }

    /**
     * 設置LinearLayout中 view的高度寬度
     *
     * @param view
     * @param width
     * @param height
     */
    public static void setViewLinearLayoutParam(View view, int width, int height, int topMargin, int bottomMargin, int lefMargin,
                                                int rightMargin)
    {

        LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) view.getLayoutParams();
        if (width != RelativeLayout.LayoutParams.MATCH_PARENT && width != RelativeLayout.LayoutParams.WRAP_CONTENT && width != RelativeLayout.LayoutParams.FILL_PARENT)
        {
            layoutParams.width = UIUtils.getInstance( ).getWidth(width);
        }
        else
        {
            layoutParams.width = width;
        }
        if (height != RelativeLayout.LayoutParams.MATCH_PARENT && height != RelativeLayout.LayoutParams.WRAP_CONTENT && height != RelativeLayout.LayoutParams.FILL_PARENT)
        {
            layoutParams.height = UIUtils.getInstance( ).getHeight(height);
        }
        else
        {
            layoutParams.height = height;
        }

        layoutParams.topMargin = UIUtils.getInstance( ).getHeight(topMargin);
        layoutParams.bottomMargin = UIUtils.getInstance( ).getHeight(bottomMargin);
        layoutParams.leftMargin = UIUtils.getInstance( ).getWidth(lefMargin);
        layoutParams.rightMargin = UIUtils.getInstance( ).getWidth(rightMargin);
        view.setLayoutParams(layoutParams);
    }

    public static void setTextSize(TextView view, int size)
    {
        view.setTextSize(TypedValue.COMPLEX_UNIT_PX, 			 UIUtils.getInstance().getHeight(size));
    }

}

在這裏我只封裝了RelativeLayoutLinearLayout,其他的佈局可以自行添加。

工具類的使用

        UIUtils.getInstance(this.getApplicationContext());
        setContentView(R.layout.activity_main);
        tvText3 = findViewById(R.id.tvText3);
        tvText4 = findViewById(R.id.tvText4);
        ViewCalculateUtil.setViewLinearLayoutParam(tvText3, 540, 100, 0, 0, 0, 0);
        ViewCalculateUtil.setViewLinearLayoutParam(tvText4, 1080, 100, 0, 0, 0, 0);
        ViewCalculateUtil.setTextSize(tvText3,30);
總結

適配的方法和原理,無非就是縮放。這兩種方式的核心也是縮放。 採用這兩種方式, 大到電視機,小到智能手錶,我們都可以方便地將手機佈局與UI設計圖保持一致。無論是在手錶還是手機還是電視上,我們看到的app頁面都是一樣的效果。

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