为什么要适配呢?这篇文章来聊一聊。
因为现在有着五花八门的Android手机厂商,生产出来的设备有着各种各样的分辨率。这一点不像Apple,设备类型就那么有限的几种。
为了做到让一张设计稿,在不同的设备上看起来都差不多,或者说不会相差太多,这就是适配的目的。
几个概念
屏幕大小
常见的有5.0英寸、6.1英寸等等,这个长度指的是屏幕对角线的长度,根据勾股定理便可以根据长和宽算出来。为什么要用对角线的长度来表示一个屏幕的大小呢,对角线相等的屏幕可以有多种长和宽。查了下,是历史遗留原因,因为最开始的时候都是用圆形显示管来显示的,所以用直径表示显示管的大小,也就是其内矩形的对角线。后来工艺发展,做成了矩形,但这种方式保留了下来屏幕分辨率
常见的有720x1280、1080x1920、1440x2560,其中的数字指的是该方向的像素块的数量。比如720x1280的屏幕,在较短的方向有720个像素块,较长的方向有1280个像素块,不同的屏幕像素块的大小可能是不同的。比如两块屏幕都是1080x1920,但一块是5.0英寸另一块是5.9英寸,那5.0的像素块就要小一些。px
pixel,像素ppi
全称pixel per inch,指每英寸上的像素数量,比如1080宽,物理长度为3.375英寸,那么每英寸上的像素数量就是1080/3.75=320个,一般针对的是描述屏幕dpi
和ppi类似,全称是dots per inch,指每英寸上墨点的数量,针对的是打印到纸上,比如160dpi,意思就是打印出一条1英寸长的线时,这条线上会有160个墨点。在 Android 里,这两个值是相等的,在屏幕上显示一张图片时,一个像素块便相当于一个墨点,但是在打印机上打印时这两个不一定相等。
因为不同屏幕有着不同的分辨率,所以我们不能在代码中以像素为单位设置图片的大小,比如一个ImageView设置成540px宽,有的屏幕上是2英寸,有的屏幕上却是3英寸,这样的效果是不可行的,因此,这里引出了新概念:
dip
全称是density independent pixel,密度独立像素,简称dp。这是一个基于像素、和屏幕密度相关的长度单位。规定:在密度是160的屏幕上,1dp=1px,所以密度是320的屏幕上,1dp=2px,总之,密度越高的屏幕上,1dp代表的像素越多。density
密度,等于dpi/160,意思就是在这块屏幕上,1dp等于多少px
在Android中,用dp代替px的好处是什么呢?例如,设计稿是1080x1920,屏幕密度是320,所以density等于2。上面一张160px宽的图片,即160/2=80dp。
在dpi为320的屏幕上,也就是density等于2,它等于160像素,即0.5英寸;
在dpi为160的屏幕上,也就是density等于1,它等于80像素,即0.5英寸。
虽然屏幕密度不同,但这张图占用的物理宽度都是0.5英寸,给人视觉上的感觉是一样的。
适配是在适配什么
首先明确一点是,不管用什么单位,Android在最终绘制到屏幕时,都会转化成像素。原本这样就可以了,不同的屏幕我可以显示出同样的物理宽度,但由于手机厂商之多、生产的屏幕之多,所以还有着特殊的情况。
比如,同样是1080x1920的屏幕,一块的dpi是480,density为480/160=3,物理宽度为1080/480=2.25英寸;另一块的dpi是360,density为360/160=2.25,物理宽度为1080/360=3英寸
1080x1920、dpi为480的设计稿上,有个产品列表,介绍图片宽度为360dp,在第一块屏幕上刚好占满屏幕宽度,但是在第二块屏幕上只占用了360x2.25=810px,宽度上剩下了一大块空白,1080-810=270px。
本质上,dip就是为了在大屏幕上显示更多的内容。这里的「大」是什么意思呢?可以看上面的例子,同样的1080像素的宽度,前者的物理宽度是2.25英寸,而后者是3英寸,明显后者比前者更大,所以在显示同样dp的图片时,后者留了一大片空白,因为可以显示更多的内容。
但是这种显示效果就不可接受的,而说了半天的适配,就是适配这样的情况,同样像素宽度的屏幕,物理尺寸却不同,让一站图片在不同的屏幕都显示相同的比例。什么意思呢,就是说,设计稿里这张图占据了全部的宽度,那么在所有的屏幕上都要占据全部的宽度,如果在设计稿中占据了一半的宽度,那么在所有的屏幕上也要占据一半的宽度。
因此,适配就是为了让UI元素在不同设备屏幕上,所占比例是相同的。
可以看出,这样的适配只能满足一个方向,宽或者高。按照宽的比例调整就无法估计高,反之同理。若设备的宽高比与设计稿的宽高比相同,此时可兼顾两者。
现在主要下面这几种适配方案。
方案:修改density
这套方案,是基于dp。
因在绘制时使用的都是px,系统在将dp转化成px时,用的是TypedValue中的方法,最后输出都是px。
1 | public static float applyDimension(int unit, float value, DisplayMetrics metrics) { |
直观的来看,同样是360dp宽的图片,在两个屏幕上所占比例不同的原因是,两个屏幕的宽度不同,一个是1080px/density=360dp,另个是1080px/density=480dp。
这种方案逻辑是,动态修改屏幕的dp宽度,让屏幕的宽度等于设计稿的宽度。同时,屏幕的dp宽度是基于像素宽度和density计算出来的,而屏幕的像素宽度无法修改,所以要修改的就是density,这个值就是存储在DisplayMetrics中的一个变量,从上面的代码中可以看到,系统在将dp转化成px时,使用的是这个变量,所以修改这个变量就可以修改系统最终计算出来的px值。公式为:动态density = 屏幕像素宽度/设计稿的dp宽度
修改的时机是,在系统使用之前,也就是在setContentView
之前。
1 | fun setCustomDensity(activity: Activity, application: Application, designWidthDp: Int) { |
这是字节给出的方案,成本极低,但是没有代码,Github上有人实现了一套,可以参考,点击跳转。
方案:smallest width
这套方案的原理是,手动指定每个像素所代表的dp数量。
举个例子,假设设计稿是1080x1920
在宽度为360dp的屏幕上,将宽度分为1080份,每份代表360/1080=0.33dp,然后生成这样一份文件:
1 | <resources> |
在宽度为380dp的屏幕上,将宽度分为1080份,每份代表380/1080=0.35dp;
在宽度为400dp的屏幕上,将宽度分为1080份,每份代表400/1080=0.37dp;
…
要针对当前的主流宽度屏幕各生成一份这样的文件,放到对应的目录下
1 | values |
在写布局文件时,控件的尺寸就用设计稿上的px尺寸,比如,设计稿上宽度为100px,那么在布局文件里就写@dimen/DIMEN_100PX
,
在360dp的屏幕上,就会读取到33dp,占比33/360=0.09;
在380dp的屏幕上,就会读取到35dp,占比35/380=0.09;
在400dp的屏幕上,就会读取到37dp,占比37/400=0.09;
虽然在精度上有一些损失,但可忽略,实现了适配的需求。这种方案需要精准命中目标设备的宽度,所以需要我们准备足够多的这样的文件。
方案:指定宽高
这套方案的原理是,手动指定每个像素所代表的像素。
这句话看着是不是有些别扭?一个像素不就是一个像素么,怎么还能代表其他像素呢?接着往下看
上面的方案是按照屏幕宽度命中设备,这个方案是按照屏幕的像素宽度命中设备。
还是那个例子,假设设计稿是1080x1920
在720x1280的屏幕上,把宽分成1080份,每份代表720/1080=0.67px;把高分成1920份,每份代表0.67px;
1 | <resources> |
其中的X代表横向,Y代表竖向。
在1080x1920的屏幕上,把宽分成1080份,每份代表1080/1080=1px;把高分成1920份,每份代表1920/1920=1px;
在1440x2560的屏幕上,把宽分成1080份,每份代表1440/1080=1.33px;把高分成1920份,每份代表2560/1920=1.33px;
将这些文件分别放到对应的目录下
1 | values |
在写布局文件时,控件的尺寸就用设计稿上的px尺寸,比如,设计稿上宽度为100px,高度为200px,那么在布局文件里就写@dimen/X100
、@dimen/Y200
,
在1280x720的屏幕上,就会读取到,宽度67px,占比67/720=0.09,高度133px,占比133/1280=0.1;
在1920x1080的屏幕上,就会读取到,宽度100px,占比100/1080=0.09,高度200px,占比200/1920=0.1;
在2560x1440的屏幕上,就会读取到,宽度133px,占比133/1440=0.09,高度266px,占比266/2560=0.1;
同上面一样,会有一些些精度上的损失,但基本上还是满足了适配的需求。这套方案也需要精准命中目标设备屏幕的像素宽高,因为同时指定了宽和高,所以相比上面只指定宽度的方案,需要准备更多的dimen文件,而现在市场的屏幕五花八门,很难保证准备的文件可以囊括所有的情况。
总结
抛开需求谈适配就是在扯淡,所以如果说哪个方案最合适,还是要看具体的需求。相比之下,第一套方案操作简单,且成本低,不用额外添加适配文件,不会进一步增加包体大小。