前置知识
- 屏幕尺寸,是指对角线的长度,单位一般是英寸,1 inch=2.54 cm
- 屏幕分辨率:是指在水平和垂直的像素点数,单位是px,1px=1像素点,一般描述分辨率是垂直水平,比如1920px1080px
- ppi,像素密度(pixels per inch),指每英寸的物理像素的数量,ppi是设备的物理属性,取决于屏幕的自身配置,计算的方式为:
- dpi(dot per inch),原来是用在印刷行业中描述每英寸包含多少个点,在Android中则是用来描述屏幕像素密度,屏幕像素密度决定了在软件概念上单位距离对应的像素总数,是在手机出厂的时候被写入系统配置文件的一个属性值,一般情况下是无法用户是无法修改的,但是开发者模式中有修改该值的入口,是软件上一个可以修改的值。
- dp(dip,density independent pixels),密度无关像素,是我们开发常用到的单位,他与px的换算关系的,160dip的设备上,1dp=1px,320dpi的设备上,1dp=2px,其中的换算关系也叫做density
ppi与dpi的不同点:dpi是每英寸上有多少个点,点越多越细腻,针对的是印刷业或者是应用到UI切的图片中,ppi是每英寸的像素数,针对的是手机屏幕,值越高越细腻,它们二者之间没有换算的关系,dpi是通过
DisplayMetrics
获取到,是写入到系统配置中的,ppi是手机的配置决定的。一般而言,ppi越大dpi越大
我们以模拟器的两个手机配置作为例子,
- Pixel2:1080*1920的手机,它的dpi是420(配置文件写入),它的density是2.625(配置文件写入)
- Pixel4XL:1400*3040的手机,它的dpi是560(配置文件写入),它的density是3.5(配置文件写入)
假如我们使用px开发,100px的控件宽度,在第一个手机则是宽度是100/1080=0.09259,第二个手机则是100/1400=0.071428。
以dp则是,100dp的控件宽度,在第一个手机则是宽度是1002.625/1080=0.243055,在第二个手机上则是:1003.5/1400=0.25
可以看到使用dp进行开发,控件的宽度是能比较完美的适配屏幕的,误差会小很多。
其中的换算比例我们可以通过代码获取到。
通过DisplayMetrics
类,其中
densityDpi
:就是dpidensity
:就是换算因子,px=density*dp,在160dpi上,不修改density的值的话,density=1,该值是可以修改的(今日头条的方案)widthPixels
和heightPixels
,为屏幕的长*高的分辨率
Android中的分辨率的定义关系(我们的设备会根据自身的dpi去不同的文件夹中取合适的文件进行读取,没有的时候才会使用默认的):
- ldpi:~120dpi的,即低密度
- mdpi:120~160,中密度
- hdpi:160~240,高密度
- xhapi:240~320,高高密度
- xxhdpi:320~480,高高高密度
- xxxhapi:480~640,高高高高密度
- nodpi:适用于所有密度的资源,这些是与密度无关的资源,无论当前屏幕的密度是多少,系统都不会缩放以此限定符标记的资源
- tvdpi:一般用于电视开发,基于mdpi-hdpi之间
屏幕需要适配的原因
- 屏幕尺寸多样,各种尺寸大小的都有
- 屏幕分辨率多样,同样尺寸大小的手机,屏幕的分辨率也不一样。
二者决定了屏幕密度也是非常的多种多样的,我们最终在屏幕展示的单位还是px,不是dp,只是dp通过转换之后化为了px。
那我们有了dp之后,为啥还需要对屏幕适配呢?
假如是1440*2880,560 dpi
转为dp之后是411*731
和1080*1920,420 dpi
转为dp之后是411*822
,占据一半则是205.5dp
即可。
但是,还是有点问题的,以上面两个机型为例子:1080*1920,420dpi
,换算为dp,则是411*731
和1400*3040,560dpi
,换算为dp,则是400*868
,可以看到第一个手机需要的是205dp,第二个手机是200dp,这其中还是有误差在的,无法完美适配。还有一些机型还比较的离谱,比如华为nova5:1080*2259,480dpi
&三星s101080*2137 420dpi
,前者换算为dp宽度是360dp
后者是411dp
,它们的相差是有点多的,这就是我们有了dp之后还需要适配的原因。
适配方案
今日头条方案
今日头条的方案:它基于下面的计算公式:
1 | px=density*dp |
在布局文件中声明的dp值,都是通过TypedValue.applyDimension()
方式,方法如下:
1 | public static float applyDimension(@ComplexDimensionUnit int unit, float value, |
如果能够修改density
的值的大小,要求修改后的计算出的屏幕宽度等于设计稿的宽度,这样就可以在布局文件中直接使用设计稿给出的各个dp的宽高值,而且使得控件在不同屏幕手机上占据同样的比例。
举个例子:假如设计稿是以1080*1920,420dpi,density为2.625,换算为dp则是411*731,假设一个宽度为100dp的控件,占据的总体宽度是
1002.625/1080=0.2430,以上面的机型为例,适配之前: 华为:
10802259,480dpi,正常情况下
density为3,占据宽度比例为
1003/1080=0.2777 Pixel4XL:
14003040,560dpi,正常情况下
density为3,占据宽度比例为
100*3.5/1400=0.25`
采用今日头条的方案,动态修改density
值(每个手机都不一样)=设备的真实宽度(px)/设计稿的宽度(dp),上面两个手机则是:2.6277
&3.4063
,修改之后
华为:1080*2259,480dpi
,占据宽度比例为100*2.6277/1080=0.2433
Pixel4XL:1400*3040,560dpi
,,占据宽度比例为100*3.4063/1400=0.2433
可以看到除了有一些的精度丢失,可以忽略不计,只要我们能动态修改density
就可以,density
是一个在DisplayMetrics
中的公开变量,我们只需要在Application初始化的时候或者是Activity
初始化的时候去计算一下就可以了,因此本方案是比较的简介有效的,不需要多套的dimens
。
这套方案的缺点是对老项目不友好,假如一开始不是使用这一套方案,则需要比较大的工作量去对现有的尺寸进行适配,还有就是某个系统空间或者是第三方控件的设计图尺寸和我们项目自身的设计图差距较大的时候,造成的失真也比较的严重,该方案是一个一刀切的方式,所有的使用了density
进行计算的控件的地方都会进行修改。举一个例子,假如某个第三方库的View设计为100*100dp
,设计图的最大宽度是1000dp,这时候View占据的比例100/1000=0.1
,如果我们使用了适配,那么这个占据的比例就会出问题。
解决的方式是:
- 修改第三方依赖源码,使得能较好的适配
- 修改设计图(代价大)
- 有第三方控件的库不适用改方案,我们只在activity层面进行应用
使用宽高限定符
该方案是使用px
作为单位而不是dp
。
我们的资源是支持宽高限定符的,比如values
文件夹是默认的values-480*320
,假如我们的设备是480*320
,就会优先去这个地方获取资源,这是我们使用宽高限定符去适配的前提。我们使用我们的设计图为基准,比如设计图为480*320
,我们定义一些列的变量,在480*320
下,变量的对应关系为1:1,比如dimen1_x=1px,dimen1_y=1px
,然后其他的分辨率则是x/[480/320],x为他们的对应高度or宽度
,比如在800*400
的文件夹下,高度换算因子为800/480
,高度的dimen1_y=1.67px
,宽度为dimen1_x=1.25px
,然后我们的布局文件就使用dimen[value]_x|y
即可,这样我们开发的时候。
该方案的缺点是:我们预生成所有的屏幕的适配文件,需要进行穷举,容错率一般。
基于SmallestWidth限定符
它的文件夹的命名规则:
1 | values // 默认 |
适配的依据是最小宽度限定符,该方案是不考虑屏幕的方向,它的smallestwidth是指最短的那一条边,原理与宽高限定符一样,通过换算比例来为不同屏幕尺寸分别准备一套dimens
文件,应用在运行的时候再去引用相匹配的dimens
文件。假如设计师按照1080*1920
设计的图,基准分辨率就是1080
,以380dp为例,则1px对应的宽度为360/1080
,它的文件是:
1 | <?xml version="1.0" encoding="utf-8"?> |
使用的时候,设计师标记控件是多少px我们就引用那个px即可。在查找不到对应的文件夹的时候,系统会查找离得最近的,比如设备为370dp
的设备,则是会找到values-sw360dp
文件夹,都存在的时候才使用默认的。
它的优点是比宽高限定更加的灵活,不会对旧项目的内容造成影响,缺点是需要引入较多的dimens文件侵入性较高,后续换其他方案麻烦,不能自动支持横竖屏幕的切换,需要加上
开发建议 & 其他
其实无论是那种方案,都会有有一些优点缺点,但是我们开发中可以避免的时候就避免,比如
- 线性布局使用weight
- 复杂布局使用约束布局或者是相对布局,不要轻易写死宽高
- 多使用自适应的宽高
我们的资源文件其实是支持很多的限定符的,可以看应用资源概览,在遇到一些较少的机型的时候,可以通过限定符进行适配,比如
1 | //屏幕方向 |
有限定符命名多个以上的,需要按照优先级排序:例如drawable-hdpi-port/
是错误的的drawable-port-hdpi/
是正确的
可以看看Android如何查找最匹配的资源:Android 如何查找最匹配的资源
参考文章
直接把反编译得到的Jar存入拖入即可查看。