Android 屏幕适配全攻略

Android 碎片化说法由来已久,多年以来,有那么一群苦逼的 Android 开发,他们饱受 Android 碎片化之苦,面对着各式各样的手机屏幕尺寸和分辨率,还要与"凶残"的产品和 UI 设计师过招,最近这两年这个趋势似乎还愈演愈烈,刘海屏、全面屏,还有即将推出的柔性折叠屏,UI 适配将变得越来越复杂(其实 Android 碎片化并不局限于手机屏幕,它还包括品牌、机型、设备尺寸、分辨率等众多)。

然而 Android 屏幕碎片化并没有像大家想象的那样头疼《对于开发者来说,屏幕碎片化并不算个事儿》。Google 早已经给了我们界面视图布局工具,你可以自定义一种或多种界面视图,以适应不同尺寸的设备。

关于屏幕适配我们需要重点搞明白两个问题,如下图:

屏幕适配

屏幕相关的概念以及解决屏幕适配的相关方案有哪些?本文也主要是围绕这两个核心点展开叙述。

1、屏幕与适配

作为消费者来说,通常会比较关注屏幕的尺寸、分辨率以及厚度这些指标。Android 碎片化并不局限于屏幕,但是屏幕的差异却是碎片化问题的“中心”。屏幕从 3 英寸到 10 英寸,分辨率从 320 到 1920 应有尽有,对我们 UI 适配造成很大困难。

除此之外,材质也是屏幕至关重要的评判因素。目前手机主流的屏幕可分为两大类:一类是 LCD(Liquid Crystal Display),既液晶显示器;另一种是 OLED(Origanic Light - Emitting Diode)既有机发光二极管。

最新的旗舰机例如华为 Mate 20 Pro 和 iPhone XS Max 使用的都是 OLED 屏幕。OLED 屏幕相比 LCD 在屏幕色彩、可弯曲程度、厚度以及耗电都有优势。正因为这些优势,全面屏、曲面屏、以及未来的柔性折叠屏,使用的都是 OLED 材质。

关于 OLED 与 LCD 的具体差别,可以参考《OLED 和 LCD 区别》和《手机屏幕的前世今生,可能比你想象的还精彩》,不过目前 LCD 屏幕相比 OLED 成本更具有优势。

对于碎片化的问题,Android 推荐使用 db 作为尺寸单位来适配 UI,因此每个 Android 开发都应该熟悉 px、dp、dpi、ppi、density 这些概念。

屏幕尺寸单位

通过 dp 加上自适应布局可以基本解决屏幕碎片化的问题,也是 Android 推荐使用的屏幕兼容性适配方案。但是它会存在两个比较大的问题:

1. 不一致性。因为 dpi 与实际 ppi 的差异性,导致在相同分辨率的手机上,控件的实际大小会有所不同。

2. 效率。设计师的设计稿都是以 px 为单位的,开发人员为了 UI 适配,需要手动通过百分比估算出 dp 值。

除了直接 dp 适配之外,目前业界比较常用的 UI 适配方法主要有下面几种:

1. 限制符适配方案。主要有宽高限定符与 smallestWidth 限定符适配方案。

2. 今日头条适配方案。通过反射修改系统的 density 值,具体可以参考《一种极低成本的 Android 屏幕适配方式》《今日头条适配方案

另外,适当舍弃也是对抗 Android 碎片化的重要武器!

2、屏幕适配概论

(1)屏幕尺寸

屏幕尺寸是指屏幕对角线的长度,单位:英寸。1 英寸 = 2.54 cm。目前最主流的 Android 手机屏幕可能都已经是 6 英寸以上。

屏幕尺寸

(2)屏幕分辨率

分辨率是指屏幕横向和纵向像素点数,单位:px,每 px 代表屏幕一个物理像素点。px = density * dp。

屏幕分辨率

(3)像素密度(dpi)

dpi 指的是在系统软件上指定的单位尺寸的像素数量,跟 ppi 不同的是,dpi 可能会被人为的调整。dpi 约等于 ppi。

          像素密度(ppi)

ppi 指的是每英寸所包含的像素数目,这个是屏幕物理参数。ppi 约等于 dpi。

屏幕像素密度

像素密度是清晰度很重要的一个概念,既像素密度越高代表显示屏能够以更高的密度显示图像。当然,显示密度越高,拟真度就越高。画面细节就越丰富。

(4)dp,与密度无关的尺寸和位置

dp(density-independent pixels) 是基于屏幕物理分辨率一个抽象的单位,用于说明与密度无关的尺寸和位置。

dp 与 px 的关系:Android 规定以 160 dpi 为基准,既像素密度为 160dpi 时,此时 1dp = 1px。如果像素密度是 320dpi,此时 1dp = 2 px。

公式:px = dp * (dpi / 160)

