Android 进阶——高级UI必知必会之常用的屏幕适配完全攻略详解(七)

引言

本系列高级UI相关文章中,已经将Android的2D绘制基础已经大致地进行了总结,相信都已经掌握了2D绘制的相关知识,这一篇就总结下关于常见屏幕适配的相关原理,需要说明的是:可以说目前并没有绝对完美的屏幕适配方案,即使在大厂里所用的也不一定是完美的。相关系列文件链接如下:

一、屏幕适配概述

Android 之所以得以发展壮大成为全球第一的移动手机操作系统,得益于它的开源性,前期由Google公司和开放手机联盟(Open Handset Alliance)领导及开发,后面越来越多的ODM、OEM、ROM提供商加入到Android阵营,在带给Android巨大的活力的同时,也带来了Android 最严重的的问题之一——“碎片化”,表现在设备碎片化、品牌碎片化、系统碎片化、传感器碎片化和屏幕碎片化等多个方面,而作为Android开发者我们首先要应对的就是屏幕碎片化的棘手问题。

二、屏幕相关术语

Android 系统为了适配更多的设备,更多的不同的屏幕,除了物理存在的相关术语之外,还抽象了一些术语来通过一定的逻辑关系与物理存在的术语相联系。
在这里插入图片描述

1、屏幕尺寸

屏幕尺寸指的是屏幕的对角线的长度(单位是:英寸1英寸=2.54厘米)。

2、屏幕分辨率

屏幕分辨率是用屏幕纵向的像素点乘以横向的像素点总数(单位是:px1px=1个像素点),比如1920*1080分辨率代表的是纵向上有1920个像素点,而横向上有1080个像素点。

3、屏幕像素密度

屏幕像素密度每英寸上的像素点数(单位是:dpi,即dot per inch),屏幕像素密度与屏幕分辨率屏幕尺寸相关,可以通过以下公式计算:
在这里插入图片描述

4、dp、dip 、dpi、sp和px

  • dp——密度独立像素是Android 特有的抽象出来的概念,与屏幕分辨率无关,dip 同dp一个意思,目前废弃了,一般都写dp。Android内部在识别图像像素时以160dpi为基准(1dip=1px1dp=1px)比如说在下列两台设备上使用DP进行操作时:
分辨率 屏幕像素密度 说明
480 * 320 160dpi 那么这台机器上的1dp会被翻译成1px
800 * 480 240dpi 而这台机器上的1dp会被翻译成1.5px

因为dp是由Android给予的基础标准按比例进行翻译的,这也是为什么我们用dp 能解决一部分适配的原因。

  • dpi——屏幕像素密度的单位。
  • sp——即Scale-IndependentPixels,可根据文字大小首选项自动进行缩放,Google推荐我们使用12sp以上的大小,通常可以使用12sp,14sp,18sp,22sp,为避免精度损失,建议最好不要使用奇数和小数。
  • px——就是我们常说的像素,dp转为px的公式: px = dp * (dpi / 160)

Android中无论使用的是dp还是sp,最终都会转为px进行运算

假设在某一应用中,用户的手指至少移动 16 像素之后,系统会识别出滚动或滑动手势,那么在基准屏幕上,用户的手指必须至少移动 16 pixels / 160 dpi,相当于 1 英寸的 1/10(2.5 毫米),相应手势才能被识别;而在配备高密度显示屏 (240dpi) 的设备上,用户的手指必须至少移动 16 pixels / 240 dpi,相当于 1 英寸的 1/15(1.7 毫米),此距离短得多,因此用户会感觉应用在该设备上更灵敏。要解决此问题,手势阈值必须在代码中以 dp 表示,然后再转换为实际像素

// The gesture threshold expressed in dp
private static final float GESTURE_THRESHOLD_DP = 16.0f;
// Get the screen's density scale
final float scale = getResources().getDisplayMetrics().density;
// Convert the dps to pixels, based on density scale
mGestureThreshold = (int) (GESTURE_THRESHOLD_DP * scale + 0.5f);
// Use mGestureThreshold as a distance in pixels...

