屏幕適配

布局適配:
  • 避免寫死控件尺寸草巡,使用wrap_content、match_parent
  • LinearLayout使用layout_weight
  • RelativeLayout使用centerInParent等
  • 使用ContraintLayout眠寿,類似RelativeLayout躬翁,比RelativeLayout性能好
  • 使用Percent-support-lib,layout_widthPercent="30%"等
圖片資源適配:
  • .9圖或則SVG圖實現(xiàn)縮放
  • 備用位圖匹配不同分辨率
限定符適配:
  • 分辨率限定符drawable-hdpi盯拱、drawable-xdpi盒发、...
  • 尺寸限定符layout-small、layout-large(不如在phone和pad上顯示不同的布局)
  • 最小寬度限定符values-sw360dp狡逢、values-sw384dp宁舰、...
  • 屏幕方向限定符layout-land、layout-port

如果對適配要求比較高奢浑,限定符適配就不能滿足需求明吩,舉個例子,假設我們有這樣的需求:顯示寬度為屏幕一半的一張圖片殷费。

先說下Android布局中單位的基本概念:
px:像素,平常所說的1920×1080就是像素數量低葫,也就是1920px×1080px详羡,代表手機高度上有1920個像素點,寬度上有1080個像素點
dpi:每英寸多少像素嘿悬,也就是說同分辨率的手機也會存在dpi不同的情況
dp:官方敘述為當屏幕每英寸有160個像素時(也就是160dpi)实柠,dp與px等價的。那如果每英寸240個像素呢善涨?1dp—>1240/160=1.5px窒盐,即1dp與1.5px等價了草则。
綜上:dpi = 像素/尺寸, px=dpi/160
dp

然后說上面的問題蟹漓,直接用px肯定不行炕横,換成dp能處理大多數情況,但是有些情況還是顯示不正確葡粒。比如寬度都為1080px的屏幕份殿,但是因為尺寸不同dpi分別是160和240嗽交,當把圖片寬度設置為540dp時卿嘲,那么在dpi為160的屏幕上顯示是540px,也就是屏幕的一半夫壁,但是在dpi為240的屏幕上拾枣,根據上述算法,顯示為540*(240/160)px盒让,所以在屏幕寬度為1080px的屏幕上顯示并不是屏幕的一半(dpi越大梅肤,顯示圖片越寬)。這樣滿足不了我們需求糯彬。

所以適配還是需要手擼凭语,常見的有:自定義像素適配、百分比布局適配撩扒、修改像素密度適配似扔。

1. 自定義像素適配

以一個特定寬度尺寸的設備為參考,在View的加載過程中根據當前設備的實際像素換算出目標像素搓谆,再作用在控件上炒辉。
首先獲取寫一個工具類獲取設計稿和當前手機屏幕的縮放比例,這里采用單例的Utils:

public class Utils {

    private static Utils utils;

    //這里是設計稿參考寬高
    private static final float STANDARD_WIDTH = 1080;
    private static final float STANDARD_HEIGHT = 1920;

    //這里是屏幕顯示寬高
    private int mDisplayWidth;
    private int mDisplayHeight;

    private Utils(Context context){
        //獲取屏幕的寬高
        if(mDisplayWidth == 0 || mDisplayHeight == 0){
            WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
            if (manager != null){
                DisplayMetrics displayMetrics = new DisplayMetrics();
                manager.getDefaultDisplay().getMetrics(displayMetrics);
                if (displayMetrics.widthPixels > displayMetrics.heightPixels){
                    //橫屏
                    mDisplayWidth = displayMetrics.heightPixels;
                    mDisplayHeight = displayMetrics.widthPixels;
                }else{
                    mDisplayWidth = displayMetrics.widthPixels;
                    mDisplayHeight = displayMetrics.heightPixels - getStatusBarHeight(context);
                }
            }
        }

    }

    public int getStatusBarHeight(Context context){
        int resID = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
        if (resID > 0){
            return context.getResources().getDimensionPixelSize(resID);
        }
        return 0;
    }

    public static Utils getInstance(Context context){
        if (utils == null){
            utils = new Utils(context.getApplicationContext());
        }
        return utils;
    }

    //獲取水平方向的縮放比例
    public float getHorizontalScale(){
        return mDisplayWidth / STANDARD_WIDTH;
    }

    //獲取垂直方向的縮放比例
    public float getVerticalScale(){
        return mDisplayHeight / STANDARD_HEIGHT;
    }

}

自定義一個RelativeLayout:

public class ScreenAdapterLayout extends RelativeLayout {

    // 防止重復調用
    private boolean flag;

