屏幕适配

作者: 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) {

            }
        });

    }
}

完事!!!

相关文章

  • 屏幕适配总结

    屏幕适配总结 为什么要针对屏幕做适配 drawable目录常见问题 : 屏幕适配方案:

  • Android屏幕适配-应用篇

    目录 Android屏幕适配-基础篇Android屏幕适配-应用篇 Android屏幕适配最主要的原因:  是由于...

  • 屏幕适配的那些坑

    屏幕适配的那些坑 屏幕适配的那些坑

  • LayaAir屏幕适配

    LayaAir屏幕适配 官方教程链接:LayaAir实战开发11-屏幕适配 屏幕适配 随着移动端设备(手机、平板、...

  • she

    1.适配的分类 系统适配 屏幕适配 1.1屏幕适配历史 1.1.1autoresizing 去掉auto layo...

  • Android屏幕适配

    一. 为什么要适配屏幕 android屏幕大小、屏幕密度碎片化严重 二. 怎么样适配屏幕 图片适配 应用图标提供不...

  • 屏幕适配AutoResizing

    适配器简介 AutoResizing 屏幕适配的历史 -iPhonestyGS\IPhone4 -没有屏幕适配可言...

  • css media 适配屏幕宽度

    H5 屏幕适配 css media 适配屏幕宽度;js 通过 适配获取屏幕宽度,来执行对应方法; max-widt...

  • 关于iOS适配的一点事

    屏幕适配及文字适配

  • Android屏幕适配(4)常见:第三种

    前言:Android屏幕适配(1)基础知识篇Android屏幕适配(2)常见:第一种Android屏幕适配(3)常...

网友评论

    本文标题:屏幕适配

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