屏幕适配

做Android,一定会接触到屏幕适配,而屏幕适配的方案也是有多种多样,这个话题一直没有停止,最近也是想再研究一下适配的多种方式。
先放一个表格

密度类型 代表的分辨率(px) 屏幕密度(dpi) density 换算(px/dp) 比例
低密度(ldpi) 240x320 120 1dp=0.75px 0.75 3
中密度(mdpi) 320x480 160 1dp=1px 1 4
高密度(hdpi) 480x800 240 1dp=1.5px 1.5 6
超高密度(xhdpi) 720x1280 320 1dp=2px 2 8
超超高密度(xxhdpi) 1080x1920 480 1dp=3px 3 12

再放几个公式

  1. 计算屏幕对角线英寸

  2. 计算density和dpi

  3. 计算dp和px的关系


一些概念:

  1. density的意思就是1dp等于几个px像素点
    比如density=3,意思是1dp=3px

  2. dp、dip、dpi、ppi
    这四个是新手容易混淆的,其中dp和dip是一样的概念,这个是android特有的一种逻辑单位,和具体设备的物理像素无关。
    而dpi和ppi是一样的概念,这个是一平方英寸里有多少个像素点的意思。

  3. 不管你在布局文件中填写的是什么单位,最后都会被转化为 px

除了下面提到的,还有比较简单有效的适配方法:

比如常使用的:RelativeLayout布局、LinearLayout布局、weight、.9.png、svg图片、ViewStub、include、merge
不长使用的,比如app需要自动适配手机和平板时用到的:布局别名、smallestWidth限定符
详情可看这里:https://www.jianshu.com/p/ec5a1a30694b


先提出以下疑问,根据疑问来学习

  1. 如果要开发一个新的App,设计人员的底图应该多大?
  2. 切图后,把图片放到res下面的哪个文件夹?
  3. 如果使用warp_content,那么这个大小在不同dpi设备里是多大的px?

根据以上疑问,我们来一一解答

  1. 很多时候有经验的设计人员给我们的原型里,已经有了dp值,但是有些设计新人并不知道如何在原型里标注多大的dp值,而且很多时候设计都是按照ios的分辨率来切图的,我们先说按照android标准尺寸切图的情况,假设我们使用1920*1080分辨率的底图

  2. 使用1920*1080分辨率作为底图设计切图后,我们尽量把切图放在高dpi文件夹里(设计底图分辨率不要太低,如1920*1080就比较清晰),否则放在低dpi文件夹里的话,如果app安装在高dpi的手机设备里,图片会拉伸,可能会模糊,现在一般至少1920*1080分辨率,这个分辨率的谷歌标准dpi是480,也就是xxhdpi

  • 3.1. 如果我们将切图放到了res/xxhdpi下面,根据谷歌设计规范这个dpi的density是3,如果1080px*1920px分辨率的底图中有一个图片是540px*960px(在density等于3时等同于180dp*320dp),那么这个图片使用warp_content的话在1080px*1920px(在density等于3时等同于360dp*640dp)分辨率并且density等于3的设备上显示时宽度正好是屏幕宽度的一半(比较dp的话是540px/3density=180dp是360dp的一半,比较px的话是540px是1080px的一半)

  • 3.2. 如果我们将切图位置不变,仍然放到了res/xxhdpi(density=3)下面,那么该540px*960px分辨率的图片在xhdpi(density=2)的设备里使用warp_content自适应时的分辨率和dp是多少呢?

    • 先算一下px:xxhdpi下面的dp是540px/3density=180dp,180dp*2density=360px,所以这个图片使用warp_content自适应时的在xhdpi下面分辨率是360px
    • 而dp就是这个图片在当前所在dpi设备里(也可以说当前所在dpi文件夹里)的dp,这个dp就是在所有dpi设备里的dp,如当前图片在xxhdpi下面,所以dp是540px/3density=180dp,在xhdpi、hdpi等等里面也都是这个dp。
    • 总结一下:图片在当前dpi文件夹里warp_content自适应时的dp是多大,在其他所有dpi显示设备里的dp也是多大,所以先计算图片在当前dpi文件夹里warp_content自适应时的dp,然后根据这个dp和其他dpi设备的density就可以计算出这个图片显示在非当前dpi设备里时的分辨率大小。
  • 3.3. 如果我们将刚才540px*960px的切图放到res/xhdpi下面的话(xhdpi的density是2,所以等同于270dp*480dp),那么这个图片使用warp_content的话在xhdpi(density=2时xhdpi是360dp宽640dp高)的设备上显示时这个图片宽度要大于屏幕宽度的一半,不信的可以试一下,我试过了没错(比较dp的话是270dp是360dp的0.75倍,比较px的话是540px是720px的0.75倍)

