Android-屏幕适配经验总结

时间:2021-07-20 18:47:52

本文记录一些适配问题的研究,基础概念不做过多介绍。

Android在做屏幕适配的时候一般考虑两个因素:分辨率和dpi。分辨率是屏幕在横向、纵向上的像素点数总和,一般用“宽x高”的形式表示,例如:1080x1920。dpi是dots per ich的缩写,表示每英寸的像素点数,例如160dpi指手机水平或垂直方向上每英寸距离有160个像素点。

一、dp和px

dp和px都是编写布局时的单位,它们之间可以通过dpi来换算,换算公式如下:

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

常见的还有一个density的概念,表示基准比例,density = (dpi/160),所以还有如下公式:

  • 公式二:px值 = dp值 * density

注:公式中的160是android中规定的基准,即:在160dpi的屏幕上,1dp=1px

通过上面的公式理解dp的概念

dp和dip的含义相同,都是density-independent pixel的缩写,表示密度无关像素,可以保证在不同像素密度的屏幕上显示相同的效果。举个例子:

两种常见的屏幕参数如下:

  • 屏幕1:分辨率=720x1280,dpi=320
  • 屏幕2:分辨率=1080x1920,dpi=480

如果要实现一个view的宽度占屏幕1宽度的一半,由分辨率可知需要view的宽度是360px,根据公式一可以知道使用180dp可以实现相同的效果(360 = dp值 * (320/160) -> dp值=180)。
接下来再根据公式一,看看180dp在屏幕2上的显示效果,px值 = 180 * (480/160) = 540,正好是屏幕2宽度的一半。所以,使用180dp就可以同时在两个屏幕上显示相同的效果了。

二、实际工作中如何使用dp适配

实际工作中,我们一般会使用限定符建立多个资源文件来做适配,一般会适配mdpi,hdpi,xhdpi,xxhdpi等,比如res下的values资源目录如下:

  • res/values-mdpi
  • res/values-hdpi
  • res/values-xhdpi

然后在不同的目录下定义不同的dp值。上面180dp的例子说明了dp值已经保证了在不同像素密度的屏幕上显示相同的效果,那么为什么还要针对不同的屏幕定义不同的dp值?不同的dp值如何确定的?下面结合例子解释这两个问题。

1. 为什么要针对不同的屏幕定义不同的dp值?

这是因为android机型屏幕尺寸太碎片化了,一个dp值并不能满足所有机型,比如常见的还有如下这种:

  • 屏幕3:分辨率=480x800,dpi=240

通过前面的公式计算可以知道,前面例子的180dp在这个屏幕上并不是宽度的一半,160dp才是一半。由此可见,一个dp值并不能满足所有屏幕,所以需要使用限定符适配不同的dpi。最终适配这三种屏幕如下:

  • res/values-hdpi/dimens.xml 中定义资源160dp 适配屏幕3
  • res/values-xhdpi/dimens.xml 中定义资源180dp 适配屏幕1
  • res/values-xxhdpi/dimens.xml 中定义资源180dp 适配屏幕2

限定符mdpi,hdpi,xhdpi等与dpi的对应关系后面给出

2. 不同限定符下的dp值如何确定的?

比如现在项目中有了一套屏幕1(xhdpi)的资源res/values-xhdpi/dimens.xml,其中有一个资源值是90dp,如果要求再适配一下屏幕3(hdpi),那么res/values-hdpi/dimens.xml中与90dp同名的资源应该是多少dp呢?通过前面的介绍,这个值是很好计算的:

  1. 根据公式一计算90dp在屏幕1上是多少px
    px = 90 * (320/160) = 180
  2. 根据1中计算出的像素,计算在屏幕3下同样比例的像素数
    屏幕3下的px = 480 * (180/720) = 120
  3. 根据上一步的结果和公式一计算出在屏幕3的dp值
    dp = 80

如果把计算过程中的90dp改成任意值r,那么最终,屏幕1的每一个资源值 r 乘以 8/9 就是在屏幕3下的dp值,对应的资源文件就是:

  • res/values-xhdpi/dimens.xml 中定义资源 r dp (适配屏幕1)
  • res/values-hdpi/dimens.xml 中定义资源 r*8/9 dp (适配屏幕3)

这里计算出来的8/9是使用横向分辨率和dpi计算出来的,如果是纵向的话并不是这个比例,所以适配的时候需要区分横竖向不同的比例转换。比如宽、横向边距等使用横向的比例,高、竖直边距等使用竖向比例。

