Android 屏幕适配,那些你必须掌握的稀碎知识点

5,268 阅读20分钟

背景

屏幕适配一直是 Android 中非常重要的环节,但是也涉及很多琐碎的知识点,本文将带你深入分析屏幕适配的各个环节,看完本文您将能够回答以下问题:

  1. dpi,ppi,density,dp,px 有什么区别?

  2. 为什么适配时要把图片放在最大分辨率的目录下呢?

  3. 同一张图片放在不同的 drawable 目录下,加载进内存后会有宽高尺寸的变化吗?

  4. 图片放在 xhdpi 目录下,分别用不同屏幕像素密度的手机加载进内存后会有宽高尺寸的变化吗?

  5. 为啥在硬盘上存储只需要 77.11k,放到内存里面就需要 30 多 M 呢?

  6. 为什么最好只将应用图标放在 mipmap 目录中?

  7. 手动修改屏幕像素密度会不会影响 app 加载固定分辨率的文件夹?

  8. 主流适配方案的对比?

dpi,ppi,density,dp,px 有什么区别?

DPI/PPI DPI = Dots Per Inch,PPI = Pixel Per Inch 两个参数的区别就在于 Dot 和 Pixel的区别,dot值的是显示器上每一个物理的点,而 pixel 指的是屏幕分辨率中的最小单位。这个两个难道会不一样么?会!当一个像素需要多于一个屏幕上的物理点来显示的时候 dot 就跟 pixel 不一样了。这个有另一个叫法叫做dppx(dot per pixel),即每个像素中有多少个点。大部分的显示器中一个像素即一个点,但目前一些比较好的屏幕和一些手机屏幕中 dppx 会大于1。比如说 Mac Retina,iPhone,HTC One 等。总的来说我们一般针对 Android 手机说的 dpi 和 ppi 是等价的。所有我们就把他们统称为屏幕像素密度了。屏幕密度越低在给定物理区域的像素就会越少,Android 中通用密度如下:

ldpi适用于低密度 (ldpi) 屏幕 (~ 120dpi) 的资源。
mdpi适用于中密度 (mdpi) 屏幕 (~ 160dpi) 的资源(这是基准密度)。
hdpi适用于高密度 (hdpi) 屏幕 (~ 240dpi) 的资源。
xhdpi适用于加高 (xhdpi) 密度屏幕 (~ 320dpi) 的资源。
xxhdpi适用于超超高密度 (xxhdpi) 屏幕 (~ 480dpi) 的资源。
xxxhdpi适用于超超超高密度 (xxxhdpi) 屏幕 (~ 640dpi) 的资源。
nodpi适用于所有密度的资源。这些是与密度无关的资源。无论当前屏幕的密度是多少,系统都不会缩放以此限定符标记的资源。
tvdpi适用于密度介于 mdpi 和 hdpi 之间的屏幕(约 213dpi)的资源。这不属于“主要”密度组。它主要用于电视,而大多数应用都不需要它。对于大多数应用而言,提供 mdpi 和 hdpi 资源便已足够,系统将视情况对其进行缩放。如果您发现有必要提供 tvdpi 资源,应按一个系数来确定其大小,即 1.33*mdpi。例如,如果某张图片在 mdpi 屏幕上的大小为 100px x 100px,那么它在 tvdpi 屏幕上的大小应该为 133px x 133px。
anydpi此限定符适合所有屏幕密度,其优先级高于其他限定符。这非常适用于矢量可绘制对象此项为 API 级别 21 中的新增配置
nnndpi用于表示非标准密度,其中 *nnn* 是正整数屏幕密度。此限定符不适用于大多数情况。使用标准密度存储分区,可显著减少因支持市场上各种设备屏幕密度而产生的开销。

要针对不同的密度创建备用可绘制位图资源,您应遵循六种主要密度之间的 3:4:6:8:12:16 缩放比。例如,如果您有一个可绘制位图资源,它在中密度屏幕上的大小为 48x48 像素,那么它在其他各种密度的屏幕上的大小应该为:

  • 36x36 (0.75x) - 低密度 (ldpi)
  • 48x48(1.0x 基准)- 中密度 (mdpi)
  • 72x72 (1.5x) - 高密度 (hdpi)
  • 96x96 (2.0x) - 超高密度 (xhdpi)
  • 144x144 (3.0x) - 超超高密度 (xxhdpi)
  • 192x192 (4.0x) - 超超超高密度 (xxxhdpi)