根据上面这些内容可以总结得到以下结论:

  1. 如果设计人员给的底图使用某个谷歌标准分辨率,比如1920*1080,根据最开始的表格可以看到这个分辨率对应的res文件夹是xxhdpi,如果把切图放到这个文件夹里,那么图片自适应的宽高和设计图是一样的,如果其中需要指定大小,可以根据公式dp=px/density来得到。也就是说如果底图是谷歌标准分辨率,把底图或者切图放到对应的res文件夹里,视觉效果和设计图是一样的。
    但是,比如在1920*1080底图中有一个切图是1920*540,那么放到xxhdpi里,在xml使用宽度使用warp_content和使用180dp(540px/3density计算的来),都是占用标准xxhdpi模拟器的一半宽(180dp*3density=540px,540px是1080px的一半;或者直接看dp,180dp是360dp的一半)。但是如果换一个模拟器(图片仍然在xxhdpi文件夹下),换的模拟器不一定是360dp宽,那么这个切图就不一定是占据模拟器一半的宽度了。在420dpi的模拟器里,这个模拟器的density是420/160=2.625,所以这个模拟器的宽约等于411dp,这个图片占据屏幕的180/411这么宽,看起来小于一半(或者看分辨率,180dp*2.625density=472.5px,472.5px/1080px小于一半)。而如果在320dp的模拟器里显示的话(标准hdpi就是320dp宽),这个图片占据屏幕的180/320这么宽,看起来大于一半。

备注:以上计算方式必须知道设备的dpi或者density其中一个,否则无法计算。(density=dpi/160)

假如知道某个图片在某个dpi文件夹里的warp_content时的px值,想知道这个图片放在其他dpi文件夹里的warp_content时的px值,可以通过 px/当前dpi设备的density 得到这个图片在当前dpi下面的dp值,然后根据该 dp值*其他dpi设备的density 得到该图片放在其他dpi设备里的px值。

  1. 如果设计人员给的底图不是某个谷歌标准分辨率(一般都不是标准分辨率),比如是用的iphone6设计的底图(配置:1334×750分辨率,326dpi),我们有以下方法可以来适配:如穷举分辨率适配法、smallestWidth适配法等等,下面我们一一列举。

所以一个新项目我们可以让设计按照某个谷歌标准分辨率做底图,然后根据上面的规则我们就知道图中对应的px在某个dpi文件夹里是多少dp。

1. 穷举分辨率适配法

简单说,就是穷举市面上所有的Android手机的宽高像素值,然后创建一批不同分辨率下的dimen文件,其中值的单位是px:


image.png

设定一个基准的分辨率,其他分辨率都根据这个基准分辨率来计算,在不同的尺寸文件夹内部,根据该尺寸编写对应的dimens文件。

比如以480x320为基准分辨率

  • 宽度为320,将任何分辨率的宽度整分为320份,取值为x1-x320

  • 高度为480,将任何分辨率的高度整分为480份,取值为y1-y480

那么对于480*800的分辨率的dimens文件来说,