    public ScreenAdapterLayout(Context context) {
        super(context);
    }

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

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (!flag){
            //獲取橫泉手、縱向縮放比
            float scaleX = Utils.getInstance(getContext()).getHorizontalScale();//
            float scaleY = Utils.getInstance(getContext()).getVerticalScale();

            int count = getChildCount();
            for (int i = 0; i < count; i++) {
                View child = getChildAt(i);
                //重新設置子View的布局屬性
                LayoutParams params = (LayoutParams) child.getLayoutParams();
                params.width = (int) (params.width * scaleX);
                params.height = (int) (params.height * scaleY);
                params.leftMargin = (int)(params.leftMargin * scaleX);
                params.rightMargin = (int)(params.rightMargin * scaleX);
                params.topMargin = (int)(params.topMargin * scaleY);
                params.bottomMargin = (int)(params.bottomMargin * scaleY);
            }
            flag = true;
        }
        // 計算完成后再進行測量
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
}

之后我們的布局文件都要用這個自定義的RelativeLayout包裹黔寇,當前我們還需要自定義LinearLayout等,就能實現(xiàn)適配斩萌,注意的是單位要用px缝裤,就是設計稿上的px值:

<?xml version="1.0" encoding="utf-8"?>
<com.netease.screenadapter.pixel.ScreenAdapterLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:layout_width="540px"
        android:layout_height="540px"
        android:layout_marginLeft="10px"
        android:text="Hello World!"
        android:background="@color/colorAccent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</com.netease.screenadapter.pixel.ScreenAdapterLayout>

完事!

2. 百分比布局適配

用Google的Percent-support-lib就可以颊郎,這里不說使用憋飞,說下實現(xiàn)。
首先肯定要自定義屬性姆吭,讓控件可以設置百分比榛做,在attrs里添加:

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <declare-styleable name="PercentLayout">
        <attr name="widthPercent" format="float" />
        <attr name="heightPercent" format="float" />
        <attr name="marginLeftPercent" format="float" />
        <attr name="marginRightPercent" format="float" />
        <attr name="marginTopPercent" format="float" />
        <attr name="marginBottomPercent" format="float" />
    </declare-styleable>

</resources>

這些屬性肯定要解析并使用,具體的解析過程可以在RelativeLayout或者LinearLayout的源碼中查看它們的特有屬性是怎么處理的。LayoutInflater的源碼中可以看出View的布局屬性检眯,都是在父容器中創(chuàng)建的(源碼分析就不貼出了厘擂,主要的方法就是調用了父容器的generateLayoutParams()方法),所以直接自定義Layout去獲取去這些屬性就可以了锰瘸。這里直接貼出處理代碼:

public class PercentLayout extends RelativeLayout {

    public PercentLayout(Context context) {
        super(context);
    }

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

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //獲取父容器的尺寸
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            ViewGroup.LayoutParams params = child.getLayoutParams();
            //如果說是百分比布局屬性
            if (checkLayoutParams(params)){
                LayoutParams lp = (LayoutParams)params;
                 float widthPercent = lp.widthPercent;
                 float heightPercent = lp.heightPercent;
                 float marginLeftPercent = lp.marginLeftPercent;
                 float marginRightPercent= lp.marginRightPercent;
                 float marginTopPercent= lp.marginTopPercent;
                 float marginBottomPercent = lp.marginBottomPercent;

                 if (widthPercent > 0){
                     params.width = (int) (widthSize * widthPercent);
                 }

                if (heightPercent > 0){
                    params.height = (int) (heightSize * heightPercent);
                }

                if (marginLeftPercent > 0){
                    ((LayoutParams) params).leftMargin = (int) (widthSize * marginLeftPercent);
                }

                if (marginRightPercent > 0){
                    ((LayoutParams) params).rightMargin = (int) (widthSize * marginRightPercent);
                }

                if (marginTopPercent > 0){
                    ((LayoutParams) params).topMargin = (int) (heightSize * marginTopPercent);
                }

                if (marginBottomPercent > 0){
                    ((LayoutParams) params).bottomMargin = (int) (heightSize * marginBottomPercent);
                }

            }
        }

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return p instanceof LayoutParams;
    }

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs){
        return new LayoutParams(getContext(), attrs);
    }

    public static class LayoutParams extends RelativeLayout.LayoutParams{

        private float widthPercent;
        private float heightPercent;
        private float marginLeftPercent;
        private float marginRightPercent;
        private float marginTopPercent;
        private float marginBottomPercent;

        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
            //解析自定義屬性
            TypedArray a = c.obtainStyledAttributes(attrs,R.styleable.PercentLayout);
            widthPercent = a.getFloat(R.styleable.PercentLayout_widthPercent, 0);
            heightPercent = a.getFloat(R.styleable.PercentLayout_heightPercent, 0);
            marginLeftPercent = a.getFloat(R.styleable.PercentLayout_marginLeftPercent, 0);
            marginRightPercent = a.getFloat(R.styleable.PercentLayout_marginRightPercent, 0);
            marginTopPercent = a.getFloat(R.styleable.PercentLayout_marginTopPercent, 0);
            marginBottomPercent = a.getFloat(R.styleable.PercentLayout_marginBottomPercent, 0);
            a.recycle();
        }
    }
}

