作Android,必定会接触到屏幕适配,而屏幕适配的方案也是有多种多样,这个话题一直没有中止,最近也是想再研究一下适配的多种方式。 先放一个表格html
密度类型 | 表明的分辨率(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的意思就是1dp等于几个px像素点 好比density=3,意思是1dp=3pxjava
dp、dip、dpi、ppi 这四个是新手容易混淆的,其中dp和dip是同样的概念,这个是android特有的一种逻辑单位,和具体设备的物理像素无关。 而dpi和ppi是同样的概念,这个是一平方英寸里有多少个像素点的意思。android
无论你在布局文件中填写的是什么单位,最后都会被转化为 pxios
好比常使用的:RelativeLayout布局、LinearLayout布局、weight、.9.png、svg图片、ViewStub、include、merge 不长使用的,好比app须要自动适配手机和平板时用到的:布局别名、smallestWidth限定符 详情可看这里:www.jianshu.com/p/ec5a1a306…git
根据以上疑问,咱们来一一解答github
不少时候有经验的设计人员给咱们的原型里,已经有了dp值,可是有些设计新人并不知道如何在原型里标注多大的dp值,并且不少时候设计都是按照ios的分辨率来切图的,咱们先说按照android标准尺寸切图的状况,假设咱们使用1920*1080分辨率的底图bash
使用1920*1080分辨率做为底图设计切图后,咱们尽可能把切图放在高dpi文件夹里(设计底图分辨率不要过低,如1920*1080就比较清晰),不然放在低dpi文件夹里的话,若是app安装在高dpi的手机设备里,图片会拉伸,可能会模糊,如今通常至少1920*1080分辨率,这个分辨率的谷歌标准dpi是480,也就是xxhdpiapp
3.1. 若是咱们将切图放到了res/xxhdpi下面,根据谷歌设计规范这个dpi的density是3,若是1080px*1920px分辨率的底图中有一个图片是540px*960px(在density等于3时等同于180dp*320dp),那么这个图片使用warp_content的话在1080px*1920px(在density等于3时等同于360dp*640dp)分辨率而且density等于3的设备上显示时宽度正好是屏幕宽度的一半(比较dp的话是540px/3density=180dp是360dp的一半,比较px的话是540px是1080px的一半)框架
3.2. 若是咱们将切图位置不变,仍然放到了res/xxhdpi(density=3)下面,那么该540px*960px分辨率的图片在xhdpi(density=2)的设备里使用warp_content自适应时的分辨率和dp是多少呢?iphone
3.3. 若是咱们将刚才540px*960px的切图放到res/xhdpi下面的话(xhdpi的density是2,因此等同于270dp*480dp),那么这个图片使用warp_content的话在xhdpi(density=2时xhdpi是360dp宽640dp高)的设备上显示时这个图片宽度要大于屏幕宽度的一半,不信的能够试一下,我试过了没错(比较dp的话是270dp是360dp的0.75倍,比较px的话是540px是720px的0.75倍)
备注:以上计算方式必须知道设备的dpi或者density其中一个,不然没法计算。(density=dpi/160)
假如知道某个图片在某个dpi文件夹里的warp_content时的px值,想知道这个图片放在其余dpi文件夹里的warp_content时的px值,能够经过 px/当前dpi设备的density 获得这个图片在当前dpi下面的dp值,而后根据该 dp值*其余dpi设备的density 获得该图片放在其余dpi设备里的px值。
因此一个新项目咱们可让设计按照某个谷歌标准分辨率作底图,而后根据上面的规则咱们就知道图中对应的px在某个dpi文件夹里是多少dp。
简单说,就是穷举市面上全部的Android手机的宽高像素值,而后建立一批不一样分辨率下的dimen文件,其中值的单位是px:
设定一个基准的分辨率,其余分辨率都根据这个基准分辨率来计算,在不一样的尺寸文件夹内部,根据该尺寸编写对应的dimens文件。
好比以480x320为基准分辨率
宽度为320,将任何分辨率的宽度整分为320份,取值为x1-x320
高度为480,将任何分辨率的高度整分为480份,取值为y1-y480
那么对于480*800的分辨率的dimens文件来讲,
x1=(480/320)*1=1.5px
x2=(480/320)*2=3px
...
这个时候,咱们用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就极可能变形,简单说,就是容错机制不好。
不过这个方案有一些团队用过,咱们能够认为它是一个比较成熟有效的方案了。
这个和上面的区别是穷举市面上全部手机的dp值,dimen的单位是dp,该方法解决了上面方法1的缺点,即便某个dp没有覆盖,系统也会寻找小于或等于该dp的文件,而后用该文件适配。这种机制和上文提到的宽高限定符适配原理上是同样的,都是系统经过特定的规则来选择对应的文件。
这种适配方式的dimen文件的生成的规则和上面同样,也是先设置一个基准dp,由于系统会根据当前设备的最小dp去选择文件夹,因此咱们把设计图的px当成dp做为基准dp就能够了,举个例子,咱们的UI设计图是640px*960px,咱们把它当成640dp*960dp,而后咱们建立基准dp文件夹values-sw640dp,咱们能够从1建立到分辨率的最大值,好比x1=1dp,x640=640dp,x960=960dp,而后咱们能够其余swdp文件夹,好比建立values-sw480dp文件夹,480/640=0.75,因此x1=0.75dp,以此类推x640*0.75=480dp,x960*0.75=720dp,看到了吧,咱们直接用UI设计图的分辨率做为基准分辨率便可,而后使用的时候设计图中的10px咱们用x10就能够了,很方便。一样的咱们也能够建立layout-swdp,在不一样dpi的设备里系统会自动选择对应的layout
思考一下:若是一个相同name的dimen在values-分辨率文件夹和values-swdp文件夹里都定义了的时候系统会使用哪一个值?我测试了一下,系统会优先使用values-swdp文件夹里的值。
这个方案的缺点:
在布局中引用 dimens 的方式,虽然学习成本低,可是在平常维护修改时较麻烦
侵入性高,若是项目想切换为其余屏幕适配方案,由于每一个 Layout 文件中都存在有大量 dimens 的引用,这时修改起来工做量很是巨大,切换成本很是高昂
没法覆盖所有机型,想覆盖更多机型的作法就是生成更多的资源文件,但这样会增长 App 体积,在没有覆盖的机型上还会出现必定的偏差,因此有时须要在适配效果和占用空间上作一些抉择
若是想使用 sp,也须要生成一系列的 dimens,致使再次增长 App 的体积
不能自动支持横竖屏切换时的适配,如上文所说,若是想自动支持横竖屏切换时的适配,须要使用 values-wdp 或 屏幕方向限定符 再生成一套资源文件,这样又会再次增长 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底图也就是$$\cfrac{Wpx}{1080px}=\cfrac{Wdp}{360dp}$$而后计算获得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 的适配效果,改用其余的适配方案
该方案的补充与扩展: juejin.im/post/5b7faf…
至此完成本篇文章,可能有的地方有些啰嗦,我是想尽量讲的详细一些,我把jess中的文章有些不清楚的地方我添加了一些解释
其余一些适配方式:
使用鸿洋大神的软件生成大量px文件放到项目中 这种方式,是根据dp、density、px换算出来一堆px文件,分辨对应不一样分辨率的手机,能解决大部分的适配问题,可是若是遇到分辨率比较高可是屏幕尺寸比较大的时候,这个设备的dpi会比较低,而后就会有些问题,并且这样也会有一大堆"values-宽X高"文件夹,里面有一大堆的px文件,增长apk体积。这时候应该能够再建立相似values-160dpi这种文件夹来解决,并且能够建立values-160dpi-1024x600这种文件夹,这些文件夹能够混用,优先用更精确的那个。
使用鸿洋大神的AutoLayout框架 这个方式也不错,里面的源码我还没仔细看,咱们公司也用的这种方式,不过有些控件会有问题,针对这些控件鸿洋有一些重写,不过没有的就须要本身去写了
约束布局(ConstraintLayout),这个我还没怎么去了解,待这几天看看研究一下再来完善该篇文章,在这篇文档里:developer.android.com/reference/a… ,谷歌明确表示废弃了百分比布局库,而应该使用约束布局。
使用pt物理单位 源连接: www.apkbus.com/blog-177177… 涉及到的代码: github.com/Firedamp/Ru… 在上面这个连接里有段代码是
resources.getDisplayMetrics().xdpi = size.x/designWidth*72f;
复制代码
在android系统里有个方法是这样的
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;
}
复制代码
value * metrics.xdpi * (1.0f/72) = 该控件实际显示的px值
能够换算为metrics.xdpi = 该控件实际显示的px值/value*72
,其中value
是该控件在设计图显示的pt值大小 而咱们要保证这个控件在任何分辨率下都相对于屏幕大小有固定的比例,只须要让 控件实际显示的px值/value=实际屏幕显示的px值/实际屏幕的pt值大小
,也就是resources.getDisplayMetrics().xdpi = size.x/designWidth*72f
参考文章较多,记录几个 www.jianshu.com/p/c772cf494… www.apkbus.com/blog-177177…