x1=(480/320)*1=1.5px

x2=(480/320)*2=3px

...


image.png

这个时候,我们用UI设计界面作为基准分辨率,比如UI设计界面是640px*960px,然后我们创建values-640x960,然后创建一堆dimen值,分别是x1-x640,值从1px-640px,如果我们要使用1000px怎么办呢?我们可以将dimen的范围写大一些也可以的,只要比例一样就行。

然后我们可以根据这个基准分辨率创建其他分辨率的文件,比如创建
values-480x800,x1就是480/640=0.75px,其他值根据此比例来生成。

当APP运行在不同分辨率的手机中时,这些系统会根据这些dimens引用去该分辨率的文件夹下面寻找对应的值。这样基本解决了我们的适配问题,而且极大的提升了我们UI开发的效率。

但是这个方案有一个致命的缺陷,那就是需要精准命中才能适配,比如1920*1080的手机就一定要找到1920*1080的限定符,否则就只能用统一的默认的dimens文件了。而使用默认的尺寸的话,UI就很可能变形,简单说,就是容错机制很差。

不过这个方案有一些团队用过,我们可以认为它是一个比较成熟有效的方案了。

2. smallestWidth适配法或者叫sw限定符适配法

这个和上面的区别是穷举市面上所有手机的dp值,dimen的单位是dp,该方法解决了上面方法1的缺点,即使某个dp没有覆盖,系统也会寻找小于或等于该dp的文件,然后用该文件适配。这种机制和上文提到的宽高限定符适配原理上是一样的,都是系统通过特定的规则来选择对应的文件。

这种适配方式的dimen文件的生成的规则和上面一样,也是先设置一个基准dp,因为系统会根据当前设备的最小dp去选择文件夹,所以我们把设计图的px当成dp作为基准dp就可以了,举个例子,我们的UI设计图是640px*960px,我们把它当成640dp*960dp,然后我们创建基准dp文件夹values-sw640dp,我们可以从1创建到分辨率的最大值,比如x1=1dp,x640=640dp,x960=960dp,然后我们可以其他swdp文件夹,比如创建values-sw480dp文件夹,480/640=0.75,所以x1=0.75dp,以此类推x640*0.75=480dp,x960*0.75=720dp,看到了吧,我们直接用UI设计图的分辨率作为基准分辨率即可,然后使用的时候设计图中的10px我们用x10就可以了,很方便。同样的我们也可以创建layout-swdp,在不同dpi的设备里系统会自动选择对应的layout

思考一下:如果一个相同name的dimen在values-分辨率文件夹和values-swdp文件夹里都定义了的时候系统会使用哪个值?我测试了一下,系统会优先使用values-swdp文件夹里的值。

这个方案的缺点:

  • 在布局中引用 dimens 的方式,虽然学习成本低,但是在日常维护修改时较麻烦

  • 侵入性高,如果项目想切换为其他屏幕适配方案,因为每个 Layout 文件中都存在有大量 dimens 的引用,这时修改起来工作量非常巨大,切换成本非常高昂

  • 无法覆盖全部机型,想覆盖更多机型的做法就是生成更多的资源文件,但这样会增加 App 体积,在没有覆盖的机型上还会出现一定的误差,所以有时需要在适配效果和占用空间上做一些抉择

  • 如果想使用 sp,也需要生成一系列的 dimens,导致再次增加 App 的体积

  • 不能自动支持横竖屏切换时的适配,如上文所说,如果想自动支持横竖屏切换时的适配,需要使用 values-wdp屏幕方向限定符 再生成一套资源文件,这样又会再次增加 App 的体积

3. 美团的修改density适配法

