一、前言
对于 Android 开发而言,在开始学习的解读那,就已经被告知,为了达到更好的 的 UI 适配效果,应该使用和像素(px)无关的一些尺寸单位来进行布局,尽量避免使用 px 为单位。
- View 的尺寸和距离,使用 dp 为单位。
- 字体的大小,使用 sp 为单位。
在我们正常进行布局适配之后,看着一切都很正常。但是假如这个时候用户在设置里,手动修改了字体和显示大小,那一切都不一样了。
本文从现象到源码,来详细追踪一下,当你在设置里修改了字体大小和显示大小的时候,你到底在修改什么?它到底影响了什么?
二、修改字体大小
2.1 该用 sp 还是 dp
虽然官方推荐使用 sp 为单位设定字体的尺寸,但是如果你依然坚定的使用 dp 来设置字体的尺寸,常规情况下,你会发现其实它们并没有什么区别。
写个 Demo 来验证一下,布局代码如下:

这里分别用了两个 TextView,并分别使用 dp 和 sp 设定了它的尺寸,最后跟随一个 View,它的尺寸和 TextView 的字体同大,来做一个标记。

可以看到,它们的文字是等大的,并且一个中文的宽度,正好是蓝色的 View 的宽度。
看样子,sp 和 dp 在显示上完全是没有区别的。
而当我们在设置页面,设置了字体的大小之后,这一切就不一样了。

这里,将字体调到最大,再来看看刚才 Demo 的显示效果。

能明显看到,使用 sp 为单位设定的字体,已经被变大了,而使用 dp 为单位设定的字体,依然保持原样的尺寸。
由此,可以简单的得出结论:
使用 sp 为单位标记字体,会随着系统的字体大小而改变。而使用 dp 则不会。
而这种设定你也可以在官方文档中找到对应的描述。

大概的意思,就是说 sp 除了受到 density(屏幕密度) 的影响之外,还受到用户设定的字体大小影响,所以一般推荐使用 sp 来设定字体,让字体的显示效果交给用户设定。
有兴趣可以看看文档:
https://developer.android.com/guide/topics/resources/more-resources.html#Dimension
既然已经有这样的结论了,那么就可以清晰认识到:
推荐使用 sp 来作为字体单位,但是如果有需要字体尺寸不跟随系统字体尺寸变动,则可以使用 dp 来作为字体单位。
2.2 sp到底是怎么被改变的
到这里就已经了解清楚 sp 和 dp 的区别了,但是我们应该不满足于此,接下来再来研究一下,sp 是在何时被改变的吧。
一切的答案都在源码里。
先从设定文字大小的入口,看看 TextView.setTextSize() 方法。

setTextSize() 是有两个重载方法的,如果不设定 TypedValue 的话,它会默认认为你设定的是一个 TypedValue.COMPLEX_UNIT_SP 值,表示以 sp 为单位。
这里最终会使用 TypedValue.applyDimension() 方法,计算出一个值,传递给 setRawTextSize() 方法,在本文中 applyDimension() 方法是如何计算尺寸的。

当 unit 为 COMPLEX_UNIT_SP 的使用,是使用 DisplayMetrics.scaledDensity 为一个比例,参与计算的。
接下来,我们的重点就是找到是什么决定 scaleDensity 的值,看看 scaleDensity 的文档说明。

这里的注释也说明了,scaleDensity 不仅仅受设备的 density 影响,还受用户设定的字体尺寸影响。
DisplayMetrics.scaleDensity 在 DisplayMetrics 类中,并没有初始化的地方,可它是一个 public 的字段,也就是说可以被外部赋值初始化。
真正为 DisplayMetrics 中,各个字段赋值的地方,在 ResourcesImpl 中,有一个 updateConfiguration() 方法,在其中,就有对 scaleDensity 进行初始化的逻辑。

可以看到,这里又引入了一个新的计算因子,fontScale。而从 Configuration 的源码又了解到,fontScale 默认值为 1 ,这也就是为什么通常情况下,density 和 scaleDensity 的值是相等的,它们分别影响了 dp 和 sp 最终渲染出来的像素尺寸。
在 Display.java 的源码中,可以找到修改 fontScale 的过程。

正常情况下,会有三种不同的比例,0.75f、1.25f、1.0f ,而这里的取值范围,完全是厂商决定的,就像在本文开头距离的设备截图中,可以看到有四个选项。
fontScale 被作为了一个系统的设置项,被存储起来,使用 Settings.System 来进行管理,它的 Key 是 Setting.System.FONT_SCALE。
而 FONT_SCALE 最终是由 ActivityManagerService 来负责取出,并且赋值到 Configuration 中的。

