提高应用兼容性之屏幕适配
背景
做 Android 开发的都知道,Android 设备的碎片化太严重了,由此产生的屏幕兼容性问题令人秃头。不过兵来将挡水来土掩,本文就来撸一撸屏幕适配方法,具体说来有:
- 使用 ConstraintLayout
- 避免使用硬编码的布局尺寸
- 创建备用布局
- 使用最小宽度限定符
- 使用可用宽度限定符
- 添加屏幕方向限定符
- 使用 Fragment 将页面组件模块化
- 创建可拉伸的九宫格位图
- 使用密度无关像素(dp)设计页面
- 使用预缩放的配置值
- 将图片资源放置在合适的目录下
- 将应用图标放在 mipmap 目录中
使用 ConstraintLayout
使用 ConstraintLayout,可以根据布局中 View 之间的空间关系来指定每个 View 的位置和大小,当屏幕尺寸改变时,所有 View 都可以一起移动和拉伸,还可以减少布局层级,何乐而不为?不过不建议在 RecyclerView 的 item 中使用,会有性能问题。
避免使用硬编码的布局尺寸
为了确保布局能够灵活地适应不同的屏幕尺寸,尽量使用 wrap_content 和 match_parent,而不是硬编码的尺寸。如果使用 LinearLayout,则可以设置 layout_weight,以便每个 View 按照自身权重值所占的比例填充剩余的空间。
创建备用布局
虽然布局可以通过拉伸或者相对位置来适配不同的屏幕尺寸,但体验并不是很好,比如本来在手机状态表现良好的布局,在平板上惨不忍睹。因此,应该提供备用布局资源,以针对特定屏幕尺寸优化设计。具体做法可以通过创建额外的 res/layout/ 目录来提供特定于屏幕的布局(针对需要不同布局的每种屏幕配置创建一个),然后将屏幕将屏幕配置限定符附加到 layout 目录名称(例如,对于可用宽度 600dp 的屏幕,附加限定符 layout-w600dp)。
使用最小宽度限定符(不考虑屏幕方向)
最小宽度屏幕尺寸限定符允许为具有最小宽度(dp为单位)的屏幕提供备用布局。
res/layout/main_activity.xml #小于 600dp 的设备
res/layout-sw600dp/main_activity.xml #大于大于 600dp 的设备
最小宽度限定符指定屏幕两侧的最小尺寸,而不考虑当前的屏幕方向,因此这是一种指定布局可用的整体屏幕尺寸的简单方法。以下是最小宽度值与典型屏幕尺寸的对应关系:
- 320 dp:典型手机屏幕(240x320 ldpi、320x480 mdpi、480x800 hdpi 等)。
- 480dp:约为 5 英寸的大手机屏幕 (480x800 mdpi)。
- 600dp:7 英寸平板电脑 (600x1024 mdpi)。
- 720dp:10 英寸平板电脑(720x1280 mdpi、800x1280 mdpi 等)
使用可用宽度限定符(考虑屏幕方向)
有时候需要根据当前可用的宽度或高度来更改布局,而不是根据屏幕的最小宽度来更改布局,因为屏幕宽度可能会根据设备的屏幕方向是横向还是纵向而发生变化。在这种情况下,应该使用可用宽度限定符来执行同样的操作。
res/layout/main_activity.xml # 当前宽度小于 600dp
res/layout-w600dp/main_activity.xml # 当前宽度大于等于 600dp
当然如果关注的是当前可用高度,则使用可用高度限定符执行同样的操作,例如:对于当前屏幕高度至少为 600 dp 的屏幕,使用限定符 layout-h600dp。
添加屏幕方向限定符
有时候比较关心的是页面在竖屏和横屏之间的表现,可以将 port 或 land 限定符添加到资源目录名称。只是需要确保这些限定符在其他尺寸限定符后面:
res/layout/main_activity.xml # 手机
res/layout-land/main_activity.xml # 手机横屏
res/layout-sw600dp/main_activity.xml # 平板
res/layout-sw600dp-land/main_activity.xml # 平板横屏
使用 Fragment 将页面组件模块化
典型例子就是新闻应用了,在平板电脑上可以在左侧使用一个 Fragment 显示报道列表,而在右侧使用另一个 Fragment 显示一篇完整的报道,它们都添加到一个容器中一般是 Activity。而在手机上,这两个 Fragment 显示在独立的容器中。
创建可拉伸的九宫格位图
如果在改变尺寸的 View 中使用 Bitmap 作为背景,系统会根据设备不同而缩放图片,导致模糊或者图像失真。这时就可以使用 .9.png 啦,即点九图。
使用密度无关像素(dp)设计页面
Android 设备不仅有不同的屏幕尺寸,而且其屏幕也有不同的像素尺寸。因此应该避免直接用像素(px)来定义距离和尺寸,而应该使用 dp。为了更好的理解,下面对一些基本概念进行说明:
-
px 即pixel,就是我们平常所说的像素。比如 1080 X 1920 指的就是宽为 1080 个像素,长(高)1920 个像素。由于像素是最小的独立显示单位,px 均为整数,不会出现有小数如 0.5px 的情况。
-
in即 inch ,英寸的意思。1 英寸约等于 2.54 厘米。我们平常所说的手机多少寸指的就是这个单位,但是这些尺寸指的是屏幕的对角线长度。
-
dpi 即 Dot Per Inch,每英寸像素点,注意和下文的dp(dip)区别开来。比如 320 X 480 分辨率的手机,宽 2 英寸,高 3 英寸,这时横向的 dpi 就是 320 / 2 = 160,相应的纵向的 dpi 就是 480 / 3 = 160,160 就是这部手机的 dpi,横向和纵向的这个值都是相同的,原因是大部分手机屏幕使用正方形的像素点。
-
dp(dip)即 device indepentdent pixels ,设备独立像素,Android 特有的单位,在 dpi = 160 屏幕上,1dp = 1px。
-
sp 和 dp 很类似,一般用来设置字体大小,和 dp 的区别是它可以根据用户的字体大小偏好来缩放。
-
density 和 dpi 的关系为 density = dpi / 160
-
pt 表示一个点,是屏幕的物理尺寸,其大小为 1 英寸的 1 / 72,也就是 72pt 等于 1 in。
日常开发中,比较关注的是 dp 和 px 之间的转换:
//px 转成 dp
public static int px2dip(Context context, float pxValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (pxValue / scale + 0.5f); // int 向下取整,加0.5 是实现四舍五入
}
//dp 转成 px
public static int dip2px(Context context, float dipValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dipValue * scale + 0.5f);
}
使用系统的预配置值
推荐使用 ViewConfiguration 类来获取 Android 系统常用的距离、速度和时间,例如可通过 getScaledTouchSlop() 方法获取系统用作滚动阈值的距离(以像素为单位):
private static final int GESTURE_THRESHOLD_DP = ViewConfiguration.get(myContext).getScaledTouchSlop();
ViewConfiguration 中以 getScaled 为前缀的方法都会返回以像素为单位的值,无论当前像素密度是多少,该值都会正确显示。
将图片资源放置在合适的目录下
- ldpi 适用于低密度 (ldpi) 屏幕 (~ 120dpi) 的资源。
- mdpi 适用于中密度 (mdpi) 屏幕 (~ 160dpi) 的资源(这是基准密度)。
- hdpi 适用于高密度 (hdpi) 屏幕 (~ 240dpi) 的资源。
- xhdpi 适用于加高 (xhdpi) 密度屏幕 (~ 320dpi) 的资源。
- xxhdpi 适用于超超高密度 (xxhdpi) 屏幕 (~ 480dpi) 的资源。
- xxxhdpi 适用于超超超高密度 (xxxhdpi) 屏幕 (~ 640dpi) 的资源。
- nodpi 适用于所有密度的资源。这些是与密度无关的资源。无论当前屏幕的密度是多少,系统都不会缩放以此限定符标记的资源。
- tvdpi 适用于密度介于 mdpi 和 hdpi 之间的屏幕(约 213dpi)的资源。这不属于“主要”密度组。它主要用于电视,而大多数应用都不需要它。
不过日常开发中设计师一般也只给 3 倍图,我们把其放在 xxhdpi 目录下即可,这是综合了减少包体积和显示效果等因素后的结果。
将应用图标放在 mipmap 目录中
与其他所有位图资源一样,对于应用图标,需要提供特定于密度的版本。不过,某些应用启动器显示的应用图标会比设备的密度级别所要求的大差不多 25%。
例如,如果设备的密度级别为 xxhdpi 且提供的最大应用图标在 drawable-xxhdpi 中,那么启动器应用会放大此图标,这会使其看起来不太清晰。因此应在 mipmap-xxxhdpi 目录中提供一个密度更高的启动器图标,而后启动器便可改用 xxxhdpi 资源。
由于应用图标可能会像这样放大,因此应将所有应用图标都放在 mipmap 目录中,而不是放在 drawable 目录中。与 drawable 目录不同,所有 mipmap 目录都会保留在 APK 中,这样,启动器应用便可选取要显示在主屏幕上的最佳分辨率图标。
拓展:
-
宽高限定符适配
其思想就是设定一个基准的分辨率,其他分辨率都根据这个基准分辨率来计算,根据该尺寸编写对应的 dimens 文件,然后 UI 设计稿是以基准分辨率制作,这样当 App 运行不同分辨率的时候,就会根据 dimens 引用该分辨率的文件夹下面去寻找相应的值。但是这种方案的容错机制很差,如果没有找到相应的限定符,使用默认的 dimens 文件就会出现变形甚至错乱,而且增大包体积。 -
smallWidth 适配
这个本质上和宽高限定符是一样的,也是生成相应的 dimens 文件,不过容错机制比宽高限定符好,毕竟是通过范围指定寻找相应的适合分辨率。 -
今日头条适配方案
原理主要是通过修改 density 值,然后按照宽/高的一个维度去适配,保持该维度上和设计图一致。这个方案侵入性低,没有设计私有 API,不够对老项目不太友好,而且可能会影响第三方库 View 的大小。 - 基于今日头条适配方案的 pt 方案
结束语
以上就是关于屏幕适配的一些通用方法啦。