Android 系统发布十多年以来,关于 Android 的 UI 的适配一直是开发环节中最重要的问题,但是我看到还是有很多小伙伴对 Android 适配方案不了解。刚好,近期准备对糗事百科 Android 客户端设计一套 UI 尺寸适配方案,可以和小伙伴们详细的聊一聊这个问题。

Android 适配最核心的问题有两个,其一,就是适配的效率,即把设计图转化为 App 界面的过程是否高效,其二如何保证实现 UI 界面在不同尺寸和分辨率的手机中 UI 的一致性。这两个问题都很重要,一个是保证我们开发的高效,一个是保证我们适配的成效;今天我们就这两个核心的问题来聊一聊 Android 的适配方案。

img

首先,大家都知道,在标识尺寸的时候,Android 并不推荐我们使用 px 这个真实像素单位,因为不同的手机之间,分辨率是不同的,比如一个 96*96 像素的控件在分辨率越来越高的手机上会在整体 UI 中看起来越来越小。

img

出现类似于上图这样这样,整体的布局效果可能会变形,所以 px 这个单位在布局文件中是不推荐的。

# dp 直接适配

针对这种情况,Android 推荐使用 dp 作为尺寸单位来适配 UI.

那么什么是 dp?dp 指的是设备独立像素,以 dp 为尺寸单位的控件,在不同分辨率和尺寸的手机上代表了不同的真实像素,比如在分辨率较低的手机中,可能 1dp=1px, 而在分辨率较高的手机中,可能 1dp=2px,这样的话,一个 96*96dp 的控件,在不同的手机中就能表现出差不多的大小了。那么这个 dp 是如何计算的呢? 我们都知道一个公式: px = dp (dpi/160) 系统都是通过这个来判断 px 和 dp 的数学关系,

那么这里又出现了一个问题,dpi 是什么呢?

dpi 是像素密度,指的是在系统软件上指定的单位尺寸的像素数量,它往往是写在系统出厂配置文件的一个固定值。

我为什么要强调它是软件系统上的概念?因为大家买手机的时候,往往会听到另一个叫 ppi 的参数,这个在手机屏幕中指的也是像素密度,但是这个是物理上的概念,它是客观存在的不会改变。dpi 是软件参考了物理像素密度后,人为指定的一个值,这样保证了某一个区间内的物理像素密度在软件上都使用同一个值。这样会有利于我们的 UI 适配。

比如,几部相同分辨率不同尺寸的手机的 ppi 可能分别是是 430,440,450, 那么在 Android 系统中,可能 dpi 会全部指定为 480. 这样的话,dpi/160 就会是一个相对固定的数值,这样就能保证相同分辨率下不同尺寸的手机表现一致。

而在不同分辨率下,dpi 将会不同,比如:

1080*720 1920*1080
dpi 320 480
dpi/160 2 3

根据上面的表格,我们可以发现,720P, 和 1080P 的手机,dpi 是不同的,这也就意味着,不同的分辨率中,1dp 对应不同数量的 px (720P 中,1dp=2px,1080P 中 1dp=3px),这就实现了,当我们使用 dp 来定义一个控件大小的时候,他在不同的手机里表现出相应大小的像素值。

img

我们可以说,通过 dp 加上自适应布局和 weight 比例布局可以基本解决不同手机上适配的问题,这基本是最原始的 Android 适配方案。

这种方式存在两个小问题,第一,这只能保证我们写出来的界面适配绝大部分手机,部分手机仍然需要单独适配,为什么 dp 只解决了 90% 的适配问题,因为并不是所有的 1080P 的手机 dpi 都是 480,比如 Google 的 Pixel2(19201080)的 dpi 是 420,也就是说,在 Pixel2 中,1dp=2.625px, 这样会导致相同分辨率的手机中,这样,一个 100dp100dp 的控件,在一般的 1080P 手机上,可能都是 300px, 而 Pixel 2 中 ,就只有 262.5px, 这样控件的实际大小会有所不同。

为了更形象的展示,假设我们在布局文件中把一个 ImageView 的宽度设置为 360dp, 那么在下面两张图中表现是不一样的:

图一是 1080P,480dpi 的手机,图二是 1080P,420dpi 的手机

1080P,480dpi的手机

1080P,420dpi的手机

从上面的布局中可以看到,同样是 1080P 的手机,差异是比较明显的。在这种情况下,我们的 UI 可能需要做一些微调甚至单独适配。

第二个问题,这种方式无法快速高效的把设计师的设计稿实现到布局代码中,通过 dp 直接适配,我们只能让 UI 基本适配不同的手机,但是在设计图和 UI 代码之间的鸿沟,dp 是无法解决的,因为 dp 不是真实像素。而且,设计稿的宽高往往和 Android 的手机真实宽高差别极大,以我们的设计稿为例,设计稿的宽高是 375px750px,而真实手机可能普遍是 10801920,

那么在日常开发中我们是怎么跨过这个鸿沟的呢?基本都是通过百分比啊,或者通过估算,或者设定一个规范值等等。总之,当我们拿到设计稿的时候,设计稿的 ImageView 是 128px128px,当我们在编写 layout 文件的时候,却不能直接写成 128dp128dp。在把设计稿向 UI 代码转换的过程中,我们需要耗费相当的精力去转换尺寸,这会极大的降低我们的生产力,拉低开发效率。