**DisplayMetrics.density 字段根据当前像素密度指定要将 dp 单位转换为像素而必须使用的缩放系数。**对于中密度屏幕,DisplayMetrics.density 等于 1.0;对于高密度屏幕,它等于 1.5;对于超高密度屏幕,它等于 2.0;对于低密度屏幕,它等于 0.75

5、mdpi、hdpi、xhdpi、xxhdpi和xxxhdpi

名称 像素密度范围 图片大小
mdpi (基准1.0x) 120dp~160dp 48×48px
hdpi (1.5x) 160dp~240dp 72×72px
xhdpi (2.0x) 240dp~320dp 96×96px
xxhdpi (3.0) 320dp~480dp 144×144px
xxxhdpi (4.0x) 480dp~640dp 192×192px

在Google文档中说明了 mdpi:hdpi:xhdpi:xxhdpi:xxxhdpi=2:3:4:6:8 的尺寸比例并进行缩放。例如一个图标的大小为48×48dp时在mdpi上图片的实际大小为48×48px;而在hdpi像素密度上,实际尺寸为mdpi上的1.5倍,则图片的实际大小为72×72px,所以需要把正确分辨率的图片放到对应的文件夹下,系统将根据运行应用的设备的像素密度自动选取正确的文件

如果你不希望那些资源在运行时不被缩放,则应将这些资源放在带有 nodpi 配置限定符的目录中,带有此限定符的资源被视为与密度无关,系统将不会对它们进行缩放。

三、屏幕适配策略

Android SDK自身就提供了基础的适配能力,当然也是最优的适配方案,在Framework层自动进行适配:

  • 在配置布局XML文件时,避免配置固定的尺寸,优先使用wrap_contentmatch_parent
  • 在LinearLayout中使用weight进行配置,不过呢,不建议嵌套LinearLayout里使用weight(因为在嵌套的 LinearLayout 中使用权重时,系统会执行多遍布局以确定每个视图的尺寸,从而降低界面性能)。
  • 灵活使用相对布局和约束布局,因为凡是 LinearLayout 所能构建的布局,ConstraintLayout 几乎都能构建,而不会影响性能,因此您应该尝试将布局转换为 ConstraintLayout。然后,您可以使用约束链定义加权布局。
  • 对于不同图片资源采用.9和SVG替代,灵活实现缩放。

不过呢以上是优先考虑的原则,但是并不能完全适配所有的业务UI,所以引出以下的策略。

使用 ConstraintLayout 时,不宜使用 match_parent,而是应将尺寸设为 0dp 以启用一种称为“匹配约束”的特殊行为

1、限定符适配

所谓的限定符就是Android在进行资源加载的时候会自动获取屏幕的相关信息,进而对相应的文件夹(通过对应的名字)进行识别,而这些特殊名字就是我们的限定符,限定符可以分为四类:

  • 根据屏幕尺寸,小屏幕small 、基准屏幕normal 、大屏幕large(大于7inch) 和 超大屏幕xlarge
  • 根据屏幕密度
屏幕密度 说明
ldpi <=120dpi
mdpi <= 160dpi
hdpi <= 240dpi
xhdpi <= 320dpi
xxhdpi <= 480dpi
xxhdpi <= 640dpi(只用来存放icon)
nodpi 与屏幕密度无关的资源.系统不会针对屏幕密度对其中资源进行压缩或者拉伸
tvdpi 介于mdpi与hdpi之间,特定针对213dpi,专门为电视准备的,手机应用开发不需要关心这个密度值.
  • 根据屏幕方向,横屏land,竖屏port

  • 根据屏幕宽高比,比标准屏幕宽高比明显的高或者宽的这样屏幕long 、和标准屏幕配置一样的屏幕宽高比notlong

