概述
Android系统发布十多年以来,关于Android的UI的适配一直是开发中一个很重要的环节,也是一个很棘手的问题。
Android适配最核心的问题有两个,其一,就是适配的效率,即把设计图转化为App界面的过程是否高效,其二如何保证实现UI界面在不同尺寸和分辨率的手机中UI的一致性。
这两个问题都很重要,一个是保证我们开发的高效,一个是保证我们适配的成效;今天我们就这两个核心的问题来聊一聊Android的适配方案。
PX直接适配
首先,大家都知道,在标识尺寸的时候,Android并不推荐我们使用px这个真实像素单位,因为不同的手机之间,分辨率是不同的,比如一个96*96像素的控件在分辨率越来越高的手机上会在整体UI中看起来越来越小。
出现类似于上图这样这样,整体的布局效果可能会变形,所以px这个单位在布局文件中是不推荐的。
dp直接适配
针对这种情况,Android推荐使用dp作为尺寸单位来适配UI.
那么什么是dp?
dp指的是设备独立像素,以dp为尺寸单位的控件,在不同分辨率和尺寸的手机上代表了不同的真实像素,比如在分辨率较低的手机中,可能1dp=2px,而在分辨率较高的手机中,可能1dp=3px,这样的话,一个96*96dp的控件,在不同的手机中就能表现出差不多的大小了。
那么这个dp是如何计算的呢?
我们都知道一个公式: px = dp(dpi/160) 系统都是通过这个来判断px和dp的数学关系,
那么这里又出现了一个问题,dpi是什么呢?
dpi是像素密度,指的是在系统软件上指定的单位尺寸的像素数量,它往往是写在系统出厂配置文件的一个固定值。
为什么要强调它是软件系统上的概念?
因为大家买手机的时候,往往会听到另一个叫ppi的参数,这个在手机屏幕中指的也是像素密度,但是这个是物理上的概念,它是客观存在的不会改变。dpi是软件参考了物理像素密度后,人为指定的一个值,这样保证了某一个区间内的物理像素密度在软件上都使用同一个值。这样会有利于我们的UI适配。
比如,几部相同分辨率不同尺寸的手机的ppi可能分别是是430,440,450,那么在Android系统中,可能dpi会全部指定为480.这样的话,dpi/160就会是一个相对固定的数值,这样就能保证相同分辨率下不同尺寸的手机表现一致。
而在不同分辨率下,dpi将会不同,比如:
根据上面的表格,我们可以发现,720P,和1080P的手机,dpi是不同的,这也就意味着,不同的分辨率中,1dp对应不同数量的px(720P中,1dp=2px,1080P中1dp=3px),这就实现了,当我们使用dp来定义一个控件大小的时候,他在不同的手机里表现出相应大小的像素值。
这种方式存在两个小问题,第一,这只能保证我们写出来的界面适配绝大部分手机,部分手机仍然需要单独适配,为什么dp只解决了90%的适配问题,因为并不是所有的1080P的手机dpi都是480,比如Google 的Pixel2(1920*1080)的dpi是420,也就是说,在Pixel2中,1dp=2.625px,这样会导致相同分辨率的手机中,这样,一个100dp*100dp的控件,在一般的1080P手机上,可能都是300px,而Pixel 2 中 ,就只有262.5px,这样控件的实际大小会有所不同。
为了更形象的展示,假设我们在布局文件中把一个ImageView的宽度设置为360dp,那么在下面两张图中表现是不一样的:
图一是1080P,480dpi的手机,图二是1080P,420dpi的手机
从上面的布局中可以看到,同样是1080P的手机,差异是比较明显的。在这种情况下,我们的UI可能需要做一些微调甚至单独适配。
宽高限定符适配
为了高效的实现UI开发,出现了新的适配方案,我把它称作宽高限定符适配。简单说,就是穷举市面上所有的Android手机的宽高像素值:
设定一个基准的分辨率,其他分辨率都根据这个基准分辨率来计算,在不同的尺寸文件夹内部,根据该尺寸编写对应的dimens文件。
比如以480x320为基准分辨率
宽度为320,将任何分辨率的宽度整分为320份,取值为x1-x320
高度为480,将任何分辨率的高度整分为480份,取值为y1-y480
那么对于800*480的分辨率的dimens文件来说,
x1=(480/320)*1=1.5px
x2=(480/320)*2=3px
......
这个时候,如果我们的UI设计界面使用的就是基准分辨率,那么我们就可以按照设计稿上的尺寸填写相对应的dimens引用了,而当APP运行在不同分辨率的手机中时,这些系统会根据这些dimens引用去该分辨率的文件夹下面寻找对应的值。这样基本解决了我们的适配问题,而且极大的提升了我们UI开发的效率。
但是这个方案有一个致命的缺陷,那就是需要精准命中才能适配,比如1920x1080的手机就一定要找到1920x1080的限定符,否则就只能用统一的默认的dimens文件了。而使用默认的尺寸的话,UI就很可能变形,简单说,就是容错机制很差。
smallestWidth 限定符适配方案
我们可以把smallestWidth 限定符屏幕适配方案当成宽高限定符方案的升级版,smallestWidth 限定符屏幕适配方案只是把dimens.xml文件中的值从px换成了dp,原理和使用方式都是没变的,这些在上面的文章中都有介绍,下面就直接开始剖析原理,smallestWidth 限定符屏幕适配方案长这样
什么是 smallestWidth
smallestWidth翻译为中文的意思就是最小宽度,那这个最小宽度是什么意思呢?
系统会根据当前设备屏幕的最小宽度来匹配values-sw<N>dp,为什么不是根据宽度来匹配,而要加上最小这两个字呢?
这就要说到,移动设备都是允许屏幕可以旋转的,当屏幕旋转时,屏幕的高宽就会互换,加上最小这两个字,是因为这个方案是不区分屏幕方向的,它只会把屏幕的高度和宽度中值最小的一方认为是最小宽度,这个最小宽度是根据屏幕来定的,是固定不变的,意思是不管您怎么旋转屏幕,只要这个屏幕的高度大于宽度,那系统就只会认定宽度的值为最小宽度,反之如果屏幕的宽度大于高度,那系统就会认定屏幕的高度的值为最小宽度
如果想让屏幕宽度随着屏幕的旋转而做出改变该怎么办呢?可以再根据values-w<N>dp(去掉sw中的s) 生成一套资源文件
如果想区分屏幕的方向来做适配该怎么办呢?那就只有再根据屏幕方向限定符生成一套资源文件咯,后缀加上-land或-port即可,像这样,values-sw400dp-land (最小宽度 400 dp 横向),values-sw400dp-port (最小宽度 400 dp 纵向)。
原理
其实smallestWidth 限定符屏幕适配方案的原理也很简单,开发者先在项目中根据主流屏幕的最小宽度 (smallestWidth)生成一系列values-sw<N>dp文件夹 (含有dimens.xml文件),当把项目运行到设备上时,系统会根据当前设备屏幕的最小宽度 (smallestWidth)去匹配对应的values-sw<N>dp文件夹,而对应的values-sw<N>dp文件夹中的dimens.xml文字中的值,又是根据当前设备屏幕的最小宽度 (smallestWidth)而定制的,所以一定能适配当前设备。
如果系统根据当前设备屏幕的最小宽度 (smallestWidth)没找到对应的values-sw<N>dp文件夹,则会去寻找与之最小宽度 (smallestWidth)相近的values-sw<N>dp文件夹,系统只会寻找小于或等于当前设备最小宽度 (smallestWidth)的values-sw<N>dp,这就是优于宽高限定符屏幕适配方案的容错率,并且也可以少生成很多values-sw<N>dp文件夹,减轻App的体积。
smallestWidth 的值是怎么算的
要先算出当前设备的smallestWidth值我们才能知道当前设备该匹配哪个values-sw<N>dp文件夹
现在来举栗:
我们假设设备的屏幕信息是1920 * 1080、480 dpi
根据上面的规则我们要在屏幕的高度和宽度中选择值最小的一方作为最小宽度,1080 < 1920,明显1080 px就是我们要找的最小宽度的值,但最小宽度的单位是dp,所以我们要把px转换为dp
帮助大家再巩固下基础,下面的公式一定不能再忘了!
px / density = dp,DPI / 160 = density,所以最终的公式是px / (DPI / 160) = dp
所以我们得到的最小宽度的值是360 dp (1080 / (480 / 160) = 360)
现在我们已经算出了当前设备的最小宽度是360 dp,我们晓得系统会根据这个最小宽度帮助我们匹配到values-sw360dp文件夹下的dimens.xml文件,如果项目中没有values-sw360dp这个文件夹,系统才会去匹配相近的values-sw<N>dp文件夹
dimens.xml文件是整个方案的核心所在,所以接下来我们再来看看values-sw360dp文件夹中的这个dimens.xml是根据什么原理生成的
dimens.xml 生成原理
dimens.xml 的生成,就要涉及到两个因数,第一个因素是 最小宽度基准值,第二个因素就是您的项目需要适配哪些 最小宽度,通俗理解就是需要生成多少个 values-sw<N>dp 文件夹。
第一个因素
最小宽度基准值是什么意思呢?简单理解就是您需要把设备的屏幕宽度分为多少份,假设我们现在把项目的最小宽度基准值定为360,那这个方案就会理解为您想把所有设备的屏幕宽度都分为360份,方案会帮您在dimens.xml文件中生成1到360的dimens引用,比如values-sw360dp中的dimens.xml是长这样的
values-sw360dp指的是当前设备屏幕的最小宽度为360dp(该设备高度大于宽度,则最小宽度就是宽度,所以该设备宽度为360dp),把屏幕宽度分为360份,刚好每份等于1dp,所以每个引用都递增1dp,值最大的dimens引用dp_360值也是360dp,刚好覆盖屏幕宽度
下面再来看看将最小宽度基准值定为360时,values-sw400dp中的dimens.xml长什么样
values-sw400dp指的是当前设备屏幕的最小宽度为400dp(该设备高度大于宽度,则最小宽度就是宽度,所以该设备宽度为400dp),把屏幕宽度同样分为360份,这时每份就等于1.1111dp了,每个引用都递增1.1111dp,值最大的dimens引用dp_360同样刚好覆盖屏幕宽度,为400dp。
第二个因素
第二个因数是需要适配哪些最小宽度?比如我们想适配的最小宽度有320dp、360dp、400dp、411dp、480dp,那方案就会为您的项目生成values-sw320dp、values-sw360dp、values-sw400dp、values-sw411dp、values-sw480dp这几个资源文件夹,像这样
方案会为我们需要适配的最小宽度,在项目中生成一系列对应的values-sw<N>dp,在前面也说了,如果某个设备没有为它提供对应的values-sw<N>dp,那它就会去寻找相近的values-sw<N>dp,但如果这个相近的values-sw<N>dp与期望的values-sw<N>dp差距太大,那适配效果也就会大打折扣。
理论上values-sw<N>dp 文件夹生成的越多越好,但也不实际,因为会增加apk的体积,这里建议以10个单位为步长。
优点
非常稳定,极低概率出现意外
不会有任何性能的损耗
适配范围可自由控制,不会影响其他三方库
在插件的配合下,学习成本低
缺点
在布局中引用dimens的方式,虽然学习成本低,但是在日常维护修改时较麻烦
侵入性高,如果项目想切换为其他屏幕适配方案,因为每个Layout文件中都存在有大量dimens的引用,这时修改起来工作量非常巨大,切换成本非常高昂
无法覆盖全部机型,想覆盖更多机型的做法就是生成更多的资源文件,但这样会增加App体积,在没有覆盖的机型上还会出现一定的误差,所以有时需要在适配效果和占用空间上做一些抉择
不能自动支持横竖屏切换时的适配,如上文所说,如果想自动支持横竖屏切换时的适配,需要使用values-w<N>dp或屏幕方向限定符再生成一套资源文件,这样又会再次增加App的体积
最小宽度限定符适配(项目地址)
今日头条屏幕适配方案
原理
今日头条屏幕适配方案的核心原理在于计算density的方式不同,根据以下公式算出 density
当前设备屏幕总宽度(单位为像素)/ 设计图总宽度(单位为 dp) = density
density 的意思就是 1 dp 占当前设备多少像素
为什么要算出 density,这和屏幕适配有什么关系呢?
大家都知道,不管你在布局文件中填写的是什么单位,最后都会被转化为px,系统就是通过上面的方法,将你在项目中任何地方填写的单位都转换为px的。
所以我们常用的px转dp的公式dp = px / density,就是根据上面的方法得来的,density在公式的运算中扮演着至关重要的一步。
我们还得明白一点,今日头条适配方案也是只能以高或宽中的一个作为基准,进行适配,那为什么不能,高以高为基准,宽以宽为基准,同时进行适配呢?
这是因为大部分市面上的 Android 设备的屏幕高宽比都不一致,特别是现在大量全面屏的问世,这个问题更加严重,不同厂商推出的全面屏手机的屏幕高宽比都可能不一致,这时我们只以高或宽其中的一个作为基准进行适配,就会有效的避免布局在高宽比不一致的屏幕上出现变形的问题。
我再来说说density,density在每个设备上都是固定的,DPI / 160 = density,屏幕的总 px 宽度 / density = 屏幕的总 dp 宽度
设备 1,屏幕宽度为1080px,480DPI,屏幕总dp宽度为1080 / (480 / 160) = 360dp
设备 2,屏幕宽度为1440,560DPI,屏幕总dp宽度为1440 / (560 / 160) = 411dp
可以看到屏幕的总dp宽度在不同的设备上是会变化的,但是我们在布局中填写的dp值却是固定不变的。
那我们能怎么办呢?
我们要想完美适配,那就必须保证这个 View 在任何分辨率的屏幕上,与屏幕的比例都是相同的
这时今日头条的方案就该出场了,我们都知道:
屏幕的总 px 宽度 / density = 屏幕的总 dp 宽度
DPI / 160 = density
而今日头条的公式是:
当前设备屏幕总宽度(单位为px)/ 设计图总宽度(单位为 dp) = density
只要 density 根据不同的设备进行实时计算并作出改变,就能保证 设计图总宽度 不变,也就完成了适配。
density修改的一段源码
验证方案可行性
假设设计图总宽度为375 dp,一个View在这个设计图上的尺寸是50dp * 50dp,这个View的宽度占整个设计图宽度的13.3%(50 / 375 = 0.133),那我们就来验证下在使用今日头条屏幕适配方案的情况下,这个View与屏幕宽度的比例在分辨率不同的设备上是否还能保持和设计图中的比例一致。
验证设备 1
屏幕总宽度为1080 px,根据今日头条的的公式求出density,1080 / 375 = 2.88 (density)
这个50dp * 50dp的View,系统最后会将高宽都换算成px,50dp * 2.88 = 144 px(根据公式dp * density = px)
144 / 1080 = 0.133,View实际宽度与屏幕总宽度的比例和View在设计图中的比例一致 (50 / 375 = 0.133),所以完成了等比例缩放
某些设备总宽度为1080 px,但是DPI可能不同,是否会对今日头条适配方案产生影响?其实这个方案根本没有根据DPI求出density,是根据自己的公式求出的density,所以这对今日头条的方案没有影响
上面只能确定在所有屏幕总宽度为1080 px的设备上能完成等比例适配,那我们再来试试其他分辨率的设备
验证设备 2
屏幕总宽度为1440 px,根据今日头条的的公式求出density,1440 / 375 = 3.84 (density)
这个50dp * 50dp的View,系统最后会将高宽都换算成px,50dp * 3.84 = 192 px(根据公式dp * density = px)
192 / 1440 = 0.133,View实际宽度与屏幕总宽度的比例和View在设计图中的比例一致 (50 / 375 = 0.133),所以也完成了等比例缩放
优点
使用成本非常低,操作非常简单,使用该方案后在页面布局时不需要额外的代码和操作,这点可以说完虐其他屏幕适配方案
侵入性非常低,该方案和项目完全解耦,在项目布局时不会依赖哪怕一行该方案的代码,而且使用的还是Android官方的API,意味着当你遇到什么问题无法解决,想切换为其他屏幕适配方案时,基本不需要更改之前的代码,整个切换过程几乎在瞬间完成,会少很多麻烦,节约很多时间,试错成本接近于 0
可适配三方库的控件和系统的控件(不止是Activity和Fragment,Dialog、Toast等所有系统控件都可以适配),由于修改的density在整个项目中是全局的,所以只要一次修改,项目中的所有地方都会受益
不会有任何性能的损耗
缺点
暂时没发现其他什么很明显的缺点,已知的缺点有一个,那就是第三个优点,它既是这个方案的优点也同样是缺点,但是就这一个缺点也是非常致命的
只需要修改一次density,项目中的所有地方都会自动适配,这个看似解放了双手,减少了很多操作,但是实际上反应了一个缺点,那就是只能一刀切的将整个项目进行适配,但适配范围是不可控的
这样不是很好吗?这样本来是很好的,但是应用到这个方案是就不好了,因为我上面的原理也分析了,这个方案依赖于设计图尺寸,但是项目中的系统控件、三方库控件、等非我们项目自身设计的控件,它们的设计图尺寸并不会和我们项目自身的设计图尺寸一样
当这个适配方案不分类型,将所有控件都强行使用我们项目自身的设计图尺寸进行适配时,这时就会出现问题,当某个系统控件或三方库控件的设计图尺寸和我们项目自身的设计图尺寸差距非常大时,这个问题就越严重
AndroidAutoSize(项目地址)