前面我们也有提到 dp 加自适应布局基本可以解决屏幕碎片化问题,这也是 Android 官方推荐适配方案,那 dp 是如何实现适配的呢?其实毫不夸张的说:在 Android 中要彻底理解 dp 与 px 之间的关系,才是理解 Android 系统对屏幕适配的原理

下面我们通过一个小例子进行说明 dp 的适配原理。我们分别有两台手机设备,分辨率分别为:480 * 320 和 800 * 480。现在我们使用 px 作为尺寸单位,一个 240px * 160px 的控件(红框部分),如下图(画的有些不太标准):

db 自适应布局

此时该控件在 480 * 320 分辨率的手机屏幕时,控件宽度占屏幕的二分之一,当在 800 * 480 分辨率时,此时控件宽度占屏幕的三分之一了。

我们将单位 px 修改为 dp,既控件为 240dp * 160dp,效果如下图:

db 自适应布局

由于 480 * 320 分辨率的像素密度为 160dpi,而 800 * 480 分辨率的像素密度为 240dpi。

根据上面公式 px = dp * (dpi / 160):

480 * 320 分辨率屏幕 :px = 160 * (160 / 160) = 160px

800 * 480 分辨率屏幕 :px = 160 * (240 / 160) = 240px

此时不论是在 480 * 320 分辨率屏幕还是 800 * 480 分辨率屏幕,该控件的宽度始终占屏幕宽度的一半。

(5)density,密度

密度,屏幕上每平方英寸中含有的像素点数量。density = dpi / 160。

密度目录

(6)sp,Scaled Pixels

(scaled pixels)在 Android 中主要用于设置文字大小,可以根据文字大小首选项进行缩放。效果类似 dp。使用时 Google 推荐以偶数为单位,奇数会影响运算和精度问题。

在 Android 中应该始终保持使用 sp 为文字大小的单位,dp 作为其它元素的尺寸单位。另外还可以考虑矢量图,而不是位图

资源像素密度文件

先来看张图,资源目录对应像素密度范围,相信每个 Android 开发人员都不会对其感到陌生:

密度目录对应 dp 范围

不同名称的 dpi 文件对应不同的 像素密度范围,其实这也是 Android 系统默认屏幕适配的原理,它会根据目标手机的像素密度到对应资源目录下去查找相关资源。

对照 dp 与 px 之间的关系,根据 px = dp * (dpi / 160)

dp 与 px

但是实际开发过程中,我们还要考虑最终打包的大小,一般不会每种密度都提供相关资源,比如我们可以为当前最主流的密度(xxhdpi)提供。但是当我们应用如果跑在其它像素密度屏幕上的时候,Android 系统是如何帮助进行适配的呢?你可以参考《Android drawable微技巧,你所不知道的 drawable 的那些细节》。系统会根据目标像素密度/资源所在的像素密度,得到一个值,然后根据这个值对图片资源宽高做缩放处理,以便达到在当前像素密度下最佳显示效果。

小结,附上屏幕相关概念思维导图:

屏幕相关概念

3、屏幕适配实践

屏幕适配实践

上面说了那么多,仅仅对屏幕适配涉及到的相关概念做了介绍。接下来就根据思维导图展开介绍屏幕适配的实践环节。

(1)wrap_content、match_parent、weight

尽量使用 wrap_content、match_parent 自适应尺寸,这个相信大家都已经用的很熟了,这里着重说下 weight,它只能在 LinerLayout 中使用(需要注意:使用 weight 的 LinerLayout 会有一定性能问题,主要发生在 measure 测量阶段):

下面通过一个简单的例子说明下,weight 在屏幕适配的作用,布局代码:

weight 示例

布局显示如下:

显示效果

我们修改上面的布局代码,调整为 TextView1 占权重 3,TextView2 占权重 1:

weight 示例

此时布局显示如下:

weight 显示效果

按照权重,我们将父布局分成 4 份,TextView1 占 3 份,TextView2 占 1 份,但实际效果显示却不是,这是为什么呢?

实际上 weight 属性的计算规则是:首先按照控件自身的尺寸进行分配,然后将剩尺寸再按照权重进行分配:

父布局宽度:200dp,TextView 1、2 宽度都是 50dp。此时剩余宽度:200 - 50 - 50 = 100dp。

剩余宽度按照 weight 进行分配:100 / 4 = 25dp,对剩余宽度划分为 4 份。

此时 TextView1 的宽度为:50 + 25 * 3 = 125dp。

TextView2 的实际宽度为:50 + 25 * 1 = 75dp。

计算公式:控件本身宽度 + (父布局宽度 - 所有子组件宽度之和) * (当前控件权重/总权重),以当前为例:

TextView2 的实际宽度:50 + (200 - 50 -50) * (1/4)= 75 dp。

(2)RelativeLayout

对于 RelativeLayout 的使用想必大家已经非常熟悉了,它主要基于控件之间的相对位置,在不同的手机屏幕上,控件的相对位置不会发生变化。