density:通过代码 context.getResources().getDisplayMetrics().density获取的“density”值。而通过该方法获取到的该值,实际上是等价于“dpi / 160”的一个结果值。

dp:密度无关像素,1 dp 约等于中密度屏幕(160dpi;“基准”密度)上的 1 像素。如果要在密度不同的屏幕上显示大致相同的可见尺寸,应该使用密度无关像素 dp。

image.png

  • 在分辨率 320*480(即 dpi 为 160)的设备下,则 160dp 等价于 160px,按钮占屏幕宽的一半。
  • 在分辨率 640*960(即 dpi 为 320)的设备下,则 160dp 等价于 320px,按钮依然占屏幕宽的一半。

px:其实就是像素单位,比如我们通常说的手机分辨列表 800*400 都是 px 的单位

dp,px 与160之间存在着某种规律:“1dp = (dpi / 160)px”,dp = px / density,Android 提供 density 的目的,归根结底就是为了做屏幕的适配。

image.png

同一张图片放在不同的 drawable 目录下,加载进内存后会有宽高尺寸的变化吗?

    <ImageView
        android:id="@+id/image"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/dev"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

Java 代码:

Drawable drawable = imageView.getDrawable();
if (drawable != null) {
  	BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
  	Bitmap bitmap = bitmapDrawable.getBitmap();
  	Log.d(TAG, " width = " + bitmap.getWidth() + " height = " + bitmap.getHeight());
} 

原图大小:352 * 484

放在 320dpi(xhdpi) 的设备上的 xhdpi 的目录中,获取到图片的宽高为 352 * 484,因为设备为 xhdpi 和 xhdpi 目录相对应,所以获取到图片的宽高自然就没有变化了。

放在 mdpi 目录中,获取到图片的宽高为 704 * 968,可以这样理解,图片放在mdpi 目录中,碰到 xhdpi 的手机,系统默认 mdpi 中的图片都比较小,就要进行放大,因为 xhdpi 是 mdpi 的两倍,所以 Bitmap 的宽高都放大了两倍。

放在xxhpi 目录中,获取到图片的宽高为 235 * 323,253 = 352 * (2/3) 323 = 484 * (2/3)

同时需要注意的是 drawable 和 drawable-mdpi 是一样的大小,是因为 drawable-mdpi 是系统默认的像素密度,其他像素密度都以它为基数,当只在 drawable 中存在图片时,如果使用该图片,那么将按照 drawable-mdpi 的放缩比例进行缩放

为什么适配时要把图片放在最大分辨率的目录下呢?

前面说过内存占用计算公式:宽 * 高 * 单位像素占用字节,放在低分辨率的手机上,Bitmap 的宽高反而变大了,那内存也增加了吗?答案是的,因为内存只和图片的宽高,单位像素占用字节有关。同一张图片,放在不同目录下,会生成不同大小的 Bitmap,Bitmap 的宽度和高度越大,占用的内存就越大,放在越低分辨率中占用的内存越大,所以为了适配(如果只有一个文件夹的话),应该尽量把图片放在 xxxhdpi 下面

图片放在 xhdpi 目录下,分别用不同屏幕像素密度的手机加载进内存后会有宽高尺寸的变化吗?

原图 Bitmap 大小:352 * 484,设备 320dpi(xhdpi)

放在 320dpi(xhdpi) 的设备上的 xhdpi 的目录中,获取到图片的宽高为 352 * 484,因为设备为 xhdpi 和 xhdpi 目录相对应,所以获取到图片的宽高自然就没有变化了。

原图 Bitmap 大小:352 * 484,设备 480dpi(xxhdpi),放在xhdpi 目录中

当使用 xxhdpi 的设备后,获取到图片的宽高为 528 * 726,也就是宽和高分别乘以 3/2,可以看出,在碰到更高分辨率(屏幕密度的手机),图片的宽高会根据比例进行放大,如果使用 mdpi 的设备下则缩小一半;

如果这个时候我把图片放在 mdpi 的目录下(原始大小),使用 xxhdpi 设备,获取到图片的宽高为 1056 * 1452,放大的倍数为 3 倍。

小结

在同一个设备上,图片放在依次放在由低到高的分辨率目录中,图片的 Bitmap 的大小不断减小。

在同一个分辨率目录中,依次运行在由低到高的分辨率设备上,图片的 Bitmap 的大小不断增加。

为啥在硬盘上存储只需要77.11k,放到内存里面就需要30多M呢?

因为这根本不是一回事呀~