# 宽高限定符适配

为了高效的实现 UI 开发,出现了新的适配方案,我把它称作宽高限定符适配。简单说,就是穷举市面上所有的 Android 手机的宽高像素值:

img

设定一个基准的分辨率,其他分辨率都根据这个基准分辨率来计算,在不同的尺寸文件夹内部,根据该尺寸编写对应的 dimens 文件。

比如以 480x320 为基准分辨率

  • 宽度为 320,将任何分辨率的宽度整分为 320 份,取值为 x1-x320
  • 高度为 480,将任何分辨率的高度整分为 480 份,取值为 y1-y480

那么对于 800*480 的分辨率的 dimens 文件来说,

x1=(480/320)*1=1.5px

x2=(480/320)*2=3px

img

这个时候,如果我们的 UI 设计界面使用的就是基准分辨率,那么我们就可以按照设计稿上的尺寸填写相对应的 dimens 引用了,而当 APP 运行在不同分辨率的手机中时,这些系统会根据这些 dimens 引用去该分辨率的文件夹下面寻找对应的值。这样基本解决了我们的适配问题,而且极大的提升了我们 UI 开发的效率,

但是这个方案有一个致命的缺陷,那就是需要精准命中才能适配,比如 1920x1080 的手机就一定要找到 1920x1080 的限定符,否则就只能用统一的默认的 dimens 文件了。而使用默认的尺寸的话,UI 就很可能变形,简单说,就是容错机制很差。

不过这个方案有一些团队用过,我们可以认为它是一个比较成熟有效的方案了。

# UI 适配框架(已经停止维护)

鸿洋大佬的适配方案的项目也来自于宽高限定符方案的启发。

使用方法也很简单:

第一步: 在你的项目的 AndroidManifest 中注明你的设计稿的尺寸。

1
2
3
4
<meta-data android:name="design_width" android:value="768">
</meta-data>
<meta-data android:name="design_height" android:value="1280">
</meta-data>

第二步: 让你的 Activity 继承自 AutoLayoutActivity.

然后我们就可以直接在布局文件里面使用具体的像素值了,比如,设计稿上是 96*96, 那么我们可以直接写 96px,APP 运行时,框架会帮助我们根据不同手机的具体尺寸按比例伸缩。

这可以说是一个极好的方案,因为它在宽高限定符适配的基础上更进一步,并且解决了容错机制的问题,可以说完美的达成了开发高效和适配精准的两个要求。

但是我们能够想到,因为框架要在运行时会在 onMeasure 里面做变换,我们自定义的控件可能会被影响或限制,可能有些特定的控件,需要单独适配,这里面可能存在的暗坑是不可预见的,还有一个比较重要的问题,那就是整个适配工作是有框架完成的,而不是系统完成的,一旦使用这个框架,未来一旦遇到很难解决的问题,替换起来是非常麻烦的,而且项目一旦停止维护,后续的升级就只能靠你自己了,这种代价团队能否承受?当然,它已经停止维护了。

不过仅仅就技术方案而言,不可否认,这是一个很好的开源项目。

# 小结

讨论的上述几种适配方案都是可以实际用于开发中的比较成熟的方案,而且确实有很多开发者正在使用。不过由于他们各自都存在一些缺陷,所以我们使用了上述方案后还需要花费额外的精力着手解决这些可能存在的缺陷。

那么,是否存在一种相对比较完美,没有明显的缺陷的方案呢?

# smallestWidth 适配

smallestWidth 适配,或者叫 sw 限定符适配。指的是 Android 会识别屏幕可用高度和宽度的最小尺寸的 dp 值(其实就是手机的宽度值),然后根据识别到的结果去资源文件中寻找对应限定符的文件夹下的资源文件。

这种机制和上文提到的宽高限定符适配原理上是一样的,都是系统通过特定的规则来选择对应的文件。

举个例子,小米 5 的 dpi 是 480, 横向像素是 1080px,根据 px=dp (dpi/160),横向的 dp 值是 1080/(480/160), 也就是 360dp, 系统就会去寻找是否存在 value-sw360dp 的文件夹以及对应的资源文件。

img

smallestWidth 限定符适配和宽高限定符适配最大的区别在于,前者有很好的容错机制,如果没有 value-sw360dp 文件夹,系统会向下寻找,比如离 360dp 最近的只有 value-sw350dp,那么 Android 就会选择 value-sw350dp 文件夹下面的资源文件。这个特性就完美的解决了上文提到的宽高限定符的容错问题。

这套方案是上述几种方案中最接近完美的方案。 首先,从开发效率上,它不逊色于上述任意一种方案。根据固定的放缩比例,我们基本可以按照 UI 设计的尺寸不假思索的填写对应的 dimens 引用。 我们还有以 375 个像素宽度的设计稿为例,在 values-sw360dp 文件夹下的 diemns 文件应该怎么编写呢?这个文件夹下,意味着手机的最小宽度的 dp 值是 360,我们把 360dp 等分成 375 等份,每一个设计稿中的像素,大概代表 smallestWidth 值为 360dp 的手机中的 0.96dp,那么接下来的事情就很简单了,假如设计稿上出现了一个 10px*10px 的 ImageView, 那么,我们就可以不假思索的在 layout 文件中写下对应的尺寸。