我们强制将density修改为谷歌标准值,也就相当于我们强制把设计人员给的图片转为谷歌某个标准分辨率,这样我们上面的计算方法就有效了,通过测试某个切图发现不同dp宽度的模拟器中该图片在屏幕的比例都是一致的。但是修改了系统的density值之后,整个布局的实际尺寸都会发生改变,如果想要在老项目文件中使用,恐怕整个布局文件中的尺寸都可能要重新按照设计稿修改一遍才行。因此,如果你是在维护或者改造老项目,使用这套方案就要三思了。

我先发一下美团方式的计算公式

public class Test {
    private static float sNonCompatDensity;
    private static float sNonCompatScaledDensity;

    public static void setCustomDensity(@NonNull Activity activity, @NonNull final Application application, final int designWidthDp) {
        final DisplayMetrics appDisplayMetrics = application.getResources().getDisplayMetrics();
        if (sNonCompatDensity == 0) {
            sNonCompatDensity = appDisplayMetrics.density;
            sNonCompatScaledDensity = appDisplayMetrics.scaledDensity;
            application.registerComponentCallbacks(new ComponentCallbacks() {
                @Override
                public void onConfigurationChanged(Configuration newConfig) {
                    if (newConfig != null && newConfig.fontScale > 0) {
                        sNonCompatScaledDensity = application.getResources().getDisplayMetrics().scaledDensity;
                    }
                }

                @Override
                public void onLowMemory() {

                }
            });
        }
        final float targetDensity = appDisplayMetrics.widthPixels / ((float) (designWidthDp));
        final float targetScaledDensity = targetDensity * (sNonCompatScaledDensity / sNonCompatDensity);
        final int targetDensityDpi = (int) (160 * targetDensity);

        appDisplayMetrics.density = targetDensity;
        appDisplayMetrics.scaledDensity = targetScaledDensity;
        appDisplayMetrics.densityDpi = targetDensityDpi;

        final DisplayMetrics activityDisplayMetrics = activity.getResources().getDisplayMetrics();
        activityDisplayMetrics.density = targetDensity;
        activityDisplayMetrics.scaledDensity = targetScaledDensity;
        activityDisplayMetrics.densityDpi = targetDensityDpi;
    }
}