需要注意的是这里是按比例计算出来的,最终的值还得根据实际情况和显示效果而定。因为不一定所有界面的设计都是按比例适配的;还有就是有些带虚拟按键的1080x1920的手机,真实的竖直像素数应该是1920减去虚拟按键的高度;还有一点,在android开发中所说的dpi的值并不是物理定义的,而是系统文件写进去的,所以这个值是可以被修改的。另一层意思是,dpi并不是由分辨率和屏幕尺寸计算出来的固定值。比如当前常见的一种机型分辨率是1080x1920,尺寸是5.15英寸,dpi是480。按照dpi的定义,使用这个分辨率和尺寸计算dpi的话,结果并不是480。所以对分辨率和尺寸都相同的手机,dpi值不一定相同,完全看手机厂商如何定义。不过,为了使显示效果最好,一般比较标准的手机dpi和分辨率都和下表一致:

ldpi mdpi hdpi xhdpi xxhdpi
分辨率 240x320 320x480 480x800 720x1280 1080x1920
dpi 120 160 240 320 480

所以,不同限定符下的dp值,使用上面1,2,3步的计算方法能满足大部分主流机型,但不一定能完美适配所有机型。

限定符与dpi的具体对应关系如下:

限定符 ldpi mdpi hdpi xhdpi xxhdpi
dpi dpi<=120 120<dpi<=160 160<dpi<=240 240<dpi<=320 320<dpi<=480

限定符的知识不仅如此,这里不做过多介绍。

三、DisplayMetrics类和wm命令

代码中,可以通过DisplayMetrics类来获取屏幕的一些信息,有三种方式可以获取DisplayMetrics的实例:

//方法1
DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);

//方法2
WindowManager wm = (WindowManager) getSystemService(
        Context.WINDOW_SERVICE);
DisplayMetrics metrics= new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(metrics);

//方法3
DisplayMetrics metrics = getResources().getDisplayMetrics();

可以通过DisplayMetrics获取的如下信息:

//等号后面的数值是某个手机的参数,分辨率=1080x1920,dpi=480,尺寸5.15英寸
metrics.heightPixels = 1920
metrics.widthPixels = 1080
metrics.densityDpi = 480   //dpi值
metrics.density = 3.0   //基准比例,dpi/160
metrics.xdpi = 422.03    //x方向准确的物理像素密度
metrics.ydpi = 424.069  //y方向准确的物理像素密度
metrics.scaledDensity = 3.0

这里的xdpi和ydpi和densityDpi的值不同,再次说明android开发中所说的dpi的值不是由硬件决定的
利用DisplayMetrics类,可以实现一些常用的工具方法,比如dp转px,px转dp等。或者在一些不方便使用资源适配的情况下,可以通过这个类判断不同的dpi来通过java代码来适配。

wm命令

wm命令是高通平台下对手机分辨率、像素密度等进行设置的命令。用法很简单:
首先使用adb shell命令进入手机的shell中,然后就可以使用wm命令了,常用的命令如下:

wm size  //输出手机的分辨率信息

如果要修改手机分辨率,上面的命令加上分辨率参数即可,比如把手机分辨率修改为800x1280,命令如下

wm size 800x1280 

如果要把上面命令修改的分辨率还原为手机原始的分辨率,使用下面的命令

wm size reset

上面是分辨率相关的命令,dpi的命令和上面的类似,如下:

wm density
wm density 240
wm density reset

掌握wm命令后,就可以方便的查看手机分辨率和dpi了,也可以使用同一部手机测试多种分辨率的适配。

注:使用wm命令修改分辨率或dpi后,在某些手机中,再使用reset命令还原后,手机某些内容可能会显示不正常(比如状态栏、输入法等),重启手机即可解决。

四、小结

通过前面几节,应该知道以下几点:

  1. 屏幕适配的时候一般考虑的两个因素:分辨率和dpi。
  2. 利用dpi换算dp和px的值(两个公式)
  3. 使用限定符适配不同dpi的屏幕,不同限定符下dp值的计算
  4. android开发中使用的dpi的值是不固定的,可以修改的
  5. 获取手机屏幕信息的DisplayMetrics类和修改屏幕参数的wm命令

五、drawable下的图片适配

这里介绍drawable下的图片资源的读取和缩放的规则,也可以这样说,如何使用一套图片资源适配多种dpi。郭神的一篇博客Android drawable微技巧,你所不知道的drawable的那些细节讲的很清晰,这里取其精华做一个总结。

图片资源的读取规则

