剖析Android屏幕适配及各方案

  最近陆陆续续被一些android屏幕适配的文章刷屏了,我发现有些问题在看别人写的文章时候还是比较不容易理解的,在这里我们把这些东西捋一捋,来讲讲android屏幕适配的原理,还有之前用过的屏幕适配框架,以及目前比较火的适配框架,欢迎探讨...

为什么要适配?


  目前,很多厂商都推出了自己的全面屏手机,例如小米mix系列,蓝绿兄弟的 find-x,nex等等,不仅用了很多新的技术,而且体验方面也是流畅到没朋友(吃鸡必备,视角广),遥想当年用htc卡的不行也是美滋滋,在惊叹科技进步的同时,也不由感. 慨. 万.千.啊!

  好了我们言归正传..

  安卓屏幕适配从很久以前到现在一直是开发从业人员比较头疼的问题,源于google亲爸爸对他的定位--开源,所以任何厂商都可以对这个系统进行定制及修改,这就导致了国内各个型号分辨率,各种尺寸手机层出不穷,碎片化非常严重,再到现在全面屏即将成为街机,除了以往16比9 的手机之外,又有了18比9等其他屏占比手机,所以我们在开发的过程当中,需要进行屏幕上的适配调整.

剖析Android屏幕适配及各方案_第1张图片
image

   对于这张图,很多人都不陌生,这是2014年表示安卓手机碎片化的一张图,仅仅是2014年的,更别说4年后的今天了 (没找到2018年的图),所以,对于安卓这个打败了塞班划时代的系统,让我们又爱又恨.一方面我们可以在不重复造轮子的情况下,在巨人的肩膀上登高望远,一方面又受困于整个市场没有一个统一的规范,所以这张图很清晰的告诉大家这就是我们所要面临的适配问题

一些概念


  碎片虽然多,但也不是一个一个进行适配的,我们只需找出碎片中的共性,然后开发一些通用的手法,让每一个机型都达到我们的要求,当然,我们要进行屏幕适配,有几个概念要了解一下

  • DPI(屏幕密度):为单位英寸的像素点(每一英寸有多少个像素点).
    这个概念非常重要,这个参数一般是在手机出厂时候写死在配置文件中的,之前我以为它是真正的屏幕密度,其实不是,它是在参考了物理密度之后,根据物理密度的区间,设定了一个统一的值.

  • PPI:这个是真正的物理密度,如果你自己想算一下,那么假如我们的手机是1080x1920分辨率的,尺寸为5寸(对角线长度),那么我们可以根据勾股定理求出对角线的像素数,再除以5,便是当前手机的密度,那我们举的例子密度为440,当然如果相同分辨率手机,尺寸越大那么密度就小,尺寸越小,密度就越大,这个也很好理解

Tips:以上面两个概念举个例子,看张图



我们可以看到相同分辨率的手机,尺寸不一样,密度肯定也不一样,但是dpi同为420,说明ppi在一个区间之内 ,统一给到了dpi为420,这样会方便我们适配,也就是说我们写在项目中的dp,在两台分辨率一致,尺寸不一致的手机上,表现是一致的.

  • PX:像素点,1px=1像素点
  • DIP(等于dp):这是google提供的一种单位,方便我们去适配,
    如果我们在开发时候将px直接写死在项目里,那么不同分辨率的手机显示比例肯定不一致,比如宽为1080px(像素单位)的手机中与一条横线是800px,那么他在720px的手机上肯定会占满整个屏幕,导致显示不全,所以为了避免这种情况,我们在项目中写的都是dp,在安卓系统中,会以160dpi为基准 也就是说手机密度为160dpi ,1dp=1px;320dpi,1dp=2px 以此类推,那我们写在项目中的dp也会因屏幕密度不同转化成不同的px呈现出来,这样可以解决大部分适配问题,根据之前的例子所以这里便有两个公式
          1. density=dpi/160,   2. dp=px/density
  • density:上文说到屏幕密度,是定死的,那肯定是不变的,那么density也是不变的,我们可以通过这个density用第二个公式去获取我们应该把我们的控件到底写成多少dp,当然这要依设计图而定,通常情况下如果设计图是1080P的 那么密度为480,density便为3,那么UI标出来的像素/3,即为dp数值,简单提一下,今日头条屏幕适配方案的核心思想就是通过修改这个density值,来进行屏幕适配的

小结:通常情况下我们用写dp这个单位去做适配,确实可以解决大部分问题,那如果有一台手机分辨率为1080x1920,dpi为440,并不是480,那么相同的dp所求出来的px,440就比480小(上面俩公式),如果你的控件写的是dp,那么肯定会一个长,一个短,所以接下来的几种方案都是为了解决这个问题而做的

各种适配方案


1.宽高限定符适配

  当我们平常用as去开发的时候,在资源文件res下面,有一个vaules文件夹,我们写的各种宽度跟高度,通常会写在这个文件夹中的一个叫dimens.xml文件中,然后在布局中引用,当然,默认是所有分辨率的手机都会引用这个文件夹,限定符适配方案就是在这里,新建市面上所有分辨率的的vaules文件夹,那么不同分辨率的手机会寻找它所在分辨率所在的文件夹中的dimens文件,并引用,例如:

