oynix

于无声处听惊雷,于无色处见繁花

Android适配

为什么要适配呢?这篇文章来聊一聊。

因为现在有着五花八门的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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static float applyDimension(int unit, float value, DisplayMetrics metrics) {
switch (unit) {
case COMPLEX_UNIT_PX: // pixel,像素
return value;
case COMPLEX_UNIT_DIP: // dp转px
return value * metrics.density;
case COMPLEX_UNIT_SP: // scaled dp 可缩放dp
return value * metrics.scaledDensity;
case COMPLEX_UNIT_PT: // point,1point = 1/72英寸
return value * metrics.xdpi * (1.0f/72);
case COMPLEX_UNIT_IN: // inch 英寸
return value * metrics.xdpi;
case COMPLEX_UNIT_MM: // millimeter 毫米
return value * metrics.xdpi * (1.0f/25.4f);
}
return 0;
}

直观的来看,同样是360dp宽的图片,在两个屏幕上所占比例不同的原因是,两个屏幕的宽度不同,一个是1080px/density=360dp,另个是1080px/density=480dp。

这种方案逻辑是,动态修改屏幕的dp宽度,让屏幕的宽度等于设计稿的宽度。同时,屏幕的dp宽度是基于像素宽度和density计算出来的,而屏幕的像素宽度无法修改,所以要修改的就是density,这个值就是存储在DisplayMetrics中的一个变量,从上面的代码中可以看到,系统在将dp转化成px时,使用的是这个变量,所以修改这个变量就可以修改系统最终计算出来的px值。公式为:动态density = 屏幕像素宽度/设计稿的dp宽度

修改的时机是,在系统使用之前,也就是在setContentView之前。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun setCustomDensity(activity: Activity, application: Application, designWidthDp: Int) {
val appDisplayMetrics = application.resources.displayMetrics
val targetDensity = 1.0f * appDisplayMetrics.widthPixels / designWidthDp
val targetDensityDpi = (targetDensity * 160).toInt()
appDisplayMetrics.density = targetDensity
appDisplayMetrics.densityDpi = targetDensityDpi
val activityDisplayMetrics = activity.resources.displayMetrics
activityDisplayMetrics.density = targetDensity
activityDisplayMetrics.densityDpi = targetDensityDpi
}

override fun onCreate(savedInstanceState: Bundle?) {
setCustomDensity(this, application, 420)
super.onCreate(savedInstanceState)
}

这是字节给出的方案,成本极低,但是没有代码,Github上有人实现了一套,可以参考,点击跳转

方案:smallest width

这套方案的原理是,手动指定每个像素所代表的dp数量。

举个例子,假设设计稿是1080x1920

在宽度为360dp的屏幕上,将宽度分为1080份,每份代表360/1080=0.33dp,然后生成这样一份文件:

1
2
3
4
5
6
7
<resources>
<dimen name="DIMEN_1PX">0.33dp</dimen>
<dimen name="DIMEN_2PX">0.66dp</dimen>
...
<dimen name="DIMEN_1079PX">359.67dp</dimen>
<dimen name="DIMEN_1080PX">360dp</dimen>
</resources>

在宽度为380dp的屏幕上,将宽度分为1080份,每份代表380/1080=0.35dp;
在宽度为400dp的屏幕上,将宽度分为1080份,每份代表400/1080=0.37dp;

要针对当前的主流宽度屏幕各生成一份这样的文件,放到对应的目录下

1
2
3
4
5
values
values-sw360dp
values-sw380dp
values-sw400dp
values-sw420dp

在写布局文件时,控件的尺寸就用设计稿上的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
2
3
4
5
6
7
8
9
10
11
12
13
<resources>
<dimen name="X1">0.67px</dimen>
<dimen name="X2">1.33px</dimen>
...
<dimen name="X1079">719.26px</dimen>
<dimen name="X1080">720px</dimen>

<dimen name="Y1">0.67px</dimen>
<dimen name="Y2">1.33px</dimen>
...
<dimen name="Y1079">1279.33px</dimen>
<dimen name="Y1080">1280px</dimen>
</resources>

其中的X代表横向,Y代表竖向。

在1080x1920的屏幕上,把宽分成1080份,每份代表1080/1080=1px;把高分成1920份,每份代表1920/1920=1px;
在1440x2560的屏幕上,把宽分成1080份,每份代表1440/1080=1.33px;把高分成1920份,每份代表2560/1920=1.33px;

将这些文件分别放到对应的目录下

1
2
3
4
values
values-1280x720
values-1920x1080
values-2560x1440

在写布局文件时,控件的尺寸就用设计稿上的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文件,而现在市场的屏幕五花八门,很难保证准备的文件可以囊括所有的情况。

总结

抛开需求谈适配就是在扯淡,所以如果说哪个方案最合适,还是要看具体的需求。相比之下,第一套方案操作简单,且成本低,不用额外添加适配文件,不会进一步增加包体大小。

------------- (完) -------------
  • 本文作者: oynix
  • 本文链接: https://oynix.com/2021/09/3907d4e375b5/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

欢迎关注我的其它发布渠道