做Android,一定会接触到屏幕适配,而屏幕适配的方案也是有多种多样,这个话题一直没有停止,最近也是想再研究一下适配的多种方式。
先放一个表格
密度类型 | 代表的分辨率(px) | 屏幕密度(dpi) | density | 换算(px/dp) | 比例 |
---|---|---|---|---|---|
低密度(ldpi) | 240x320 | 120 | 1dp=0.75px | 0.75 | 3 |
中密度(mdpi) | 320x480 | 160 | 1dp=1px | 1 | 4 |
高密度(hdpi) | 480x800 | 240 | 1dp=1.5px | 1.5 | 6 |
超高密度(xhdpi) | 720x1280 | 320 | 1dp=2px | 2 | 8 |
超超高密度(xxhdpi) | 1080x1920 | 480 | 1dp=3px | 3 | 12 |
再放几个公式
-
计算屏幕对角线英寸
-
计算density和dpi
-
计算dp和px的关系
一些概念:
-
density的意思就是1dp等于几个px像素点
比如density=3,意思是1dp=3px -
dp、dip、dpi、ppi
这四个是新手容易混淆的,其中dp和dip是一样的概念,这个是android特有的一种逻辑单位,和具体设备的物理像素无关。
而dpi和ppi是一样的概念,这个是一平方英寸里有多少个像素点的意思。 -
不管你在布局文件中填写的是什么单位,最后都会被转化为 px
先提出以下疑问,根据疑问来学习
- 如果要开发一个新的App,设计人员的底图应该多大?
- 切图后,把图片放到res下面的哪个文件夹?
- 如果使用warp_content,那么这个大小是多少dp?
根据以上疑问,我们来一一解答
-
很多时候有经验的设计人员给我们的原型里,已经有了dp值,但是有些设计新人并不知道如何在原型里标注多大的dp值,而且很多时候设计都是按照ios的分辨率来切图的,我们先说按照android标准尺寸切图的情况,假设我们使用1920*1080分辨率的底图
-
设计切图后,我们尽量放在高dpi文件夹里(设计底图分辨率不要太低,如1920*1080就比较清晰),否则放在低dpi文件夹里的话,如果app安装在高dpi的手机设备里,图片会拉伸,可能会模糊。现在一般至少1920*1080分辨率,这个分辨率的谷歌标准dpi是480,也就是xxhdpi
3.1. 如果我们将切图放到了res/xxhdpi下面,根据谷歌设计规范这个dpi的density是3,如果1920px*1080px分辨率的底图中有一个图片是960px*540px,那么这个图片使用warp_content的话实际宽高分别是180dp和320dp(180=540/density,320=960/density)。又因为xxhdpi的谷歌标准分辨率是1920px*1080px,根据density等于3可以计算出xxhdpi的全屏幕宽高相当于360dp和640dp(1080/3和1920/3),所以这个图片宽度正好是屏幕宽度的一半
3.2. 如果我们刚才将切图放到res/xhdpi下面的话,这个dpi的density是2,所以这个图片使用warp_content的话实际宽高分别是270dp和480dp(270=540/density,480=960/density)。又因为xhdpi的谷歌标准分辨率是720px*1280px,根据density等于2可以计算出xhdpi的宽高相当于360dp和640dp。而这个图片宽高是270dp和480dp,所以这个图片宽度要大于屏幕宽度的一半,不信的可以试一下,我试过了是这样的
根据上面这些内容可以总结得到以下结论:
- 如果设计人员给的底图使用某个谷歌标准分辨率,比如1920*1080,根据最开始的表格可以看到这个分辨率对应的res文件夹是xxhdpi,如果把切图放到这个文件夹里,那么图片自适应的宽高和设计图是一样的,如果其中需要指定大小,可以根据公式dp=px/density来得到。也就是说如果底图是谷歌标准分辨率,把底图或者切图放到对应的res文件夹里,视觉效果和设计图是一样的。
但是,比如在1920*1080底图中有一个切图是1920*540,那么放到xxhdpi里,在xml使用warp_content和使用180dp(540/density计算的来),都是占用标准xxhdpi模拟器的一半宽(180dp*3density=540px,540px是1080px的一半;或者直接看dp,180dp是360dp的一半)。但是如果换一个模拟器(图片仍然在xxhdpi文件夹下),换的模拟器不一定是360dp宽,那么这个切图就不一定是占据模拟器一半的宽度了。在420dpi的模拟器里,这个模拟器的density是420/160=2.625,所以这个模拟器的宽约等于411dp,这个图片占据屏幕的180/411这么宽,看起来小于一半(或者看分辨率,180dp*2.625density=472.5px,472.5px/1080px小于一半)。而如果在320dp的模拟器里显示的话(标准hdpi就是320dp宽),这个图片占据屏幕的180/320这么宽,看起来大于一半。
备注:以上计算方式必须知道设备的dpi或者density其中一个,否则无法计算。(density=dpi/160)
- 假如我们知道某个图片在当前dpi文件夹里的warp_content时的dp值,想知道这个图片放在其他dpi文件夹里的warp_content时的dp值,需要先根据当前图片的 dp*当前dpi设备的density 得到该图片占用的物理像素px值,然后根据该px值/其他dpi设备的density得到该图片在其他dpi设备里的dp值。
公式可以简化为
其他dpi的density/当前dpi的density*某个图片在当前dpi设备里的dp值=某个图片在其他dpi设备里的dp值
- 假如知道某个图片在某个dpi文件夹里的warp_content时的px值,想知道这个图片放在其他dpi文件夹里的warp_content时的px值,可以通过 px/当前dpi设备的density 得到这个图片在当前dpi下面的dp值,然后根据该 dp值*其他dpi设备的density 得到该图片放在其他dpi设备里的px值。
公式可以简化为
其他dpi设备的density/当前dpi设备的density*该图片在当前dpi设备里的px值=该图片在其他dpi设备里的px值
2. 如果设计人员给的底图不是某个谷歌标准分辨率,比如是用的ios的某个机型设计的底图,我们可以根据这个机型找接近这个机型dpi值的res文件夹下的某个dpi文件夹,然后其中有间距也是用dp=px/density得到,其中density就是接近这个ios机型dpi的某个dpi文件夹对应的density的值,比如xxhdpi对应的density就是3,当然这种情况肯定会有点像素上的误差,不过因为间距之类的都比较小,看着不明显。
所以一个新项目我们可以让设计按照某个谷歌标准分辨率做底图,然后根据上面的规则我们就知道图中对应的px在某个dpi文件夹里是多少dp。
但是!让设计人员使用谷歌标准分辨率做底图只是理想情况,很多时候设计人员的底图分辨率并不是谷歌标准分辨率,比如设计人员只有一套以某个ios机型为底图的设计图,如果把其中的切图像上面所说放入接近的dpi的文件夹里,然后把程序安装在模拟器或者手机里运行,实际肯定有一些偏差的,我们能不能把这些偏差解决呢?
所以,有这样一些方案,我们来一个个的说
-
先说这个穷举分辨率适配法,简单说,就是穷举市面上所有的Android手机的宽高像素值,然后创建一批不同分辨率下的dimen文件,其中值的单位是px:
image.png
设定一个基准的分辨率,其他分辨率都根据这个基准分辨率来计算,在不同的尺寸文件夹内部,根据该尺寸编写对应的dimens文件。
比如以480x320为基准分辨率
-
宽度为320,将任何分辨率的宽度整分为320份,取值为x1-x320
-
高度为480,将任何分辨率的高度整分为480份,取值为y1-y480
那么对于480*800的分辨率的dimens文件来说,
x1=(480/320)*1=1.5px
x2=(480/320)*2=3px
...
image.png
这个时候,我们用UI设计界面作为基准分辨率,比如UI设计界面是640px*960px,然后我们创建values-640x960,然后创建一堆dimen值,分别是x1-x640,值从1px-640px,如果我们要使用1000px怎么办呢?我们可以将dimen的范围写大一些也可以的,只要比例一样就行。
然后我们可以根据这个基准分辨率创建其他分辨率的文件,比如创建
values-480x800,x1就是480/640=0.75px,其他值根据此比例来生成。
当APP运行在不同分辨率的手机中时,这些系统会根据这些dimens引用去该分辨率的文件夹下面寻找对应的值。这样基本解决了我们的适配问题,而且极大的提升了我们UI开发的效率。
但是这个方案有一个致命的缺陷,那就是需要精准命中才能适配,比如1920*1080的手机就一定要找到1920*1080的限定符,否则就只能用统一的默认的dimens文件了。而使用默认的尺寸的话,UI就很可能变形,简单说,就是容错机制很差。
不过这个方案有一些团队用过,我们可以认为它是一个比较成熟有效的方案了。
- smallestWidth适配,或者叫sw限定符适配。这个和上面的区别是穷举市面上所有手机的dp值,dimen的单位是dp,该方法解决了上面方法1的缺点,即使某个dp没有覆盖,系统也会寻找小于或等于该dp的文件,然后用该文件适配。这种机制和上文提到的宽高限定符适配原理上是一样的,都是系统通过特定的规则来选择对应的文件。
这种适配方式的dimen文件的生成的规则和上面一样,也是先设置一个基准dp,而这个基准dp其实直接使用UI设计图的px值就可以,举个例子,我们的UI设计图是640px*960px,然后我们创建基准dp文件夹values-sw640dp,当然里面的dimen设置的范围可以超过640份,比如x1=1dp,然后我们创建sw480dp文件夹,480/640=0.75,所以x1=0.75dp,以此类推x640=480dp,看到了吧,我们直接用UI设计图的分辨率作为基准分辨率即可,然后使用的时候设计图中的10px我们用x10就可以了,很方便。
思考一下:如果一个相同name的dimen在values-分辨率文件夹和values-swdp文件夹里都定义了的时候系统会使用哪个值?我测试了一下,系统会优先使用values-swdp文件夹里的值。
而这个方法是根据你要适配哪种dpi来生成,比如我们要用xxhdpi来做基准适配,xxhdpi的density是3,宽度是1080/3=360dp,所以我们创建values-sw360dp文件夹,然后创建一些dimen值,值的范围是x1-x360,和方法1一样我们可以将dimen的范围写大一些也可以的,只要比例一样就行。然后x1=1dp,x2=2dp,其他值以此类推,然后我们怎么创建其他文件夹下面的dimen值呢?我们用values-sw540dp文件夹来举例,我们创建的x1=540/360=1.5dp,其他值以此类推。
这个方案的缺点:
-
在布局中引用 dimens 的方式,虽然学习成本低,但是在日常维护修改时较麻烦
-
侵入性高,如果项目想切换为其他屏幕适配方案,因为每个 Layout 文件中都存在有大量 dimens 的引用,这时修改起来工作量非常巨大,切换成本非常高昂
-
无法覆盖全部机型,想覆盖更多机型的做法就是生成更多的资源文件,但这样会增加 App 体积,在没有覆盖的机型上还会出现一定的误差,所以有时需要在适配效果和占用空间上做一些抉择
-
如果想使用 sp,也需要生成一系列的 dimens,导致再次增加 App 的体积
-
不能自动支持横竖屏切换时的适配,如上文所说,如果想自动支持横竖屏切换时的适配,需要使用 values-w<N>dp 或 屏幕方向限定符 再生成一套资源文件,这样又会再次增加 App 的体积
- 美团的这个,我们强制将density修改为谷歌标准值,也就相当于我们强制把设计人员给的图片转为谷歌某个标准分辨率,这样我们上面的计算方法就有效了,通过测试某个切图发现不同dp宽度的模拟器中该图片在屏幕的比例都是一致的。但是修改了系统的density值之后,整个布局的实际尺寸都会发生改变,如果想要在老项目文件中使用,恐怕整个布局文件中的尺寸都可能要重新按照设计稿修改一遍才行。因此,如果你是在维护或者改造老项目,使用这套方案就要三思了。
我先发一下美团方式的计算公式
public class Test {
private static float sNonCompatDensity;
private static float sNonCompatScaledDensity;
public static void setCustomDensity(@NonNull Activity activity, @NonNull final Application application, final int designWidthDp) {
final DisplayMetrics appDisplayMetrics = application.getResources().getDisplayMetrics();
if (sNonCompatDensity == 0) {
sNonCompatDensity = appDisplayMetrics.density;
sNonCompatScaledDensity = appDisplayMetrics.scaledDensity;
application.registerComponentCallbacks(new ComponentCallbacks() {
@Override
public void onConfigurationChanged(Configuration newConfig) {
if (newConfig != null && newConfig.fontScale > 0) {
sNonCompatScaledDensity = application.getResources().getDisplayMetrics().scaledDensity;
}
}
@Override
public void onLowMemory() {
}
});
}
final float targetDensity = appDisplayMetrics.widthPixels / ((float) (designWidthDp));
final float targetScaledDensity = targetDensity * (sNonCompatScaledDensity / sNonCompatDensity);
final int targetDensityDpi = (int) (160 * targetDensity);
appDisplayMetrics.density = targetDensity;
appDisplayMetrics.scaledDensity = targetScaledDensity;
appDisplayMetrics.densityDpi = targetDensityDpi;
final DisplayMetrics activityDisplayMetrics = activity.getResources().getDisplayMetrics();
activityDisplayMetrics.density = targetDensity;
activityDisplayMetrics.scaledDensity = targetScaledDensity;
activityDisplayMetrics.densityDpi = targetDensityDpi;
}
}
美团这种方式如何使用呢,比如设计人员的底图分辨率是W*H(竖屏,并且W<H),如果你要适配1080*1920(xxhdpi)也就是说你想把切图放到xxhdpi的文件夹下面,而1080*1920的谷歌标准是360dp宽(1080/xxhdpi的density),所以Wpx相对于1080px底图也就是然后计算得到Wdp的值,然后把这个图片和切图放到xxhdpi下面,在Activity的onCreate方法的setContentView前面调用Test.setCustomDensity(this, application,Wdp)
,然后显示效果是和设计图一样的
举个例子,比如设计人员的底图是500*1000,其中有个切图是500*1000,然后如果你要适配1080*1920(xxhdpi),而1080*1920的谷歌标准是360dp宽(1080/xxhdpi的density),所以500px相对于1080px底图也就是500/1080*360=167dp,然后把这个图片放到xxhdpi下面,然后调用Test.setCustomDensity(this, application,167)
,这样就会在任何模拟器里这个图片看起来宽度都是正好占满屏幕宽度。
而如果要适配720*1280(xhpdi),而720*1280的谷歌标准是360dp宽(720/xhdpi的density),所以500px相对于1080px底图也就是500/1080*360=167dp,然后把这个图片放到xhpdi下面,然后调用Test.setCustomDensity(this, application,167)
而如果要适配480*800(hpdi),而480*800的谷歌标准是320dp宽(480/hdpi的density),所以500px相对于480px底图也就是500/480*320=333dp,然后把这个图片放到hpdi下面,然后调用Test.setCustomDensity(this, application,333)
但是这个方式也有缺点:
这个方案依赖于设计图尺寸,但是项目中的系统控件、三方库控件、等非我们项目自身设计的控件,它们的设计图尺寸并不会和我们项目自身的设计图尺寸一样
当这个适配方案不分类型,将所有控件都强行使用我们项目自身的设计图尺寸进行适配时,这时就会出现问题,当某个系统控件或三方库控件的设计图尺寸和和我们项目自身的设计图尺寸差距非常大时,这个问题就越严重。
解决方案有两种:
方案 1
调整设计图尺寸,因为三方库可能是远程依赖的,无法修改源码,也就无法让三方库来适应我们项目的设计图尺寸,所以只有我们自身作出修改,去适应三方库的设计图尺寸,我们将项目自身的设计图尺寸修改为这个三方库的设计图尺寸,就能完成项目自身和三方库的适配
这时项目的设计图尺寸修改了,所以项目布局文件中的 dp 值,也应该按照修改的设计图尺寸,按比例增减,保持与之前设计图中的比例不变
但是如果为了适配一个三方库修改整个项目的设计图尺寸,是非常不值得的,所以这个方案支持以 Activity 为单位修改设计图尺寸,相当于每个 Activity 都可以自定义设计图尺寸,因为有些 Activity 不会使用三方库 View,也就不需要自定义尺寸,所以每个 Activity 都有控制权的话,这也是最灵活的
但这也有个问题,当一个 Activity 使用了多个设计图尺寸不一样的三方库 View,就会同样出现上面的问题,这也就只有把设计图改为与几个三方库比较折中的尺寸,才能勉强缓解这个问题
方案 2
第二个方案是最简单的,也是按 Activity 为单位,取消当前 Activity 的适配效果,改用其他的适配方案
所以有人修改了该种方式,如今日头条的方案
参考链接:https://juejin.im/post/5bce688e6fb9a05cf715d1c2
至此完成本篇文章,可能有的地方有些啰嗦,我是想尽可能讲的详细一些,我把jess中的文章有些不清楚的地方我添加了一些解释
网友评论