├── src/main  
│   ├── res  
│   ├── ├──values  
│   ├── ├──values-800x480  
│   ├── ├──values-860x540  
│   ├── ├──values-1024x600  
│   ├── ├──values-1024x768  
│   ├── ├──...  
│   ├── ├──values-2560x1440

通常情况下,我们会将1dp=1px的480x320分辨率的文件夹作为基准,那么我们所定义的vaules-480x320中的dimens文件肯定以1px为单位逐次递增:(以宽度举例)

    1px
    2px
    3px
    4px
    5px
    ...

那么如果是800x480分辨率的dimens 就应该为(宽度)
480(现在的)/320(基准的)=1.5 那么对应的dimens为:

    1.5px
    3px
    4.5px
    6px
    7.5px
    ...

同理:如果是1920x1080

    3px
    6px
    9px
    12px
    15px
    ...

好,那如果设计图是按照1080x1920设计的,UI小姐姐在其中一张图上标了一个100x100的按钮,那么我们只需要在这个1080x1920分辨率的dimens文件中找到100的值,并引用就可以了.

  • 小结:这是一个比较古老的方案,是安卓屏幕适配的先驱者,很多开源的适配项目都是根据这个这个原理进一步填充跟扩展的,我们发现其完全摒弃了dp(虽然写dp也是会被转成px),使用起来非常简单,通过工具也可以批量生成各种文件,能保证绝大部分手机比例正常显示,也是比较成熟的,但是这个方案比较依赖于精准限定,也就是说你的手机分辨率如果没有出现在我的项目中,比如1080x2140,那对不起,他会使用默认的valus,这样我们的界面肯定会显示不正常,还有就是市面上手机分辨率层出不穷,那我到底要建多少个文件夹,当我每次用到res,看到几十个vaules,项目看着臃肿不说,dimens多也会增大apk体积,不过总体来说,这确实是一种良好适配方式

2.smallestWidth适配
  顾名思义,最小宽度适配,也属于限定符适配的一种,只不过是按照手机宽,高的最小值为基准进行适配,不同的是,我们需要在项目中新建这个样子的文件夹:

├── src/main  
│   ├── res  
│   ├── ├──values  
│   ├── ├──values-sw320dp  
│   ├── ├──values-sw360dp 
│   ├── ├──values-sw400dp  
│   ├── ├──values-sw410dp  
│   ├── ├──...  

文件夹中的数字320dp,360dp等就是我们所说的屏幕宽高最小值所代表的dp值,比如宽度为1080px的手机,dpi为480,那么根据第前面两个公式
  density=480/160=3
  dp=1080/3=360dp
那么宽度为360dp的手机都会从sw360dp这个文件中去读取dimens数值,其他同理;那每一个文件夹中的dimens文件应该怎么写呢?通过文件夹名称我们就会发现,我们是用dp去做单位的,它主要进行了两个步骤
1.将我们的手机屏幕宽度根据dpi转换成了dp
2.然后计算设计图宽度每一像素占多少dp
举个例子:
假如我们设计图宽度为750px(ios通用设计宽度),如果屏幕是360dp,1px占多少dp怎么算?
0.48=360/750


360dp
0.00dp
0.48dp
0.96dp
1.44dp
1.92dp
...

这样我们算出的dp跟设计图所标注的px完全是一个比例,也就是说设计图写的是多少像素,我们在项目里直接@dimen/qb_px_像素值
简单到没朋友...
当然生成这些文件也是有工具的,工具戳这里,用ide直接import就可以
使用:

├── DimenTypes.class 
 //适配Android 3.2以上   大部分手机的sw值集中在  300-460之间
     DP_sw__300(300),  // values-sw300
     DP_sw__310(310),
     DP_sw__320(320),
     DP_sw__360(360);
    // 想生成多少自己以此类推
1.换成自己要用的文件夹
├── DimenGenerator.class 
1.首先根据设计图尺寸修改
/**
     * 设计稿尺寸(将自己设计师的设计稿的宽度填入)
     */
    private static final int DESIGN_WIDTH = 750;

    /**
     * 设计稿的高度  (将自己设计师的设计稿的高度填入)
     */
    private static final int DESIGN_HEIGHT = 2150;
2.修改输出路径,执行main方法
public static void main(String[] args) {
        int smallest = DESIGN_WIDTH>DESIGN_HEIGHT? DESIGN_HEIGHT:DESIGN_WIDTH;  //     求得最小宽度
        DimenTypes[] values = DimenTypes.values();
        for (DimenTypes value : values) {
            MakeUtils.makeAll(smallest, value, "/Users/gaox/Desktop/smallestwidth/dimens_sw");
        }

    }
  • 小结:smallestWidth方案还是比较友好的,因为它不会像宽高限定符适配方案需要精准命中,假如我们生成的文件夹并没有匹配到,那么它会向下进行匹配,这样就不存在匹配不到脱机显示,那为了适配精准,一般手机宽度dp值在300-450,我们可以以10dp位单位去生成文件夹,这样即使个别手机没有精准匹配到,相差也基本上可以忽略不计,这套适配方案用于新项目上非常简单,只需要在项目初始阶段,生成配套文件夹就可以了,那如果有老项目想用,设计图尺寸又不一样,那改起来就费劲了,而且很多人还是比较排斥建很多个vaules文件夹的,总体来看,如果没有更好的方案,还是比较推荐使用的