限定符指定的尺寸并不是实际屏幕尺寸,而是 Activity 的窗口可用的宽度或高度(以 dp 为单位)。Android 系统可能会将部分屏幕用于系统界面(如屏幕底部的系统栏或顶部的状态栏),所以此部分屏幕不可供布局使用。如果App在多窗口模式下使用,则它只能使用该窗口的尺寸,且该窗口进行大小调整时就会使用新窗口尺寸并触发配置更改,以便系统可以选择适当的布局文件。因此,在声明尺寸时,您应具体说明 Activity 需要的尺寸。在声明为布局提供多少空间时,系统会考虑系统界面使用的所有空间。

1.1、屏幕分辨率适配(不推荐)

屏幕分辨率限定符适配(以px为单位)需要在 res 文件夹下创建各种屏幕分辨率对应的 values-xxx 文件夹:
在这里插入图片描述
把所有的需要适配的设备的分辨率都创建对应的资源文件夹,然后选择一个基准分辨率,例如基准分辨率为 1280x720,先将宽度等分成 720 份,则对应取值为 1px~720px,再将高度分成 1280 份,取值为 1px~1280px,生成各种分辨率对应的 dimens.xml 文件。如下分别为分辨率 1280x720 与 1920x1080 所对应的横向dimens.xml 文件(values-xx/lay_x.xml):
在这里插入图片描述
假设设计图上的一个控件的宽度为 720px,那么布局中配置android:layout_width="@dimen/x720" ,当运行时系统会根据设备的分辨率去寻找对应的 dimens.xml 文件。在分辨率为 1280x720 的设备上时就会去找 values-1280x720 文件夹下的 lay_x.xml 文件,而运行在分辨率为 1920x1080 的设备上,系统就会去找 values-1920x1080 文件夹下的 lay_x.xml 文件,从而完成适配,本质上就是
多套dimens资源适配
,不过呢这是相当古老的策略,基本已经淘汰了。

1.2、屏幕尺寸限定符适配 (过时)

当我们一个App中的同一页面在平板和手机时,布局和尺寸都不一样就可以使用屏幕尺寸来进行适配,本质上也和屏幕分辨率适配差不多,也是创建多套res资源进行适配,除了创建dimens资源外还可能创建多套layout资源,在屏幕尺寸大于7英寸的设备上就会加载res/layout-large/下的布局文件,而在小于7英寸的设备上就会加载res/layout/下的,仅适用于Android3.2之前,在此版本之后为了更精确地区分屏幕尺寸大小,Google推出了最小宽度限定符smallestWidth 。

1.3、smallestWidth 限定符适配(主流)

最小“宽度”限定符 smallestWidth 限定符(以dp和sp为单位)的使用和屏幕尺寸基本一致,只是使用了具体的“宽度”(以dp为单位)进行限定,程序运行在最小宽度为 360dp 的设备上,系统会自动找到对应的 values-sw360dp 文件夹下的 dimens.xml 文件(比如sw360dp代表的是最小“宽度”为360dp),

此处“最小宽度”是不区分屏幕方向的,即无论是宽度还是高度,哪一边小就认为哪一边是“最小宽度”,它代表的是屏幕两侧的最小尺寸。

如下分别为最小宽度为 360dp 与最小宽度为 640dp 所对应的 dimens.xml 文件:
在这里插入图片描述