img

而这种 diemns 引用,在不同的 values-swdp 文件夹下的数值是不同的,比如 values-sw360dp 和 values-sw400dp,

img

img

当系统识别到手机的 smallestWidth 值时,就会自动去寻找和目标数据最近的资源文件的尺寸。

其次,从稳定性上,它也优于上述方案。原生的 dp 适配可能会碰到 Pixel 2 这种有些特别的手机需要单独适配,但是在 smallestWidth 适配中,通过计算 Pixel 2 手机的的 smallestWidth 的值是 411,我们只需要生成一个 values-sw411dp (或者取整生成 values-sw410dp 也没问题) 就能解决问题。

smallestWidth 的适配机制由系统保证,我们只需要针对这套规则生成对应的资源文件即可,不会出现什么难以解决的问题,也根本不会影响我们的业务逻辑代码,而且只要我们生成的资源文件分布合理,,即使对应的 smallestWidth 值没有找到完全对应的资源文件,它也能向下兼容,寻找最接近的资源文件。

当然,smallestWidth 适配方案有一个小问题,那就是它是在 Android 3.2 以后引入的,Google 的本意是用它来适配平板的布局文件(但是实际上显然用于 diemns 适配的效果更好),不过目前所有的项目应该最低支持版本应该都是 4.0 了(糗事百科这么老的项目最低都是 4.0 哦),所以,这问题其实也不重要了。

评论中还说到了一个缺陷我忘了提,那就是多个 dimens 文件可能导致 apk 变大,这是事实,根据生成的 dimens 文件的覆盖范围和尺寸范围,apk 可能会增大 300kb-800kb 左右,目前糗百的 dimens 文件大小是 406kb,我认为这是可以接受的。

# 今日头条适配方案(更新)

文章链接,之前确实没有接触过,我简单看了一遍,可以说,这也是相对比较完美的方案,我先简单说一下这个方案的思路,它是通过修改 density 值,强行把所有不同尺寸分辨率的手机的宽度 dp 值改成一个统一的值,这样就解决了所有的适配问题。

比如,设计稿宽度是 360px,那么开发这边就会把目标 dp 值设为 360dp,在不同的设备中,动态修改 density 值,从而保证 (手机像素宽度) px/density 这个值始终是 360dp, 这样的话,就能保证 UI 在不同的设备上表现一致了。

这个方案侵入性很低,而且也没有涉及私有 API,应该也是极不错的方案,我暂时也想不到强行修改 density 是否会有其他影响,既然有今日头条的大厂在用,稳定性应当是有保证的。

但是根据我的观察,这套方案对老项目是不太友好的,因为修改了系统的 density 值之后,整个布局的实际尺寸都会发生改变,如果想要在老项目文件中使用,恐怕整个布局文件中的尺寸都可能要重新按照设计稿修改一遍才行。因此,如果你是在维护或者改造老项目,使用这套方案就要三思了。

# 福利赠送

生成 diemns 文件的过程以及数据计算方法上面已经讲清楚了,大家完全可以自己去生成这些文件,我在这里附赠生成 values-sw 的项目代码,大家直接拿去用,是 Java 工程。点击这里获取项目地址

# 关于一些问题

Q: 该适配方案怎么用?

A: 点击进入上文的 github 项目,下载到本地,然后运行该 Java 工程,会在本地根目录下生成相应的文件,如果需要生成更多尺寸,在 DimenTypes 文件中填写你需要的尺寸即可。

Q: 是否有推荐的尺寸?

A 300,320,360,390,411,450,这几个尺寸是比较必要的,然后在其中插入一些其他的尺寸即可,如果不放心,可以在 300-450 之间,以 10 为步长生成十几个文件就 OK 了。

Q: 平板适配的问题?

A: 这个可以分成两个问题,第一,团队有没有专门针对平板设计 UI? 第二,才是如何对平板适配。如果团队内部没有针对平板设计 UI, 那么大家对于 App 在平板上运行的要求大抵也就是不要太难看即可。针对这种情况的适配方法是被动适配,即不要生成 480 以上的适配文件,这样在平板上,系统就会使用 480 这个尺寸的 dimens 文件,这样效果比主动适配更好;而如果团队主动设计了平板的 UI,那么我们就需要主动生成平板的适配文件,大概在 600-800 之间,关键尺寸是 640,768。然后按照 UI 设计的图来写即可。

Q:用了这套方案是否就不需要使用 wrap_content 等来布局了?

A: 这是绝对错误的做法!如果 UI 设计上明显更适合使用 wrap_content,match_parent,layout_weight 等,我们就要毫不犹豫的使用,而且在高这个维度上,我们要依照情况设计为可滑动的方式,或者 match_parent, 尽量不要写死。总之,所有的适配方案都不是用来取代 match_parent,wrap_content 的,而是用来完善他们的