存放在硬盘上的图片文件,会根据各自的压缩规则进行压缩,比如 Jpeg 这种有损压缩的图片格式,最常使用可变字长编码的哈弗曼编码,会使用哈弗曼树,也就是最优二叉树,根据某些数据出现的频率对数据段编码,从而减少占用的硬盘大小。

比如说 “10111” 这个序列在图片的二进制数据中出现的概率最大,那我们可以用“01”来代替这一段数据,原来5位的数据,用2位就可以表示了,这就是压缩率60%。当然这只是打个比方,在实际操作中需要考虑“异前缀原则”等编码的基本原则。

而如果把图像读取到内存中就不一样了,因为我们需要每一个像素都能在屏幕上显示,所以会把每个像素点都加载至内存中,不会对相同像素进行压缩或者是替换,所以你也应该能明白前面提到的Bitmap占用内存大小的计算公式的由来了。

修改屏幕像素密度会不会影响 app 加载固定分辨率的文件夹?

经测试,在不同分辨率下放置内容不同的图片但名称相同的图片,修改屏幕像素密度并不会影响 app 加载固定分辨率的文件夹,也就是说还是加载相同内容的图片。

adb shell wm density   //查看屏幕像素密度
adb shell wm density 640 //设置屏幕像素密度为 640
adb shell wm density reset  //重置为初始像素密度

最好只将应用图标放在 mipmap 目录中

mipmap 翻译过来就是纹理映射技术,mipmap 文件夹下的图标会通过 Mipmap 纹理技术进行优化。Mipmap 纹理技术是目前解决纹理分辨率与视点距离关系的最有效途径,它会先将图片压缩成很多逐渐缩小的图片,例如一张 64 * 64的图片,会产生64 * 64,32 * 32,16 * 16,8 * 8,4 * 4,2 * 2,1 * 1的 7 张图片,当屏幕上需要绘制像素点为20 * 20 时,程序只是利用 32 * 32 和 16 * 16 这两张图片来计算出即将显示为 20 * 20 大小的一个图片,这比单独利用 32 * 32 的那张原始片计算出来的图片效果要好得多,速度也更快

为什么要把应用图标放在 mipmap 目录中,下面引用官方的解释:

与其他所有位图资源一样,对于应用图标,您也需要提供特定于密度的版本。不过,某些桌面应用显示的应用图标会比设备的密度级别所要求的大差不多 25%(However, some app launchers display your app icon as much as 25% larger than what's called for by the device's density bucket)。

例如,如果设备的密度存储分区为 xxhdpi,而您提供的最大应用图标密度级别为 drawable-xxhdpi,则启动器应用会放大此图标,这会导致它看起来不太清晰。因此,您应在 mipmap-xxxhdpi 目录中提供一个密度更高的启动器图标,而后启动器便可改用 xxxhdpi 资源。

由于应用图标可能会像这样放大,因此您应将所有应用图标都放在 mipmap 目录中,而不是放在 drawable 目录中。与 drawable 目录不同,所有 mipmap 目录都会保留在 APK 中,即使您构建特定于密度的 APK 也是如此。这样,启动器应用便可选取要显示在主屏幕上的最佳分辨率图标。

注意

个人感觉应该在不同分辨率的 mipmap 文件夹下分别提供大小不同的图片,然后通过纹理映射技术显示 mipmap 中没有提供的图片时,显示图片又快又好。

所有 mipmap 目录都会保留在 APK 中,即使您构建特定于密度的 APK 也是如此,也就是说就算配置了类似 resConfigs "xhdpi" 这种方式,对 mipmap 目录下的文件没有任何作用,所以最好就是只把应用图标放进去就行了。因为应用图标占用的体积也不大,同时应用图标的清晰显示也至关重要。

市面上主流适配方案(来自 JessYan 大佬的文章)

density 在每个设备上都是固定的,DPI / 160 = density屏幕的总 px 宽度 / density = 屏幕的总 dp 宽度,由于 Android 的碎片化问题,大部分市面上的 Android 设备的宽高比都不一致,这时我们只以高或宽其中的一个作为基准进行适配,就会有效的避免布局在高宽比不一致的屏幕上出现变形的问题。

今日头条适配方案
原理
  • 设备 1,屏幕宽度为 1080px480DPI,屏幕总 dp 宽度为 1080 / (480 / 160) = 360dp
  • 设备 2,屏幕宽度为 1440px560DPI,屏幕总 dp 宽度为 1440 / (560 / 160) = 411dp

可以看到屏幕的总 dp 宽度在不同的设备上是会变化的,但是我们在布局中填写的 dp 值却是固定不变的

