Android的屏幕适配一直以来都在折磨着我们这些开发者,本篇文章将以Google的官方文档为基础,全面而深入的讲解Android屏幕适配的原因、重要概念、解决方案及最佳实践。
1 Android屏幕适配出现的原因
Android的屏幕尺寸很多,为了让我们开发的程序能够比较美观的显示在不同尺寸、分辨率、像素密度(这些概念我会在下面详细讲解)的设备上,那就要在开发的过程中进行处理,至于如何去进行处理,这就是我们今天的主题了。
2 重要概念
什么是屏幕尺寸、屏幕分辨率、屏幕像素密度?
什么是dp、dip、dpi、sp、px?他们之间的关系是什么?
什么是mdpi、hdpi、xdpi、xxdpi?如何计算和区分?
在下面的内容中我们将介绍这些概念。
2.1 屏幕尺寸
屏幕尺寸指屏幕的对角线的长度,单位是英寸,1英寸=2.54厘米。
比如常见的屏幕尺寸有2.4、2.8、3.5、3.7、4.2、5.0、5.5、6.0等。
2.2 屏幕分辨率
屏幕分辨率是指在横纵向上的像素点数,单位是px,1px=1个像素点。一般以纵向像素*横向像素,如1960*1080。
2.3 屏幕像素密度
屏幕像素密度是指每英寸上的像素点数,单位是dpi,即“dot per inch”的缩写。屏幕像素密度与屏幕尺寸和屏幕分辨率有关,在单一变化条件下,屏幕尺寸越小、分辨率越高,像素密度越大,反之越小。
2.4 dp、dip、dpi、sp、px
px我们应该是比较熟悉的,前面的分辨率就是用的像素为单位,大多数情况下,比如UI设计、Android原生API都会以px作为统一的计量单位,像是获取屏幕宽高等。
dip和dp是一个意思,都是Density Independent Pixels的缩写,即密度无关像素,上面我们说过,dpi是屏幕像素密度,假如一英寸里面有160个像素,这个屏幕的像素密度就是160dpi,那么在这种情况下,dp和px如何换算呢?在Android中,规定以160dpi为基准,1dip=1px,如果密度是320dpi,则1dip=2px,以此类推。
假如同样都是画一条320px的线,在480*800分辨率手机上显示为2/3屏幕宽度,在320*480的手机上则占满了全屏,如果使用dp为单位,在这两种分辨率下,160dp都显示为屏幕一半的长度。这也是为什么在Android开发中,写布局的时候要尽量使用dp而不是px的原因。
而sp,即scale-independent pixels,除了拥有dp的特性,还会随着系统的字体大小改变而改变,建议在设置字体大小的数值要使用sp作为单位,具体可参考这篇文章。
sp与dp之间的转换方法如下:
1 | public static int px2dip(float pxValue) { |
2.5 mdpi、hdpi、xdpi、xxdpi
其实之前还有个ldpi,但是随着移动设备配置的不断升级,这个像素密度的设备已经很罕见了,所在现在适配时不需考虑。
mdpi、hdpi、xdpi、xxdpi用来修饰Android中的drawable文件夹及values文件夹,用来区分不同像素密度下的图片和dimen值。
那么如何区分呢?Google官方指定按照下列标准进行区分:
| 名称 | 像素密度范围 |
|---|---|
| mdpi | 120dpi~160dpi |
| hdpi | 160dpi~240dpi |
| xhdpi | 240dpi~320dpi |
| xxhdpi | 320dpi~480dpi |
| xxxhdpi | 480dpi~640dpi |
| 在进行开发的时候,我们需要把合适大小的图片放在合适的文件夹里面。下面以图标设计为例进行介绍。 | |
| 在设计图标时,对于五种主流的像素密度(MDPI、HDPI、XHDPI、XXHDPI 和 XXXHDPI)应按照 2:3:4:6:8 的比例进行缩放。例如,一个启动图标的尺寸为48x48 dp,这表示在 MDPI 的屏幕上其实际尺寸应为 48x48 px,在 HDPI 的屏幕上其实际大小是 MDPI 的 1.5 倍 (72x72 px),在 XDPI 的屏幕上其实际大小是 MDPI 的 2 倍 (96x96 px),依此类推。 | |
| 虽然 Android 也支持低像素密度 (LDPI) 的屏幕,但无需为此费神,系统会自动将 HDPI 尺寸的图标缩小到 1/2 进行匹配。 | |
| 下图为图标的各个屏幕密度的对应尺寸: |
| 屏幕密度 | 图标尺寸 |
|---|---|
| mdpi | 48x48px |
| hdpi | 72x72px |
| xhdpi | 96x96px |
| xxhdpi | 144x144px |
| xxxhdpi | 192x192px |
3 解决方案
3.1 支持各种屏幕尺寸
3.1.1 使用wrap_content、match_parent、weight
为了确保布局的灵活性并适应各种尺寸的屏幕,应尽量使用 “wrap_content” 和 “match_parent” 控制某些视图组件的宽度和高度。使用 “wrap_content”,系统就会将视图的宽度或高度设置成所需的最小尺寸以适应视图中的内容,而 “match_parent”(在低于 API 级别 8 的级别中称为 “fill_parent”)则会展开组件以匹配其父视图的尺寸。
weight是线性布局的一个独特的属性,我们可以使用这个属性来按照比例对界面进行分配,完成一些特殊的需求。但是,我们对于这个属性的计算应该如何理解呢?
首先,我们将布局设置为线性布局,横向排列,然后放置两个宽度为0dp的按钮,分别设置weight为1和2,我们可以想象到两个按钮按照1:2的宽度比例正常排列了,这也是我们经常使用到的场景。但是假如我们的宽度不是0dp(wrap_content和0dp的效果相同),而是match_parent呢?
在这种情况下,占比和上面正好相反,这是怎么回事呢?说到这里,我们就不得不提一下weight的计算方法了。android:layout_weight的真实含义是:如果View设置了该属性并且有效,那么该 View的宽度等于原有宽度+剩余空间的占比。从这个角度我们来解释一下上面的现象。在上面的代码中,我们设置每个Button的宽度都是match_parent,假设屏幕宽度为L,那么每个Button的宽度也应该都为L,剩余宽度就等于L-(L+L)= -L。
Button1的weight=1,所以最终宽度为L+1/3*(-L)=2/3L,Button2的计算类似,最终宽度为L+2/3(-L)=1/3L。
同时,垂直方向上的结论和水平是完全一样的。虽然说我们演示了match_parent的显示效果,并说明了原因,但是在真正用的时候,我们都是设置某一个属性为0dp,然后按照权重计算所占百分比。
3.1.2 使用相对布局,禁用绝对布局
在开发中,我们大部分时候使用的都是线性布局、相对布局和帧布局,绝对布局由于适配性极差,所以极少使用。
由于各种布局的特点不一样,所以不能说哪个布局好用,到底应该使用什么布局只能根据实际需求来确定。我们可以使用 LinearLayout 的嵌套实例并结合 “wrap_content” 和 “match_parent”,以便构建相当复杂的布局。不过,我们无法通过 LinearLayout 精确控制子视图的特殊关系;系统会将 LinearLayout 中的视图直接并排列出。
如果我们需要将子视图排列出各种效果而不是一条直线,通常更合适的解决方法是使用 RelativeLayout,这样就可以根据各组件之间的特殊关系指定布局了。例如,我们可以将某个子视图对齐到屏幕左侧,同时将另一个视图对齐到屏幕右侧。
3.2 使用限定符
3.2.1 使用尺寸限定符
上面所提到的灵活布局或者是相对布局,可以为我们带来的优势就只有这么多了。虽然这些布局可以拉伸组件内外的空间以适应各种屏幕,但它们不一定能为每种屏幕都提供最佳的用户体验。因此,我们的应用不仅仅只实施灵活布局,还应该应针对各种屏幕配置提供一些备用布局。
如何做到这一点呢?我们可以通过使用配置限定符,在运行时根据当前的设备配置自动选择合适的资源了,例如根据各种屏幕尺寸选择不同的布局。
很多应用会在较大的屏幕上实施“双面板”模式,即在一个面板上显示项目列表,而在另一面板上显示对应内容。平板电脑和电视的屏幕已经大到可以同时容纳这两个面板了,但手机屏幕就需要分别显示。因此,我们可以使用以下文件以便实施这些布局:
res/layout/main.xml,单面板(默认)布局:
1 | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
res/layout-large/main.xml,双面板布局:
1 | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
请注意第二种布局名称目录中的 large 限定符。系统会在属于较大屏幕(例如 7 英寸或更大的平板电脑)的设备上选择此布局。系统会在较小的屏幕上选择其他布局(无限定符)。
3.2.2 使用最小宽度限定符
在版本低于 3.2 的 Android 设备上,开发人员遇到的问题之一是“较大”屏幕的尺寸范围,该问题会影响戴尔 Streak、早期的 Galaxy Tab 以及大部分 7 英寸平板电脑。即使这些设备的屏幕属于“较大”的尺寸,但很多应用可能会针对此类别中的各种设备显示不同的布局(“较大”的概念不清晰导致的)。这就是 Android 3.2 版在引入其他限定符的同时引入“最小宽度”限定符的原因。
最小宽度限定符可让您通过指定某个最小宽度(以 dp 为单位)来定位屏幕。例如,标准 7 英寸平板电脑的最小宽度为 600 dp,因此如果您要在此类屏幕上的用户界面中使用双面板(但在较小的屏幕上只显示列表),您可以使用上文中所述的单面板和双面板这两种布局,但您应使用 sw600dp 指明双面板布局仅适用于最小宽度为 600 dp 的屏幕,而不是使用 large 尺寸限定符。
res/layout/main.xml,单面板(默认)布局:
1 | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
res/layout-sw600dp/main.xml,双面板布局:
1 | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
也就是说,对于最小宽度大于等于 600 dp 的设备,系统会选择 layout-sw600dp/main.xml(双面板)布局,否则系统就会选择 layout/main.xml(单面板)布局。
但 Android 版本低于 3.2 的设备不支持此技术,原因是这些设备无法将 sw600dp 识别为尺寸限定符,因此我们仍需使用 large 限定符。这样一来,就会有一个名称为 res/layout-large/main.xml 的文件(与 res/layout-sw600dp/main.xml 一样)。但是没有太大关系,我们将马上学习如何避免此类布局文件出现的重复。
3.2.3 使用布局别名
最小宽度限定符仅适用于 Android 3.2 及更高版本。因此,如果我们仍需使用与较低版本兼容的概括尺寸范围(小、正常、大和特大)。例如,如果要将用户界面设计成在手机上显示单面板,但在 7 英寸平板电脑、电视和其他较大的设备上显示多面板,那么我们就需要提供以下文件:
- res/layout/main.xml: 单面板布局
- res/layout-large: 多面板布局
- res/layout-sw600dp: 多面板布局
后两个文件是相同的,因为其中一个用于和 Android 3.2 设备匹配,而另一个则是为使用较低版本 Android 的平板电脑和电视准备的。
要避免平板电脑和电视的文件出现重复(以及由此带来的维护问题),您可以使用别名文件。例如,您可以定义以下布局:
- res/layout/main.xml,单面板布局
- res/layout/main_twopanes.xml,双面板布局
然后添加这两个文件:
res/values-large/layout.xml:
1 | <resources> |
res/values-sw600dp/layout.xml:
1 | <resources> |
后两个文件的内容相同,但它们并未实际定义布局。它们只是将 main 设置成了 main_twopanes 的别名。由于这些文件包含 large 和 sw600dp 选择器,因此无论 Android 版本如何,系统都会将这些文件应用到平板电脑和电视上(版本低于 3.2 的平板电脑和电视会匹配 large,版本高于 3.2 的平板电脑和电视则会匹配 sw600dp)。
3.2.4 使用屏幕方向限定符
某些布局会同时支持横向模式和纵向模式,但我们可以通过调整优化其中大部分布局的效果。在新闻阅读器示例应用中,每种屏幕尺寸和屏幕方向下的布局行为方式如下所示:
- 小屏幕,纵向:单面板,带徽标
- 小屏幕,横向:单面板,带徽标
- 7 英寸平板电脑,纵向:单面板,带操作栏
- 7 英寸平板电脑,横向:双面板,宽,带操作栏
- 10 英寸平板电脑,纵向:双面板,窄,带操作栏
- 10 英寸平板电脑,横向:双面板,宽,带操作栏
- 电视,横向:双面板,宽,带操作栏
因此,这些布局中的每一种都定义在了 res/layout/ 目录下的某个 XML 文件中。为了继续将每个布局分配给各种屏幕配置,该应用会使用布局别名将两者相匹配:
res/layout/onepane.xml:(单面板)
1 | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
res/layout/onepane_with_bar.xml:(单面板带操作栏)
1 | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
res/layout/twopanes.xml:(双面板,宽布局)
1 | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
res/layout/twopanes_narrow.xml:(双面板,窄布局)
1 | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
既然我们已定义了所有可能的布局,那就只需使用配置限定符将正确的布局映射到各种配置即可。
现在只需使用布局别名技术即可做到这一点:
res/values/layouts.xml:
1 | <resources> |
res/values-sw600dp-land/layouts.xml:
1 | <resources> |
res/values-sw600dp-port/layouts.xml:
1 | <resources> |
res/values-large-land/layouts.xml:
1 | <resources> |
res/values-large-port/layouts.xml:
1 | <resources> |
3.3 使用自动拉伸位图(.9图)
在Android的设计过程中,为了适配不同的手机分辨率,图片大多需要拉伸或者压缩,这样就出现了可以任意调整大小的一种图片格式“.9.png”。详细教程请参考这篇文章。
3.4 支持各种屏幕密度
3.4.1 使用密度无关像素
由于各种屏幕的像素密度都有所不同,因此相同数量的像素在不同设备上的实际大小也有所差异,这样使用像素定义布局尺寸就会产生问题。因此,请务必使用 dp 或 sp 单位指定尺寸。
例如,请使用 dp(而非 px)指定两个视图间的间距:
1 | <Button android:layout_width="wrap_content" |
请务必使用 sp 指定文字大小:
1 | <TextView |
除了介绍这些最基础的知识之外,我们下面再来讨论一下另外一个问题。
经过上面的介绍,我们都清楚,为了能够规避不同像素密度的陷阱,Google推荐使用dp来代替px作为控件长度的度量单位,但是我们来看下面的一个场景。
假如我们以Nexus5作为书写代码时查看效果的测试机型,Nexus5的总宽度为360dp,我们现在需要在水平方向上放置两个按钮,一个是150dp左对齐,另外一个是200dp右对齐,最后中间留有10dp间隔。
但是如果在Nexus S或者是Nexus One运行,两个按钮就发生了重叠。我们都已经用了dp了,为什么会出现这种情况呢?
虽然说dp可以去除不同像素密度的问题,使得1dp在不同像素密度上面的物理长度基本相同(如果xdpi,ydpi与densityDpi不一样,则物理长度会有偏差),但是还是由于Android屏幕设备的多样性,如果使用dp来作为度量单位,并不是所有的屏幕的宽度都是相同的dp长度,比如说,Nexus S和Nexus One属于hdpi,屏幕宽度是320dp,而Nexus 5属于xxhdpi,屏幕宽度是360dp,Galaxy Nexus属于xhdpi,屏幕宽度是384dp,Nexus 6 属于xxxhdpi,屏幕宽度是410dp。所以说,光Google自己一家的产品就已经有这么多的标准,而且屏幕宽度和像素密度没有任何关联关系,即使我们使用dp,在320dp宽度的设备和410dp的设备上,还是会有90dp的差别。
所以总结的结果是,我们要尽量使用match_parent和wrap_content,尽可能少的用dp来指定控件的具体长宽,再结合上权重,大部分的情况我们都是可以做到适配的。如果遇到上述情况的话,这里有一种方法就是,我们假设手机屏幕的宽度都是320某单位,那么我们将一个屏幕宽度的总像素数平均分成320份,每一份对应具体的像素就可以了。具体介绍可以参考这篇文章。
3.4.2 提供备用位图
由于 Android 可在具有各种屏幕密度的设备上运行,因此我们提供的位图资源应始终可以满足各类普遍密度范围的要求:低密度、中等密度、高密度以及超高密度。这将有助于我们的图片在所有屏幕密度上都能得到出色的质量和效果。
要生成这些图片,我们应先提取矢量格式的原始资源,然后根据以下尺寸范围针对各密度生成相应的图片。
- xhdpi:2.0
- hdpi:1.5
- mdpi:1.0(最低要求)
- ldpi:0.75
也就是说,如果我们为 xhdpi 设备生成了 200x200 px尺寸的图片,就应该使用同一资源为 hdpi、mdpi 和 ldpi 设备分别生成 150x150、100x100 和 75x75 尺寸的图片。然后,将生成的图片文件放在 res/ 下的相应子目录中(mdpi、hdpi、xhdpi、xxhdpi),系统就会根据运行您应用的设备的屏幕密度自动选择合适的图片。
注意:
实际开发的过程中,通常都会做三套配图,对应着drawable-hdpi、drawable-xhdpi、drawable-xxhdpi三个文件夹。当然时间赶的话,做一套也是可以的。具体可以参考王月半子的文章和慕课网的这篇文章。对于慕课网的这篇,其中对资源的加载策略做了详细的讲解,值得一看,同时,慕课网一文所提及的资源加载策略同样适用于values文件夹。对于王月半子的文章中有提及过mipmap和drawable的区别,我在此基础上的总结是APP的桌面图标坚决用mipmap-[]dpi,其它的资源除所提及的必须放在drawable文件夹中的外,google并没有给出明确的指示,所以放mipmap-[]dpi中和drawable-[*]dpi都可以。最后还需要注意的一点是,如果有的手机尺寸比较奇葩,可以试试wdp限定符,还是不行的话,就建议动态设置了。
这样一来,只要我们引用 @drawable/id,系统都能根据相应屏幕的 dpi 选取合适的位图。还记得我们上面提到的图标设计尺寸吗?和这个其实是一个意思。但是还有个问题需要注意下,如果是.9图或者是不需要多个分辨率的图片,就放在drawable文件夹即可。
drawable对应的dpi是160dpi,相当于drawable-mdpi,但是不会作为一个特定dpi的文件夹使用。它的作用是存放一些xml文件、.9图以及一些不需要适配多个分辨率的图片。
3.5 Android 8.0 图标适配
4 最佳实践
4.1 关于高清设计图尺寸
Google官方给出的高清设计图尺寸有两种方案,一种是以mdpi设计,然后对应放大得到更高分辨率的图片,另外一种则是以高分辨率作为设计大小,然后按照倍数对应缩小到小分辨率的图片。
根据经验,我更推荐第二种方法,因为小分辨率在生成高分辨率图片的时候,会出现像素丢失,我不知道是不是有方法可以阻止这种情况发生。
而分辨率可以以1280*720或者是1960*1080作为主要分辨率进行设计。这里有一篇关于UI设计的规范说明,UI设计时可以参考。
4.2 ImageView的ScaleType属性
设置不同的ScaleType会得到不同的显示效果,一般情况下,设置为centerCrop能获得较好的适配效果。
4.3 动态设置
有一些情况下,我们需要动态的设置控件大小或者是位置,比如说popwindow的显示位置和偏移量等,这个时候我们可以动态的获取当前的屏幕属性,然后设置合适的数值。
1 | public class ScreenSizeUtil { |