美团这种方式如何使用呢,比如设计人员的底图分辨率是W*H(竖屏,并且WTest.setCustomDensity(this, application,Wdp),然后显示效果是和设计图一样的

举个例子,比如设计人员的底图是500*1000,其中有个切图是500*1000,然后如果你要适配1080*1920(xxhdpi),而1080*1920的谷歌标准是360dp宽(1080/xxhdpi的density),所以500px相对于1080px底图也就是500/1080*360=167dp,然后把这个图片放到xxhdpi下面,然后调用Test.setCustomDensity(this, application,167),这样就会在任何模拟器里这个图片看起来宽度都是正好占满屏幕宽度。

而如果要适配720*1280(xhpdi),而720*1280的谷歌标准是360dp宽(720/xhdpi的density),所以500px相对于1080px底图也就是500/1080*360=167dp,然后把这个图片放到xhpdi下面,然后调用Test.setCustomDensity(this, application,167)

而如果要适配480*800(hpdi),而480*800的谷歌标准是320dp宽(480/hdpi的density),所以500px相对于480px底图也就是500/480*320=333dp,然后把这个图片放到hpdi下面,然后调用Test.setCustomDensity(this, application,333)

但是这个方式也有缺点:
这个方案依赖于设计图尺寸,但是项目中的系统控件、三方库控件、等非我们项目自身设计的控件,它们的设计图尺寸并不会和我们项目自身的设计图尺寸一样
当这个适配方案不分类型,将所有控件都强行使用我们项目自身的设计图尺寸进行适配时,这时就会出现问题,当某个系统控件或三方库控件的设计图尺寸和和我们项目自身的设计图尺寸差距非常大时,这个问题就越严重。
解决方案有两种:

方案 1
调整设计图尺寸,因为三方库可能是远程依赖的,无法修改源码,也就无法让三方库来适应我们项目的设计图尺寸,所以只有我们自身作出修改,去适应三方库的设计图尺寸,我们将项目自身的设计图尺寸修改为这个三方库的设计图尺寸,就能完成项目自身和三方库的适配

这时项目的设计图尺寸修改了,所以项目布局文件中的 dp 值,也应该按照修改的设计图尺寸,按比例增减,保持与之前设计图中的比例不变

但是如果为了适配一个三方库修改整个项目的设计图尺寸,是非常不值得的,所以这个方案支持以 Activity 为单位修改设计图尺寸,相当于每个 Activity 都可以自定义设计图尺寸,因为有些 Activity 不会使用三方库 View,也就不需要自定义尺寸,所以每个 Activity 都有控制权的话,这也是最灵活的

但这也有个问题,当一个 Activity 使用了多个设计图尺寸不一样的三方库 View,就会同样出现上面的问题,这也就只有把设计图改为与几个三方库比较折中的尺寸,才能勉强缓解这个问题

方案 2
第二个方案是最简单的,也是按 Activity 为单位,取消当前 Activity 的适配效果,改用其他的适配方案

该方案的补充与扩展:
https://juejin.im/post/5b7fafb351882542af1c75ad


4. 有人修改了与完善了美团的这种方式,如今日头条的方案

https://juejin.im/post/5bce688e6fb9a05cf715d1c2

至此完成本篇文章,可能有的地方有些啰嗦,我是想尽可能讲的详细一些,我把jess中的文章有些不清楚的地方我添加了一些解释


其他一些适配方式:

  1. 使用鸿洋大神的软件生成大量px文件放到项目中
    这种方式,是根据dp、density、px换算出来一堆px文件,分辨对应不同分辨率的手机,能解决大部分的适配问题,但是如果遇到分辨率比较高但是屏幕尺寸比较大的时候,这个设备的dpi会比较低,然后就会有些问题,而且这样也会有一大堆"values-宽X高"文件夹,里面有一大堆的px文件,增加apk体积。这时候应该可以再创建类似values-160dpi这种文件夹来解决,而且可以创建values-160dpi-1024x600这种文件夹,这些文件夹可以混用,优先用更精确的那个。

  2. 使用鸿洋大神的AutoLayout框架
    这个方式也不错,里面的源码我还没仔细看,我们公司也用的这种方式,不过有些控件会有问题,针对这些控件鸿洋有一些重写,不过没有的就需要自己去写了

  3. 约束布局(ConstraintLayout),这个我还没怎么去了解,待这几天看看研究一下再来完善该篇文章,在这篇文档里:https://developer.android.com/reference/android/support/percent/package-summary ,谷歌明确表示废弃了百分比布局库,而应该使用约束布局。

  4. 使用pt物理单位
    源链接: http://www.apkbus.com/blog-177177-76719.html
    涉及到的代码: https://github.com/Firedamp/Rudeness/blob/daad96fc6bcef8579fefdfc8e8ddc1046c89d26a/rudeness-sdk/src/main/java/com/bulong/rudeness/RudenessScreenHelper.java
    在上面这个链接里有段代码是

    resources.getDisplayMetrics().xdpi = size.x/designWidth*72f;
    
    

    在android系统里有个方法是这样的

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

    value * metrics.xdpi * (1.0f/72) = 该控件实际显示的px值可以换算为metrics.xdpi = 该控件实际显示的px值/value*72,其中value是该控件在设计图显示的pt值大小
    而我们要保证这个控件在任何分辨率下都相对于屏幕大小有固定的比例,只需要让 控件实际显示的px值/value=实际屏幕显示的px值/实际屏幕的pt值大小,也就是resources.getDisplayMetrics().xdpi = size.x/designWidth*72f

参考文章较多,记录几个
https://www.jianshu.com/p/c772cf49469a
http://www.apkbus.com/blog-177177-76719.html

你可能感兴趣的:(屏幕适配)