2.3 获取用户字体的改变
到这里,修改字体大小相关的所有内容,就形成了闭环,了解清楚 sp 尺寸的来龙去脉。
而在开发过程中,如果想要知道当前环境下,用户是否改变过字体大小,可以直接从 Configuration 中获取即可。

只要不为 1 ,就是表示用户有改变。
三、修改显示大小
在设置中,除了设置『字体大小』之外,还有一个『显示大小』的设置,我们就继续看看,当你修改『显示大小』之后,你到底在修改什么?

3.1 使用 dp 做一个全屏应该怎么写
在 UI 适配的时候,我们一般推荐使用 dp 设置距离和大小,而使用 sp 来设定字体大小。
在使用 dp 为单位来做屏幕适配的时候,影响效果的主要有屏幕像素和 density,当你知道这两个参数之后,你就能很清晰的知道,假如你想写一个全屏的 UI 的时候,除了使用 match_parent 之外,你还可以直接指定一个绝对的 dp 值,进行全屏化。
首先说点题外的概念,你想通过 DisplayMetrics 获取到设备的 density 的时候,你会发现它实际上有两个值:density 和 densityDpi。
例如正常我们所说的 3 倍的手机,density 为 3.0 ,densityDpi 为 480。
它们之间的关系,就是以 160 为基准倍数,进行计算,公式如下:
density * 160 = densityDpi
不过这里对它们的理解,最好不要硬记名称,在很多场景下,说到 density 就是我们这里说的 densityDpi。我们只需要看它们的值就知道它们的关系,毕竟不会有一个 480倍(density)的手机。
拿到一个标准的屏幕尺寸,例如一个标准的 3 倍的手机,它的参数如下:
- screen px:1080x1920
- density:3.0
- densityDpi :480
通过这些参数,我们就可以算出,该设备全屏支持的 dp 值就是 360dp*640dp。
3.2 dp 的全屏效果
了解到一个 3 倍的设备,设置全屏需要写 360dp * 640dp ,接下来我们就开始写个 Demo 来看看效果。

这里只是做一个横向的铺满屏幕的设定,屏幕宽度铺满需要 360dp。我们这里使用一个 TextView 来设定十个字符,每个字符的尺寸为 36dp。同时下面做一个 View 铺满屏幕 360dp。
让我们看看在设备上运行的效果:

而当我们在设置页面中,修改了『显示尺寸』之后,显示的效果就不一样了。
前面设置页面中,『显示尺寸』的设定,可以设置三个值:小、默认、大。
先看看调小之后的效果:

可以看到,将『显示尺寸』调小之后,360dp 已经无法铺满横向的一屏了。
再来看看,当它调整到大的时候,显示的效果吧。

可以看到,调大之后,一行字,虽然同为 36dp 的文字已经写不下一行了,基本上已经超出了屏幕之外。
3.3 屏幕尺寸,到底被改变了什么
既然已经知道了现象,那么我们看看,当我们调整了『屏幕尺寸』之后,我们到底改变了什么。
前面提到,获取到屏幕相关的一些参数,可以使用 DisplayMetrics 这个对象来获取。
示例代码如下:

在『屏幕尺寸』为默认的时候,我们是已经知道它的输出了,接下来运行看看在屏幕尺寸为大的时候,输出的值。

通过输出的 Log,看到其实屏幕的像素尺寸并没有改变,而它的 density 却发生了改变。
这也导致,横向的 dp 尺寸,由原本的 360 变成了 320(1080/3.375),所以才会导致原本适配的很好的 UI ,已经在一屏之中显示不下的原因。
相反,当『屏幕尺寸』设置为 小 的时候,实际上缩小了 density ,从而加大了铺满一屏的 dp 尺寸,导致文字没有刚好显示一屏。
3.4 快速验证结论
使用 DisplayMetrics 的方式,还需要写代码来验证。有一个更简单的方式来验证它,就是使用 wm 命令来验证,它可以支持获取到当前屏幕的 density。
adb shell wm density
从输出的结果也可以看到,它是有一个 Physical density 和 Override density 的,从字面意思,不难看出它们的含义。
wm 命令就是前面提到的一个很典型的 density 和 densityDpi 概念互通的例子,这里说的 density 就是 DisplayMetrics 中的 densityDpi,大家了解一下就可以了。
来看看,当『屏幕尺寸』设置为大的时候,wm 命令输出的结果吧。

3.5 修改显示小结
到这里,就可以得出结论,当你修改设置页面的『屏幕尺寸』的时候,实际上你在修改你设备的 density 值,它也决定了你的 UI 是如何显示在屏幕上的。
四、总结
到现在应该都清楚,你在目标机上,多么完美的适配,最终运行在客户机的时候,具体显示成什么样子,还是依赖当前的环境。
