然后我們布局的時候刽严,用自定的Layout包裹就行:

<?xml version="1.0" encoding="utf-8"?>
<com.netease.screenadapter.percentlayout.PercentLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="寬50%;高75%"
        android:background="#f00"
        app:widthPercent="0.5"
        app:heightPercent="0.75"
        app:marginLeftPercent="0.5"/>

</com.netease.screenadapter.percentlayout.PercentLayout>

完事获茬!
總結下自定義屬性解析:

  1. 在attrs里創(chuàng)建自定義屬性:
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="PercentLayout">
        <attr name="widthPercent" format="float" />
        ...
    </declare-styleable>
</resources>
  1. 創(chuàng)建自定義Layout港庄,比如:
public class PercentLayout extends RelativeLayout 
  1. 在自定義Layout中創(chuàng)建靜態(tài)內部類LayoutParams繼承自該Layout. LayoutParams并實現(xiàn)構造方法,在其構造方法中用obtainStyledAttributes去解析這些自定義屬性:
public static class LayoutParams extends RelativeLayout.LayoutParams

        private float widthPercent;
        ...

        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
            //解析自定義屬性
            TypedArray a = c.obtainStyledAttributes(attrs,R.styleable.PercentLayout);
            widthPercent = a.getFloat(R.styleable.PercentLayout_widthPercent, 0);
            ...
            a.recycle();
        }
}
  1. 重寫自定義Layout的generateLayoutParams()方法恕曲,使用我們自定義的LayoutParams:
    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs){
        return new LayoutParams(getContext(), attrs);
    }
  1. 重寫checkLayoutParams鹏氧,模仿ViewGroup中的代碼,可寫可不寫佩谣。用于獲取LayoutParams時的類型判斷把还,也可以直接用p instanceof LayoutParams去判斷:
    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return p instanceof LayoutParams;
    }
  1. 使用:
    if (checkLayoutParams(params)){
        LayoutParams lp = (LayoutParams)params;
        float widthPercent = lp.widthPercent;
        ...
    }

3. 修改像素密度適配

修改density、scaleDensity茸俭,densityDpi的值吊履,直接更改系統(tǒng)內部對于目標尺寸的像素密度。
density:屏幕密度调鬓,系統(tǒng)針對某一尺寸的分辨率縮放比例(某一尺寸是指每寸有160px的屏幕艇炎,上面也有提到過),假設某個屏幕每英寸有320px腾窝,那么此時density為2
scaleDensity:字體縮放比例缀踪,默認情況下和density一樣
densityDpi:每英寸像素的,比如剛才說的160或320虹脯,可以通過屏幕尺寸和分辨率算出來

為什么修改這些值能達到屏幕適配驴娃?
TypeValue源碼中有這樣一段:

    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;
    }

這段代碼說明我們不管在XML里設置什么單位(sp、dp循集、px)唇敞,最終都會轉換成px設置到屏幕上,而轉換過程的計算方式就用到了density咒彤、scaledDensity疆柔。

為什么修改density,不使用系統(tǒng)的density镶柱?
因為相同分辨率的屏幕婆硬,因為尺寸不同,density也會不同奸例,例子上面提到過。

原理完事直接貼代碼:
新建一個Density類,提供setDensity()方法:

public class Density {

    private static final float  WIDTH = 320;//參考設備的寬查吊,單位是dp 320 / 2 = 160

    private static float appDensity;//表示屏幕密度
    private static float appScaleDensity; //字體縮放比例谐区,默認appDensity

    public static void setDensity(final Application application, Activity activity){
        //獲取當前app的屏幕顯示信息
        DisplayMetrics displayMetrics = application.getResources().getDisplayMetrics();
        if (appDensity == 0){
            //初始化賦值操作
            appDensity = displayMetrics.density;
            appScaleDensity = displayMetrics.scaledDensity;

            //添加字體變化監(jiān)聽回調
            application.registerComponentCallbacks(new ComponentCallbacks() {
                @Override
                public void onConfigurationChanged(Configuration newConfig) {
                    //字體發(fā)生更改,重新對scaleDensity進行賦值
                    if (newConfig != null && newConfig.fontScale > 0){
                        appScaleDensity = application.getResources().getDisplayMetrics().scaledDensity;
                    }
                }

                @Override
                public void onLowMemory() {

                }
            });
        }

        //計算目標值density, scaleDensity, densityDpi
        float targetDensity = displayMetrics.widthPixels / WIDTH; // 1080 / 360 = 3.0
        float targetScaleDensity = targetDensity * (appScaleDensity / appDensity);
        int targetDensityDpi = (int) (targetDensity * 160);

        //替換Activity的density, scaleDensity, densityDpi
        DisplayMetrics dm = activity.getResources().getDisplayMetrics();
        dm.density = targetDensity;
        dm.scaledDensity = targetScaleDensity;
        dm.densityDpi = targetDensityDpi;
    }

}