这会导致什么呢?假设我们布局中有一个 View 的宽度为 100dp,在设备 1 中 该 View 的宽度占整个屏幕宽度的 27.8% (100 / 360 = 0.278)

但在设备 2 中该 View 的宽度就只能占整个屏幕宽度的 24.3% (100 / 411 = 0.243),可以看到这个 View 在像素越高的屏幕上,dp 值虽然没变,但是与屏幕的实际比例却发生了较大的变化,所以肉眼的观看效果,会越来越小,这就导致了传统的填写 dp 的屏幕适配方式产生了较大的误差。

这时我们要想完美适配,那就必须保证这个 View 在任何分辨率的屏幕上,与屏幕的比例都是相同的。

这时我们该怎么做呢?改变每个 Viewdp 值?不现实,在每个设备上都要通过代码动态计算 Viewdp 值,工作量太大。

如果每个 Viewdp 值是固定不变的,那我们只要保证每个设备的屏幕总 dp 宽度不变,就能保证每个 View 在所有分辨率的屏幕上与屏幕的比例都保持不变,从而完成等比例适配,并且这个屏幕总 dp 宽度如果还能保证和设计图的宽度一致的话,那我们在布局时就可以直接按照设计图上的尺寸填写 dp

屏幕的总 px 宽度 / density = 屏幕的总 dp 宽度

在这个公式中我们要保证 屏幕的总 dp 宽度设计图总宽度 一致,并且在所有分辨率的屏幕上都保持不变,我们需要怎么做呢?屏幕的总 px 宽度 每个设备都不一致,这个值是肯定会变化的,这时今日头条的公式就派上用场了。

当前设备屏幕总宽度(单位为像素)/ 设计图总宽度(单位为 dp) = density

这个公式就是把上面公式中的 屏幕的总 dp 宽度 换成 设计图总宽度,原理都是一样的,只要 density 根据不同的设备进行实时计算并作出改变,就能保证 设计图总宽度 不变,也就完成了适配。

总得来说就是 density 修改为【当前设备屏幕总宽度(单位为像素)/ 设计图总宽度(单位为 dp)】 ,总的 dp 数发生了改变(比如都是设计图的 375),View 设置的dp 保持不变,保持 View 占用总屏幕比例不变

优缺点

优点是代码侵入性低,试错成本接近于 0,但是修改的 density 是全局的,当某个系统控件或三方库控件的设计图尺寸和和我们项目自身的设计图尺寸差距非常大时,这个问题就越严重。(解决方案可以按 Activity 为单位,取消当前 Activity 的适配效果,改用其他的适配方案)。

SmallestWidth 限定符适配方案(最小宽度限定符适配方案)

说到这个,肯定会有人提起宽高限定符屏幕适配方案,可以把 SmallestWidth 限定符适配当做宽高限定符适配方案的升级版。smallestWidth 限定符屏幕适配方案 只是把 dimens.xml 文件中的值从 px 换成了 dp,原理和使用方式都是没变的。

├── src/main
│   ├── res
│   ├── ├──values
│   ├── ├──values-sw320dp
│   ├── ├──values-sw360dp
│   ├── ├──values-sw400dp
│   ├── ├──values-sw411dp
│   ├── ├──values-sw480dp
│   ├── ├──...
│   ├── ├──values-sw600dp
│   ├── ├──values-sw640dp

原理

其实 smallestWidth 限定符屏幕适配方案 的原理也很简单,开发者先在项目中根据主流屏幕的 最小宽度 (smallestWidth) 生成一系列 values-swdp 文件夹 (含有 dimens.xml 文件),当把项目运行到设备上时,系统会根据当前设备屏幕的 最小宽度 (smallestWidth) 去匹配对应的 values-swdp 文件夹,而对应的 values-swdp 文件夹中的 dimens.xml 文字中的值,又是根据当前设备屏幕的 最小宽度 (smallestWidth) 而定制的,所以一定能适配当前设备。

如果系统根据当前设备屏幕的 最小宽度 (smallestWidth) 没找到对应的 values-swdp 文件夹,则会去寻找与之 最小宽度 (smallestWidth) 相近的 values-swdp 文件夹,系统只会寻找小于或等于当前设备 最小宽度 (smallestWidth)values-swdp,这就是优于 宽高限定符屏幕适配方案 的容错率,并且也可以少生成很多 values-swdp 文件夹,减轻 App 的体积。

假如 最小宽度 的值是 360 dp (1080 / (480 / 160) = 360)