虽然与屏幕分辩限定符适配意义都需要多套 dimens.xml 文件, 那么smallestWidth 限定符适配的优势在哪呢?首先屏幕分辨率限定符适配是根据屏幕分辨率的,Android 设备分辨率一大堆,而且还要考虑虚拟键盘和状态栏,这样就需要大量的 dimens.xml 文件。而无论手机屏幕分辨率和像素密度几何,90% 的手机的最小“宽度”都为 360dp平板的最小“宽度”为580dp),所以采用 smallestWidth 限定符适配只需要少量dimens.xml 文件即可,再者屏幕分辨率限定符适配需要设备分辨率与 values-xx 文件夹完全匹配才能达到适配,而 smallestWidth 限定符适配在寻找 dimens.xml 文件时是从大往小找,例如设备的最小宽度为 360dp,就会先去找 values-360dp,若找不到则会向下去找最接近的,若还没有则使用默认的 values 下的 demens.xml 文件,所以即使没有完全匹配也能达到不错的适配效果。谷歌说明了不同屏幕 dp 宽度与不同屏幕尺寸和方向的一般对应关系:
在这里插入图片描述

获取设备最小“宽度”代码为:

DisplayMetrics dm = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(dm);
int heightPixels = ScreenUtils.getScreenHeight(this);
int widthPixels = ScreenUtils.getScreenWidth(this);
float density = dm.density;
float heightDP = heightPixels / density;
float widthDP = widthPixels / density;
float smallestWidthDP;
if(widthDP < heightDP) {
    smallestWidthDP = widthDP;
}else {
    smallestWidthDP = heightDP;
}

以上的这些值可以通过ScreenMatch 插件自动生成dimens里的值:

  • 首先在res目录下右键,然后选择ScreenMatch,点击OK之后,就会在Project目录下生成默认的values/dimens.xml文件里的值(不过名称是screenMatch_example_dimens.xml)
    在这里插入图片描述
    和用于配置ScreenMatch插件的screenMatch.properties文件
    在这里插入图片描述
  • 把screenMatch_example_dimens.xml文件里的值复制到values下的dimens.xml
  • 根据需求修改默认的配置文件(默认是以360dp作为基线)
  • 再选中dimens.xml文件右键ScreenMatch,点击OK。

系统在进行资源加载的时候,会根据自身机型的特性不同,去加载不同文件夹底下的资源,sw360dp大多数情况下都可以兼容所有手机的分辨率。

sw360dp---->大多数手机设备
sw720dp---->10寸平板
sw560dp---->8寸平板
sw480dp---->7寸平板

1.4、屏幕方向限定符适配

如果我们要求给横屏、竖屏显示的布局不一样,就可以使用屏幕方向限定符来实现,另外屏幕方向限定符适配还可以搭配最小“宽度”限定符来使用,其他的思想也大同小异,比如res/values-sw360dp-port/activity_main.xml表示

1.5、可用宽度限定符适配

可用宽度限定符适配是根据当前可用的宽度或高度来进行适配的,而不是根据屏幕的最小宽度来加载布局。比如说App有一个界面布局,需求是在屏幕宽度至少为 600dp 时才使用该布局,就可以使用可用宽度限定符进行适配,原理大同小异,创建对应的资源例如res/layout-w600dp/main_activity.xml

以上所有的限定符策略,核心思想都是一样的,就是多套资源进行适配,当然也可以在清单文件中进行声明所支持的屏幕分辨率。

2、布局别名适配

所谓布局别名适配就是把不同名字的布局XML文件,通过在values声明,使之产生联系代表同一个文件,实现起来很简单,假如App一个界面在手机(res/layout/main.xml )和平板(res/layout/main_pad.xml )上需要分别加载不同的布局,使用布局别名适配方案的话需要以下步骤:

2.1、首先,在res/layout/下创建两种布局文件res/layout/main.xml和res/layout/main_pad.xml

2.2、然后分别建立res/values/layout.xml、res/values-large/layout.xml、res/values-sw600dp/layout.xml(Android3.2之后的平板布局)三个文件。

  • res/values/layout.xml
<resources>
    <item name="main" type="layout">@layout/main</item>
</resources>
  • res/values-large/layout.xml

当App运行在Android3.2之前的平板上时,会自动加载该values下对应的文件引用

<resources>
    <item name="main" type="layout">@layout/main_pad</item>
