屏幕适配

作者: CoderYuZ | 来源:发表于2019-04-29 17:27 被阅读0次
    布局适配:
    • 避免写死控件尺寸,使用wrap_content、match_parent
    • LinearLayout使用layout_weight
    • RelativeLayout使用centerInParent等
    • 使用ContraintLayout,类似RelativeLayout,比RelativeLayout性能好
    • 使用Percent-support-lib,layout_widthPercent="30%"等
    图片资源适配:
    • .9图或则SVG图实现缩放
    • 备用位图匹配不同分辨率
    限定符适配:
    • 分辨率限定符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等,就能实现适配,注意的是单位要用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就可以,这里不说使用,说下实现。
    首先肯定要自定义属性,让控件可以设置百分比,在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的布局属性,都是在父容器中创建的(源码分析就不贴出了,主要的方法就是调用了父容器的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里创建自定义属性:
    <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <declare-styleable name="PercentLayout">
            <attr name="widthPercent" format="float" />
            ...
        </declare-styleable>
    </resources>
    
    1. 创建自定义Layout,比如:
    public class PercentLayout extends RelativeLayout 
    
    1. 在自定义Layout中创建静态内部类LayoutParams继承自该Layout. LayoutParams并实现构造方法,在其构造方法中用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的值,直接更改系统内部对于目标尺寸的像素密度。
    density:屏幕密度,系统针对某一尺寸的分辨率缩放比例(某一尺寸是指每寸有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,不使用系统的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;
    
                //添加字体变化监听回调
                application.registerComponentCallbacks(new ComponentCallbacks() {
                    @Override
                    public void onConfigurationChanged(Configuration newConfig) {
                        //字体发生更改,重新对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) {
    
                }
            });
    
        }
    }
    

    完事!!!

    相关文章

      网友评论

        本文标题:屏幕适配

        本文链接:https://www.haomeiwen.com/subject/ctrynqtx.html