3.今日头条适配方案

  通过上面两个方案,很明显我们可以发现,适配的核心,就是把设计图中所标注的px,根据手机dpi不同,分别换算成相应的px(宽高限定符),和相应的dp值(smallestwidth),也就是说我们写在layout中的dp(px),会根据手机dpi不同而改变,那假如现在有一个控件,我就想在布局里写死,我心情不好,就想这么写:

             

那么能不能有办法进行适配一下呢?那我们就要说一下今日头条团队的适配方案了,他们把我们的适配思路转换了一下

  之前我们说过手机的dpi是固定的,那么根据公式density=dpi/160我们的density是可以算出的,你也可以理解为固定的,我们平常写在布局中的dp也可以根据公式dp=px/density算出来,button中的100dp也许你就是这么算的,但是注意,如果我们想要用头条的方案去适配:

 density=dpi/160这个公式要替换成
 density=当前设备屏幕总宽度(单位为像素)/ 设计图总宽度(单位为 dp)

也就是说这个density不是通过dpi计算出来的,而是我们根据设计图自己算的,那么算出这个density的最终目的就是告诉你1dp等于多少px,没错,跟smallestWidth刚好完全相反!

   这里不得不说android的一个机制,不管我们写在布局中的单位是px,dp,sp等等什么也好最终都会转换为px,原因在这里:

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

我们可以发现,写在项目中的dp会乘以density最终转换成px,在不动任何代码的情况下,可以在app初始化的时候,比如说application中获取屏幕的宽度,计算出density,然后修改系统的density为你算的就可以了,方法如下(解决了字体显示不正确等issues):

private static float sNoncompatDensity;
private static float sNoncompatScaledDensity;

private static void setCustomDensity(@NonNull Activity activity, @NonNull final Application application) {
        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 / 360;
        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;
    }

有两个问题需要特别说明一下:
①以上举例是以宽度进行density计算的,却不是一定要以宽度算,我们要根据页面的实际情况选取是按照宽度计算还是按照高度计算,通常一个不支持上下滑动的页面,内容又比较短,我们便要在高度上让其维持在一定比例显示,所以要按高度算,只是把上面方法,宽度改成高度就行了
②不仅仅能在application中进行全局修改,该方案也支持在每一个activity中修改,这样会更加灵活,哪怕每一个页面的设计尺寸都不一样,都能进行合理适配,最关键的是几乎没有工作量

  • 小结:这套方案以设计图的宽,高(dp)为基准,计算出的density含义其实是,设计图中的1dp占屏幕宽/高中的多少个像素,那么在用density乘以我们layout中的dp值,再除以整个屏幕的宽/高,这个比例在不同分辨率的手机上绝壁是相等的,举个例子:
①宽1080px,设计图为360dp,控件宽:100dp
0.2777=1080/360*100/1080
②宽720px,设计图为360dp,空间宽:100dp
0.2777=720/360*100/720

这套方案,我个人是比较推荐的,不管是新项目,还是老项目,只要你想用,代码几乎不用任何修改,全是系统的api,侵入性非常之低,使用上又非常灵活,如果哪天在使用过程中觉得哪里有问题,更换其他更好的适配方案又非常安全,删掉方法就行了,不过我们修改density,修改的是本项目尺寸按照设计图来计算,一些三方库并不是按照我们的这个设计图尺寸设计的,这样难免会有问题,比如一些dialog,toast,popwindow等库显示不正确,那么我们便需要调整,我们可以调整当前activity不用,或者调整成跟三方库一致的尺寸,也有大佬针对这些问题专门做了调整,传送门在这:大佬的头条适配究极方案,使用起来更简单.

  • 综上所述
    安卓手机的碎片化在可以预见到的将来会越来越严重,这个问题完全没有办法解决,现在的流行的适配方案,可以根据自身的项目需求进行选取,当然,鱼和熊掌不能兼得,我们往往再得到了我们想要的同时,也会失去一些东西,比如我们适配是要保证每一台手机显示的一致性,那大屏跟小屏显示一致,那大屏还有什么意义?有人会说,大屏手机就是为了让用户在一屏内看到更多的信息而设计,这个很难说谁在本末倒置,只能仁者见仁智者见智,方案永远没有最好的,只有符合设计需求的适配才是最好的.
    那如果不用上面说的方案是不是就没法适配了呢?
    当然是不是,
    1.一般情况下dp是可以满足的.
    2.布局尽量不要写死 ,wrap_content ,match_parent,weight等等都是有意义的
    3.看到设计图时,要想想如果这个出现在大屏手机上哪里会有问题,写的时候可以用代码进行比例换算(屏幕的宽或者高),通常情况下需要算高
    4.可以考虑用constraintlayout进行布局编写

你可能感兴趣的:(剖析Android屏幕适配及各方案)