现在我们已经算出了当前设备的最小宽度是 360 dp,我们晓得系统会根据这个 最小宽度 帮助我们匹配到 values-sw360dp 文件夹下的 dimens.xml 文件,如果项目中没有 values-sw360dp 这个文件夹,系统才会去匹配相近的 values-swdp 文件夹

smallestWidth 限定符屏幕适配方案 的原理也同样是按百分比进行布局,如果在布局中,一个 View 的宽度引用 dp_100,那不管运行到哪个设备上,这个 View 的宽度都是当前设备屏幕总宽度的 360分之100,前提是项目提供有当前设备屏幕对应的 values-swdp,如果没有对应的 values-swdp,就会去寻找相近的 values-swdp,这时就会存在误差了。

由于该方案是以最小宽度进行适配的,加入设置了 360 份,但是当想要设置的高度大过 360 份时,应该如何设置?

这里边的数字是可以自己定义的,比如设计图的宽度是 375,手机为 1920*1080,dpi=480, 那么此时 values-sw360dp 中的 dimens.xml 文件 375 处生成的值都等于 360 那么 361.... 362.... 这里的值都是可以加的 此文件下不止到375处结束,什么时候结束都是自己可以控制的! 可以通过修改插件,继续生成到一定量的值

其实 smallestWidth 限定符屏幕适配方案 的原理和 今日头条屏幕适配方案 挺像的,今日头条屏幕适配方案 是根据屏幕的宽度或高度动态调整每个设备的 density (每 dp 占当前设备屏幕多少像素),而 smallestWidth 限定符屏幕适配方案 同样是根据屏幕的宽度动态调整每个设备 每份占的 dp 值

优缺点

非常稳定,不会有任何性能损耗,适配范围可以自由控制,不会影响其他第三方库,接入成本也很低。但是侵入性很强,需要在适配效果和空间占用做取舍,不能自动支持横竖屏切换时的适配,如上文所说,如果想自动支持横竖屏切换时的适配,需要使用 values-wdp屏幕方向限定符 再生成一套资源文件,这样又会再次增加 App 的体积。

AndroidAutoSize

AndroidAutoSize 是根据今日头条适配方案进行优化的,AndroidAutoSize 有两种类型的布局单位可以选择,一个是 主单位 (dp、sp),一个是 副单位 (pt、in、mm),两种单位面向的应用场景都有不同,也都有各自的优缺点。

主单位: 使用 dp、sp 为单位进行布局,侵入性最低,会影响其他三方库页面、三方库控件以及系统控件的布局效果,但 AndroidAutoSize 也通过这个特性,使用 ExternalAdaptManager 实现了在不修改三方库源码的情况下适配三方库的功能。

副单位: 使用 pt、in、mm 为单位进行布局,侵入性高,对老项目的支持比较好,不会影响其他三方库页面、三方库控件以及系统控件的布局效果,可以彻底的屏蔽修改 density 所造成的所有未知和已知问题,但这样 AndroidAutoSize 也就无法对三方库进行适配。

原理

至于自动运行解析是运用 ContentProvider,在它的 onCreate 中启动该框架,执行时期比 Application#onCreate 还靠前。至于多线程的初始化可以查看原文进行操作。

如何适配三方库界面?(前提是三方库页面的布局使用的是 dp 和 sp, 如果布局全部使用的 px, 那 AndroidAutoSize 也将无能为力)

在使用主单位时可以使用 ExternalAdaptManager 来实现在不修改三方库源码的情况下,适配三方库的所有页面 (Activity、Fragment)。

通过 ExternalAdaptManager#addExternalAdaptInfoOfActivity(Class, ExternalAdaptInfo) 将需要自定义的类和自定义适配参数添加进方法即可替代实现 CustomAdapt 的方式,这里 展示了使用方式,以及详细的注释。

通过 ExternalAdaptManager#addCancelAdaptOfActivity(Class) 将需要取消适配的类添加进方法即可替代实现 CancelAdapt 的方式,这里 也展示了使用方式,以及详细的注释。

优缺点

算是对今日头条方案方案的全面扩展和优化,解决今日头条方案的痛点,同时更加灵活,目前来看是最全面的方案。

总结

屏幕适配其实是个很琐碎的知识点,能够真正将上面这些知识点串联起来的人并不多,本文参考了许多大佬的文章,感谢他们。喜欢的点个赞吧~

参考

developer.android.com/guide/topic…

developer.android.com/training/mu…

blog.csdn.net/pdskyzcc1/a…

juejin.cn/post/684490…

blog.csdn.net/zhaokaiqian…