前提
- 什么是dpi、dp、sp、xp…
(1) android中的dp在渲染前会将dp转为px,计算公式:
px = density * dp;
density = dpi / 160;
px = dp * (dpi / 160);
而dpi是根据屏幕真实的分辨率和尺寸来计算的,每个设备都可能不一样的。
(2) 屏幕尺寸、分辨率、像素密度三者关系
通常情况下,一部手机的分辨率是宽x高,屏幕大小是以寸为单位,那么三者的关系是:
举个例子:屏幕分辨率为:1920*1080,屏幕尺寸为5吋的话,那么dpi为440。
- Android适配最核心的问题有两个,其一,就是适配的效率,即把设计图转化为App界面的过程是否高效,其二如何保证实现UI界面在不同尺寸和分辨率的手机中UI的一致性。
- Android设备系统及屏幕分辨率统计信息汇总(截至2018年7月)
屏幕分辨率限定符(宽高限定符)
缺点
- dimens.xml 文件所占的体积较大(可能超过2M)
但是这个方案有一个致命的缺陷,那就是需要精准命中才能适配,比如1920x1080的手机就一定要找到1920x1080的限定符,否则就只能用统一的默认的dimens文件了。而使用默认的尺寸的话,UI就很可能变形,简单说,就是容错机制很差。
经过我的测试,并不需要分辨率精确命中,系统会自动向下查找最接近的分辨率适配。
原理
屏幕分辨率限定符适配需要在 res 文件夹下创建各种屏幕分辨率对应的 values-xxx 文件夹,如下图:
然后根据一个基准分辨率,例如基准分辨率为 1280x720,将宽度分成 720 份,取值为 1px~720px,将高度分成 1280 份,取值为 1px~1280px,生成各种分辨率对应的 dimens.xml 文件。如下分别为分辨率 1280x720 与 1920x1080 所对应的横向dimens.xml 文件:
假设设计图上的一个控件的宽度为 720px,那么布局中就写 android:layout_width="@dimen/x720" ,当运行程序的时候,系统会根据设备的分辨率去寻找对应的 dimens.xml 文件。例如运行在分辨率为 1280x720 的设备上,系统会自动找到对应的 values-1280x720 文件夹下的 lay_x.xml 文件,由上图可知 x720 对应的值为
720.px,可铺满该屏幕宽度。运行在分辨率为 1920x1080 的设备上,系统会自动找到对应的 values-1920x1080 文件夹下的 lay_x.xml 文件,由上图可知 x720 对应的值为 1080.0px,可铺满该屏幕宽度。这样就达到了屏幕适配的要求!
smallestWidth(最小宽度) 限定符
优点
- 只需要少量 dimens.xml 文件即可达到适配
- 不用考虑虚拟按键的问题
缺点
- 因为以dp为基础,需要UI单独设计
- 占用一定的体积,apk可能会增大300kb-800kb左右
- Android 3.2 之后引入的,不过现在应该都不存在这个问题了
原理
smallestWidth 限定符适配原理与屏幕分辨率限定符适配原理一样,系统都是根据限定符去寻找对应的 dimens.xml 文件。例如程序运行在最小宽度为 360dp 的设备上,系统会自动找到对应的 values-sw360dp 文件夹下的 dimens.xml 文件。区别就在于屏幕分辨率限定符适配是拿 px 值等比例缩放,而 smallestWidth 限定符适配是拿 dp值来等比缩放 而已。需要注意的是“最小宽度”是不区分方向的,即无论是宽度还是高度,哪一边小就认为哪一边是“最小宽度”。如下分别为最小宽度为 360dp 与最小宽度为 640dp 所对应的 dimens.xml 文件:
使用方法
使用工具https://github.com/ladingwu/dimens_sw可以生产相应的dimens文件。
在DimenGenerator文件中设置设计的宽高,一般设计都是按照像素来设计,我们也可以直接设置像素值,这样就可以无脑的使用了,不用再计算。
今日头条方案(修改density)
优点
- 使用成本非常低,操作非常简单,使用该方案后在页面布局时不需要额外的代码和操作,这点可以说完虐其他屏幕适配方案
- 侵入性非常低,该方案和项目完全解耦,在项目布局时不会依赖哪怕一行该方案的代码,而且使用的还是 Android 官方的 API,意味着当你遇到什么问题无法解决,想切换为其他屏幕适配方案时,基本不需要更改之前的代码,整个切换过程几乎在瞬间完成,会少很多麻烦,节约很多时间,试错成本接近于 0
- 可适配三方库的控件和系统的控件(不止是是 Activity 和 Fragment,Dialog、Toast 等所有系统控件都可以适配),由于修改的 density 在整个项目中是全局的,所以只要一次修改,项目中的所有地方都会受益
- 不会有任何性能的损耗
缺点
- 缺点也是上面的优点之一,因为修改density是针对整个app的,会影响第三方布局,这个缺点是非常致命的,并没有完美的解决方案,缺点解决方案
原理
如果看了上面今日头条的那篇适配文章,那么你可能已经知道其原理了,不明白的话可以继续看下我的解释:
我们知道 px = dp * density,我们要适配的话需要确保 dp 不变去修改 density,而安卓默认 density = dpi / 160,其意思就是 1dp 有多少 px,也就是像素密度,我们开发是按照一份设计稿来做的,那么有没有什么办法来让 density 和设计稿尺寸做联系呢?假设我们设计稿是宽度是 1080px,资源放在 xxhdpi,那么我们宽度转换为 dp 就是 1080 / 3 = 360dp,要在不同设备上宽度都表现为 360dp,那么就需要修改其 density = screenWidthPx / 360,这样就满足了上述条件,而和 density 相关的还有 densityDpi、scaledDensity,我们根据 density 等比修改 densityDpi、scaledDensity 即可。
由于 API 26 及以上的 Activity#getResources()#getDisplayMetrics() 和 Application#getResources()#getDisplayMetrics() 是不同的引用,所以在 API 26 及以上适配是没有影响的,但在 API 26 以下 Activity#getResources()#getDisplayMetrics() 和 Application#getResources()#getDisplayMetrics() 是相同的引用,导致适配有问题。
这样会导致获取状态栏和导航栏高度有问题,其获取状态栏高度代码为如下所示:
public static int getStatusBarHeight() {
Resources resources = Utils.getApp().getResources();
int resourceId = resources.getIdentifier("status_bar_height", "dimen", "android");
return resources.getDimensionPixelSize(resourceId);
}
复制代码由于使用的是 Application#getResources,这会导致最后计算状态栏高度使用的是修改过后的 density,在这里也要感谢 @magic0908 无意间提到的 Resources.getSystem() 来获取系统的 Resources,果不其然可以获取到正确高度的状态栏高度,代码如下所示:
public static int getStatusBarHeight() {
Resources resources = Resources.getSystem();
int resourceId = resources.getIdentifier("status_bar_height", "dimen", "android");
return resources.getDimensionPixelSize(resourceId);
}
复制代码同理获取导航栏高度也可以这样。
今日头条屏幕适配方案的核心原理在于,根据以下公式算出 density
当前设备屏幕总宽度(单位为像素)/ 设计图总宽度(单位为 dp) = density
density 的意思就是 1 dp 占当前设备多少像素
为什么要算出 density,这和屏幕适配有什么关系呢?
public static float applyDimension(int unit, float value,
DisplayMetrics metrics)
{
switch (unit) {
case COMPLEX_UNIT_PX:
return value;
case COMPLEX_UNIT_DIP:
return value * metrics.density;
case COMPLEX_UNIT_SP:
return value * metrics.scaledDensity;
case COMPLEX_UNIT_PT:
return value * metrics.xdpi * (1.0f/72);
case COMPLEX_UNIT_IN:
return value * metrics.xdpi;
case COMPLEX_UNIT_MM:
return value * metrics.xdpi * (1.0f/25.4f);
}
return 0;
}
复制代码大家都知道,不管你在布局文件中填写的是什么单位,最后都会被转化为 px,系统就是通过上面的方法,将你在项目中任何地方填写的单位都转换为 px 的
所以我们常用的 px 转 dp 的公式 dp = px / density,就是根据上面的方法得来的,density 在公式的运算中扮演着至关重要的一步
比较
是否考虑虚拟按键 | 占用体积 | 是否影响第三方控件 | |
---|---|---|---|
屏幕分辨率限定符 | √ | 较大 | × |
最小宽度限定符 | × | 较小 | × |
今日头条 | × | 不占用 | √ |
测试
在android studio中release环境下编译一个空的apk包:
方案 | 体积 |
---|---|
无 | 1.37MB |
屏幕分辨率限定符 | 1.95MB |
最小宽度 | 1.7MB |
今日头条 | 1.37MB |
总结
最终我选择最小宽度限定符方案,因为该方案无副作用,相比之下,占用体积较小。
参考
一种非常好用的Android屏幕适配
Android 屏幕适配从未如斯简单
Android 目前稳定高效的UI适配方案
骚年你的屏幕适配方式该升级了!-今日头条适配方案