当我们使用资源id来去引用一张图片时,Android会使用一些规则来帮我们匹配最适合的图片。什么叫最适合的图片?比如我的手机屏幕密度是xxhdpi,那么drawable-xxhdpi文件夹下的图片就是最适合的图片。因此,当我引用一张图片时,如果drawable-xxhdpi文件夹下有这张图就会优先被使用,在这种情况下,图片是不会被缩放的。但是,如果drawable-xxhdpi文件夹下没有这张图时, 系统就会自动去其它文件夹下找这张图了,优先会去更高密度的文件夹下找这张图片,也就是drawable-xxxhdpi文件夹,然后发现这里也没有android_logo这张图,接下来会尝试再找更高密度的文件夹,发现没有更高密度的了,这个时候会去drawable-nodpi文件夹找这张图,发现也没有,那么就会去更低密度的文件夹下面找,依次是drawable-xhdpi -> drawable-hdpi -> drawable-mdpi -> drawable-ldpi。

android项目资源下的drawable(不带任何限定符)目录默认就是drawable-mdpi的意思

作者:郭霖
链接:http://blog.csdn.net/guolin_blog/article/details/50727753

图片资源的缩放规则

根据上面的读取规则,如果最终没有读取到最适合的图片,而是读取了低密度或高密度的图片,那么系统会自动做一个缩放操作(低密度的图片放大,高密度的图片缩小)。具体的缩放比例就是dpi的比例:

ldpi mdpi hdpi xhdpi xxhdpi
dpi 120 160 240 320 480
比例 3 4 6 8 12

比如当前手机dpi是160(对应mdpi)
如果读取了drawable-mdpi下的图片大小是48x48,那么显示到屏幕上的图片大小是48x48(最适合的图片不缩放)
如果读取了drawable-ldpi下的图片大小是48x48,那么显示到屏幕上的图片大小是36x36(48/36 = 4/3)
如果读取的是drawable-xhdpi下的图大小是48x48,那么显示到屏幕上的图片大小是96x96。

一套图片资源适配多种dpi

根据Android的开发建议,我们在准备图片资源时尽量应该给每种密度的设备都准备一套,这样程序的适配性就可以达到最好,比如AndroidStudio中新建项目的时候,AS会默认给我们生成不同规格的ic_launcher.png作为默认的app图标,如下:

  • mipmap-mdpi/ic_launcher.png (48x48)
  • mipmap-hdpi/ic_launcher.png (72x72)
  • mipmap-xhdpi/ic_launcher.png (96x96)
  • mipmap-xxhdpi/ic_launcher.png (144x144)

括号中是图片大小,mipmap看成drawable即可。

上面的4个资源,通过计算可知,完全符合前面的图片资源的缩放规则的比例关系,所以,上面的资源只保留一个的话同样可以适配另外3种dpi。
类似的,项目中,UI设计师只需要给我们提供一种dpi下的一套图片即可适配所有dpi,原理就是图片资源的读取缩放规则。那么,我们希望UI给我们哪种dpi的图片呢?当然是高dpi下的图片了,因为高dpi目录下的图片,显示到低dpi的设备下,由缩放规则可以知道图片会被缩小。相反的,低dpi图片显示到高dpi的设备上,图片会被放大。图片缩小几乎没有什么副作用,而放大可能会影响图片质量,所以,使用高dpi目录下的图片适配效果更好,比如ic_launcher.png只保留一个的话,选择保留 mipmap-xxhdpi/ic_launcher.png (144x144)。当然,也不是越高越好,比如当前手机市场有更高密度的手机xxxhdpi,但那是极少数,为了这极少数增加软件包的大小是不划算的。当然如果以后xxxhdpi的手机普及了,针对这种级别的屏幕密度来设计图片就是首选了。

其他问题
  1. 内存方面,使用xxhdpi规格的图片适配和使用mdpi规格的图片适配,理论上它们对内存的占用最终是相同的。比如上面ic_launcher.png的例子,如果选择使用mdpi适配,那么原图大小是48x48,显示到xxhdpi设备上之后,图片被放大到144x144。如果使用xxhdpi适配,那么原图大小就是144x144,显示到xxhdpi设备上不会缩放,所以两种适配方法,最终显示到xxhdpi设备上的大小是一样的,占用内存也一样。
  2. 需要注意的是,使用一套图片做适配,在某些情况下可能会有一些问题,看:Android 开发中 drawable 有必要放多套分辨率的图片资源吗?
  3. wm命令对于测试图片并不好用,比如480dpi(对应xxhdpi)的手机,如果使用wm命令将手机dpi改为160(对应mdpi),那么这时候依然会优先读取xxhdpi下的图片资源,我试了两个不同厂商的手机都是这样。

参考文章:
Android中px dpi dip density densityDpi 的相关说明
DPI、PPI、DP、PX 的详细计算方法及算法来源是什么?
ANDROID 屏幕适配
Android 适配时资源限定符的说明
Android drawable微技巧,你所不知道的drawable的那些细节