不过在 Android Studio 2.3 之后,Android 官方模板默认开始使用 ConstraintLayout 作为默认根布局。它的出现主要是为解决布局嵌套,以更加灵活的方式定位和调整控件位置,你可以参考《约束布局ConstraintLayout看这一篇文章就够了》。

(3)自动拉伸位图 .9.PGN 图片的使用(学名:Nine-Patch)

关于 .9 图的制作主要有两种方式:直接通过设计人员出图 .9图,这种方式我们再过多叙述。另外我们可以通过 Android Studio 完成一张普通图到 .9图。

我们通过 Android Sudio 先把一张图片重命名为 .9图片:

.9 图

然后通过 Android Studio 自带的编辑器预览:

Android studio 制作 .9 图

其实 .9图的制作主要分为两个步骤:

第一步:指定哪一部分可以被拉伸。

第二部,指定哪一部分不可以被拉伸。

当我们将鼠标放在图片上时,在图片的边缘会有一个宽度为 1dp 的像素点,这个像素点就是控制哪些部分是可以被拉伸,其余部分则是不可以被拉伸。感兴趣的朋友可以自己去体会下,但是大部分情况还是由设计人员直接提供(这是个精细活,需要慢慢调整)。

(4)最小宽度限定符(Smallest-Width)

Android 系统在 3.2 之后,我们可以通过最小宽度限定符为不同像素密度的设备进行适配,如:layout 文件 layout-sw480dp,values 文件:values-sw480dp,这里的 sw 代表的就是最小宽度(Smallest-Width,设备上最小的一边)。Android 系统会根据当前设备的像素密度去匹配最接近的资源目录。如下图提供 160dp 至 820dp 的 values 资源目录:

不同密度 values 目录

如下分别提供 7英寸(sw600dp) 与 10英寸(sw720dp) 平板的 layout 资源目录:

适合平板尺寸

最小宽度限定符(Smallest-Width)是目前主流的适配方案之一。

(5)使用布局别名进行兼容适配

使用布局别名方式,更多是为了兼容 Android 3.2 之前的设备。如果不需要这部分内容仅使用最小宽度限定符即可(Smallest-Width)。

布局别名说明

通过布局别名的方式,我们仅需要一个 layout 布局资源目录,此时可以通过不同的 values 资源目录进行适配:

不同密度布局

values-sw320dp 目录下 layout.xml 资源配置:

布局别名资源配置

values-sw480dp 目录下 layout.xml 资源配置:

布局别名资源配置

注意:name 名称需要保持一致,系统会根据目标设备密度自动匹配不同密度下 main 名称对应的布局文件,从而达到在不同像素密度适配不同的布局文件的效果。

(6)屏幕方向限定符(Orientation)

屏幕方向限定符

例如 values资源文件:values-sw600dp-land / values-sw600-port:

方向限定符说明

注意,我们通过 setContentView(R.layout.main),系统会自动根据当前屏幕方向选择 values-sw600dp-land 或 values-sw600dp-port 下分别对应的 main_land.xml 或 main_port.xml 布局文件。从而完成不同屏幕方向适配不同的布局文件目的。

实际开发过程中,可能对于使用屏幕方向限定符的需求场景还是相对较少的。

最后附上屏幕适配解决方案思维导图:

解决方案思维导图

总结

1、基于自适应尺寸的 wrap_content、match_parent,weight 避免固定的尺寸单位。

2、使用基于控件相对位置的 RelativeLayout,现在更加推荐使用 ConstraintLayout,对于布局性能以及扁平化更具有优势。

3、考虑使用自动拉伸的 .9 图或者使用矢量图,矢量图可以保证图片不受拉伸影响而导致失真。

4、限定符,包括:最小宽度限定符(Smallest-Width)、布局别名、屏幕方向限定符。

另外文中也给大家提到,例如今日头条通过修改系统 density 方式完成屏幕适配,不过任何一种方案都有利弊,具体你可以参考这里。它在帮助我们减少开发成本方面确实有很大帮助。很多人也有反馈到 DPI 的存在,就是为了让大屏显示更多的内容,如果大屏和小屏显示内容相同,那用户买大屏手机又有什么意义呢,我觉着大家对 DPI 的理解是对的,但是 Android 碎片化又太严重了,在需求和开发成本面前,还是各种各样,例如百分比的库层出不穷。鱼和熊掌不可兼得,DPI 的意义在 Google 的设计理念是完全正确的,但不是所有公司都能承受这个成本。

以上便是个人在屏幕适配方面探索实践的相关经验,文中如有不妥或其它更好的方案,欢迎您的指出!

如果喜欢我的文章,就请点个赞吧。

推荐阅读

一种非常好用的 Android 屏幕适配

骚年你的屏幕适配方式该升级了!-smallestWidth 限定符适配方案

推荐阅读更多精彩内容