然后在每個Activity里調用Density.setDensity(getApplication(),this)設置就可以了逻卖,當然可以在BaseActivity里調用宋列。但是最好的解決方式是在Application的registerActivityLifecycleCallbacks()里設置:

public class App extends Application {

    @Override
    public void onCreate() {
        super.onCreate();

        registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
            @Override
            public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
                Density.setDensity(App.this, activity);
            }

            @Override
            public void onActivityStarted(Activity activity) {

            }

            @Override
            public void onActivityResumed(Activity activity) {

            }

            @Override
            public void onActivityPaused(Activity activity) {

            }

            @Override
            public void onActivityStopped(Activity activity) {

            }

            @Override
            public void onActivitySaveInstanceState(Activity activity, Bundle outState) {

            }

            @Override
            public void onActivityDestroyed(Activity activity) {

            }
        });

    }
}

完事!F酪病炼杖!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市盗迟,隨后出現(xiàn)的幾起案子坤邪,更是在濱河造成了極大的恐慌,老刑警劉巖罚缕,帶你破解...
    沈念sama閱讀 218,204評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件艇纺,死亡現(xiàn)場離奇詭異,居然都是意外死亡邮弹,警方通過查閱死者的電腦和手機黔衡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來腌乡,“玉大人盟劫,你說我怎么就攤上這事∮肱Γ” “怎么了侣签?”我有些...
    開封第一講書人閱讀 164,548評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長渣锦。 經常有香客問我硝岗,道長,這世上最難降的妖魔是什么袋毙? 我笑而不...
    開封第一講書人閱讀 58,657評論 1 293
  • 正文 為了忘掉前任型檀,我火速辦了婚禮,結果婚禮上听盖,老公的妹妹穿的比我還像新娘胀溺。我一直安慰自己,他們只是感情好皆看,可當我...
    茶點故事閱讀 67,689評論 6 392
  • 文/花漫 我一把揭開白布仓坞。 她就那樣靜靜地躺著,像睡著了一般腰吟。 火紅的嫁衣襯著肌膚如雪无埃。 梳的紋絲不亂的頭發(fā)上徙瓶,一...
    開封第一講書人閱讀 51,554評論 1 305
  • 那天,我揣著相機與錄音嫉称,去河邊找鬼侦镇。 笑死,一個胖子當著我的面吹牛织阅,可吹牛的內容都是我干的壳繁。 我是一名探鬼主播,決...
    沈念sama閱讀 40,302評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼荔棉,長吁一口氣:“原來是場噩夢啊……” “哼闹炉!你這毒婦竟也來了?” 一聲冷哼從身側響起润樱,我...
    開封第一講書人閱讀 39,216評論 0 276
  • 序言:老撾萬榮一對情侶失蹤渣触,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后祥国,有當地人在樹林里發(fā)現(xiàn)了一具尸體昵观,經...
    沈念sama閱讀 45,661評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,851評論 3 336
  • 正文 我和宋清朗相戀三年舌稀,在試婚紗的時候發(fā)現(xiàn)自己被綠了啊犬。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,977評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡壁查,死狀恐怖觉至,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情睡腿,我是刑警寧澤语御,帶...
    沈念sama閱讀 35,697評論 5 347
  • 正文 年R本政府宣布,位于F島的核電站席怪,受9級特大地震影響应闯,放射性物質發(fā)生泄漏。R本人自食惡果不足惜挂捻,卻給世界環(huán)境...
    茶點故事閱讀 41,306評論 3 330
  • 文/蒙蒙 一碉纺、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧刻撒,春花似錦骨田、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至醋火,卻和暖如春悠汽,著一層夾襖步出監(jiān)牢的瞬間箱吕,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評論 1 270
  • 我被黑心中介騙來泰國打工柿冲, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留殖氏,地道東北人。 一個月前我還...
    沈念sama閱讀 48,138評論 3 370
  • 正文 我出身青樓姻采,卻偏偏與公主長得像,于是被迫代替她去往敵國和親爵憎。 傳聞我的和親對象是個殘疾皇子慨亲,可洞房花燭夜當晚...
    茶點故事閱讀 44,927評論 2 355

推薦閱讀更多精彩內容