一、前言

对于 Android 开发而言,在开始学习的解读那,就已经被告知,为了达到更好的 的 UI 适配效果,应该使用和像素(px)无关的一些尺寸单位来进行布局,尽量避免使用 px 为单位。

  • View 的尺寸和距离,使用 dp 为单位。
  • 字体的大小,使用 sp 为单位。

在我们正常进行布局适配之后,看着一切都很正常。但是假如这个时候用户在设置里,手动修改了字体和显示大小,那一切都不一样了。

本文从现象到源码,来详细追踪一下,当你在设置里修改了字体大小和显示大小的时候,你到底在修改什么?它到底影响了什么?

二、修改字体大小

2.1 该用 sp 还是 dp

虽然官方推荐使用 sp 为单位设定字体的尺寸,但是如果你依然坚定的使用 dp 来设置字体的尺寸,常规情况下,你会发现其实它们并没有什么区别。

写个 Demo 来验证一下,布局代码如下:

Java 给word 字体加粗_全屏

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

Java 给word 字体加粗_屏幕尺寸_02

可以看到,它们的文字是等大的,并且一个中文的宽度,正好是蓝色的 View 的宽度。

看样子,sp 和 dp 在显示上完全是没有区别的。

而当我们在设置页面,设置了字体的大小之后,这一切就不一样了。

Java 给word 字体加粗_屏幕尺寸_03

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

Java 给word 字体加粗_屏幕尺寸_04

能明显看到,使用 sp 为单位设定的字体,已经被变大了,而使用 dp 为单位设定的字体,依然保持原样的尺寸。

由此,可以简单的得出结论:

使用 sp 为单位标记字体,会随着系统的字体大小而改变。而使用 dp 则不会。

而这种设定你也可以在官方文档中找到对应的描述。

Java 给word 字体加粗_UI_05

大概的意思,就是说 sp 除了受到 density(屏幕密度) 的影响之外,还受到用户设定的字体大小影响,所以一般推荐使用 sp 来设定字体,让字体的显示效果交给用户设定。

有兴趣可以看看文档:

https://developer.android.com/guide/topics/resources/more-resources.html#Dimension

既然已经有这样的结论了,那么就可以清晰认识到:

推荐使用 sp 来作为字体单位,但是如果有需要字体尺寸不跟随系统字体尺寸变动,则可以使用 dp 来作为字体单位。

2.2 sp到底是怎么被改变的

到这里就已经了解清楚 sp 和 dp 的区别了,但是我们应该不满足于此,接下来再来研究一下,sp 是在何时被改变的吧。

一切的答案都在源码里。

先从设定文字大小的入口,看看 TextView.setTextSize() 方法。

Java 给word 字体加粗_Java 给word 字体加粗_06

setTextSize() 是有两个重载方法的,如果不设定 TypedValue 的话,它会默认认为你设定的是一个 TypedValue.COMPLEX_UNIT_SP 值,表示以 sp 为单位。

这里最终会使用 TypedValue.applyDimension() 方法,计算出一个值,传递给 setRawTextSize() 方法,在本文中 applyDimension() 方法是如何计算尺寸的。

Java 给word 字体加粗_Java 给word 字体加粗_07

当 unit 为 COMPLEX_UNIT_SP 的使用,是使用 DisplayMetrics.scaledDensity 为一个比例,参与计算的。

接下来,我们的重点就是找到是什么决定 scaleDensity 的值,看看 scaleDensity 的文档说明。

Java 给word 字体加粗_屏幕尺寸_08

这里的注释也说明了,scaleDensity 不仅仅受设备的 density 影响,还受用户设定的字体尺寸影响。

DisplayMetrics.scaleDensity 在 DisplayMetrics 类中,并没有初始化的地方,可它是一个 public 的字段,也就是说可以被外部赋值初始化。

真正为 DisplayMetrics 中,各个字段赋值的地方,在 ResourcesImpl 中,有一个 updateConfiguration() 方法,在其中,就有对 scaleDensity 进行初始化的逻辑。

Java 给word 字体加粗_屏幕尺寸_09

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

在 Display.java 的源码中,可以找到修改 fontScale 的过程。

Java 给word 字体加粗_Java 给word 字体加粗_10

正常情况下,会有三种不同的比例,0.75f、1.25f、1.0f ,而这里的取值范围,完全是厂商决定的,就像在本文开头距离的设备截图中,可以看到有四个选项。

fontScale 被作为了一个系统的设置项,被存储起来,使用 Settings.System 来进行管理,它的 Key 是 Setting.System.FONT_SCALE。

而 FONT_SCALE 最终是由 ActivityManagerService 来负责取出,并且赋值到 Configuration 中的。

Java 给word 字体加粗_Java 给word 字体加粗_11

2.3 获取用户字体的改变

到这里,修改字体大小相关的所有内容,就形成了闭环,了解清楚 sp 尺寸的来龙去脉。

而在开发过程中,如果想要知道当前环境下,用户是否改变过字体大小,可以直接从 Configuration 中获取即可。

Java 给word 字体加粗_全屏_12

只要不为 1 ,就是表示用户有改变。

三、修改显示大小

在设置中,除了设置『字体大小』之外,还有一个『显示大小』的设置,我们就继续看看,当你修改『显示大小』之后,你到底在修改什么?

Java 给word 字体加粗_Java 给word 字体加粗_13

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 来看看效果。

Java 给word 字体加粗_全屏_14

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

让我们看看在设备上运行的效果:

Java 给word 字体加粗_全屏_15

而当我们在设置页面中,修改了『显示尺寸』之后,显示的效果就不一样了。

前面设置页面中,『显示尺寸』的设定,可以设置三个值:小、默认、大。

先看看调小之后的效果:

Java 给word 字体加粗_Java 给word 字体加粗_16

可以看到,将『显示尺寸』调小之后,360dp 已经无法铺满横向的一屏了。

再来看看,当它调整到大的时候,显示的效果吧。

Java 给word 字体加粗_Java 给word 字体加粗_17

可以看到,调大之后,一行字,虽然同为 36dp 的文字已经写不下一行了,基本上已经超出了屏幕之外。

3.3 屏幕尺寸,到底被改变了什么

既然已经知道了现象,那么我们看看,当我们调整了『屏幕尺寸』之后,我们到底改变了什么。

前面提到,获取到屏幕相关的一些参数,可以使用 DisplayMetrics 这个对象来获取。

示例代码如下:

Java 给word 字体加粗_UI_18

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

Java 给word 字体加粗_屏幕尺寸_19

通过输出的 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 命令输出的结果吧。

Java 给word 字体加粗_全屏_20

3.5 修改显示小结

到这里,就可以得出结论,当你修改设置页面的『屏幕尺寸』的时候,实际上你在修改你设备的 density 值,它也决定了你的 UI 是如何显示在屏幕上的。

四、总结

到现在应该都清楚,你在目标机上,多么完美的适配,最终运行在客户机的时候,具体显示成什么样子,还是依赖当前的环境。