</resources>
  • res/values-sw600dp/layout.xml
    当App运行在Android3.2之后的平板上时,会自动加载该values下对应的文件引用
<resources>
    <item name="main" type="layout">@layout/main_pad</item>
</resources>

以上三个values文件皆声明了“main”别名布局。

  • 使用别名

经过再values声明了之后就有了main为别名的布局,在Activity中setContentView(R.layout.main)即可,App在运行时就自动会检测手机的屏幕大小,如果是平板设备就会加载res/layout/main_pad.xml,如果是手机设备,就会加载res/layout/main.xml 。

3、清单文件里配置支持的屏幕

App自然是使其适合所有屏幕尺寸和密度的设备上运行最好,但如果有特殊要求,可能不希望App在某些特殊屏幕上运行,就可以在manifest清单文件上声明。

3.1、配置屏幕的最大长宽比

用户可以在分屏和自由窗口模式下启动可调整大小的 Activity并通过拖动其边或角来更改该 Activity 的大小,多窗口模式适用于在 Android 7.0或更高版本中运行的所有应用并应用默认可调整大小。 当然还可以为整个应用或特定 Activity 明确设置 android:resizeableActivity=true 属性(当android:resizeableActivity =false时就不支持多窗口模式)。 Android 8.0和更高版本设置最大长宽比可以在清单文件中 < activity > 标记中配置 android:MaxAspectRatio 来配置最大比例:

<!-- Render on full screen up to screen aspect ratio of 2.4 -->
<!-- Use a letterbox on screens larger than 2.4 -->
<activity android:maxAspectRatio="2.4">
 ...
</activity>

在Android 7.1 及更低版本,请在 < application > 元素中添加一个 名为 android.max_aspect 的 < meta-data > 元素:

<!-- Render on full screen up to screen aspect ratio of 2.4 -->
<!-- Use a letterbox on screens larger than 2.4 -->
<meta-data android:name="android.max_aspect" android:value="2.4" />

要想最大长宽比生效,Activity 的android:resizeableActivity必须为false

3.2、配置支持平板或电视

在清单文件里使用 < supports-screens > 节点配置支持的屏幕尺寸。

<manifest ... >
    <supports-screens android:smallScreens="false"
                      android:normalScreens="false"
                      android:largeScreens="true"
                      android:xlargeScreens="true"/>
    ...
</manifest>

清单文件里虽然支持 < compatible-screens > 配置App支持的确切特定的屏幕尺寸和密度,< compatible-screens> 元素必须包含一个或多个 < screen> 元素且每个 < screen > 元素都使用 android:screenSize 和 android:screenDensity 属性指定与您的应用兼容的屏幕配置 。 每个 < screen > 元素都必须同时包含这两个属性才能指定一个屏幕配置 ,缺少任一属性都会导致该元素无效。

<manifest ... >
    <compatible-screens>
        <!-- all small size screens -->
        <screen android:screenSize="small" android:screenDensity="ldpi" />
        <screen android:screenSize="small" android:screenDensity="mdpi" />
        <screen android:screenSize="small" android:screenDensity="hdpi" />
        <screen android:screenSize="small" android:screenDensity="xhdpi" />
        <!-- all normal size screens -->
        <screen android:screenSize="normal" android:screenDensity="ldpi" />
        <screen android:screenSize="normal" android:screenDensity="mdpi" />
        <screen android:screenSize="normal" android:screenDensity="hdpi" />
        <screen android:screenSize="normal" android:screenDensity="xhdpi" />
    </compatible-screens>
    ...
    <application ... >
        ...
    <application>
</manifest>

4、运行时动态代码适配

核心思想很简单,选取一个分辨率作为基准,计算缩放比例,然后继承ViewGroup重写onMeasure方法,在布局里替换Android系统的原生ViewGroup,用自己写的ViewGroup包裹控件,并且在onMeasure方法里重新设置尺寸,由于篇幅问题,请参见下篇。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章