Android 中更为精细的屏幕适配方案

系统 dp 适配存在的问题

做 Android 开发的朋友应该都知道,Android 中屏幕的碎片化问题很严重,为此 Google 提供了 dp 这个单位来适配不同的屏幕。

相同大小的两个屏幕可能具有不同的像素数

从上图我们可以看出来,对于不同屏幕分辨率(像素多少)我们不能使用像素 px 作为单位,而应该使用一种和屏幕密度相关的单位 dp 来表示,最后在绘制的时候转换成 px。

px = dp * (dpi / 160)

DPI : Dots Per Inch,每英寸点数,是一个量度单位,每一英寸长度中,取样、可显示或输出点的数目。例如下图:

通过勾股定理可知: $$ c=\sqrt{a^2 + b^2}$$

而 dpi 的定义公式为(这里的 in 表示屏幕尺寸英寸): $$ dpi=\frac{\sqrt{a^2 + b^2}}{in}=\frac{c}{in}$$

假设我们的屏幕是 1920 * 1080 的屏幕,物理尺寸是 5 英寸,根据上面公式可以得到: $$ dpi=\frac{\sqrt{1080^2 + 1920^2}}{5}=\frac{2203}{5}=440$$

dp 这个单位是基于 160dpi 的屏幕,所以在 160dpi屏幕上 1dp = 1px,而上面我们的屏幕是 440 dpi 可以得知 1dp 实际的是 2.75 像素。我们将 px 和 dp 之间的关系抽取为一个变量:

$$ density = \frac{dpi}{160} $$

$$ px= dp * (\frac{dpi}{160}) = dp * density $$

我们的 UI 设计的设计图是一个固定的宽高,例如设计的是 1080x1920像素,而且是按照屏幕宽度为360dp来设计的,实际上根据上面的公式我们的屏幕对应的应该是 392.7dp:

$$ dp = \frac{px}{\frac{dpi}{160}} = 160 * \frac{px}{dpi} = 160 * \frac{1080}{440} = 392.7 > 360$$

也就是说实际的物理尺寸可能导致我们的实际 dpi 不是 360dpi,而我们的 UI 设计师只能设计一种标准的 360dpi 的设计图,从而导致了可能在其他 dpi 设备上显示效果不一致问题。一般我们设计图都是以固定的尺寸来设计的。比如以分辨率 1920px * 1080px 来设计,以 density 为 3 来标注,也就是屏幕其实是 640dp * 360dp。如果我们想在所有设备上显示完全一致,其实是不现实的,因为屏幕高宽比不是固定的,16:9、4:3 甚至其他宽高比层出不穷,宽高比不同,显示完全一致就不可能了。

DisplayMetrics 适配原理

通过阅读源码,我们可以得知,density 是 DisplayMetrics 中的成员变量,而 DisplayMetrics 实例通过 Resources#getDisplayMetrics 可以获得,而 Resouces 通过 Activity 或者 Application 的 Context 获得。

所以我们只需要计算出实际的 density 替换系统默认的 density 即可。

$$ density = \frac{px}{dpi} = \frac{screenWidth}{360} $$

这个方案就是今日头条的解决方案,详细请参考:https://mp.weixin.qq.com/s/d9QCoBP6kV9VSWvVldVVwA

在开源项目 AndroidUtilCode 中给我们提供了类似的思路的解决方法,不是直接更改 density 而是通过 pt 换算出新的 dpi 设置给 DisplayMetrics, 换算方法如下:

float newXdpi = dm.xdpi = (width * 72f) / designShortSize;

思路其实很简单,就是根据屏幕实际宽度和预设宽度(1080)比例关系更改 pt 的换算系数 72 的实际值。