Android屏幕适配最优方案
一、今日头条屏幕适配方案
今日头条屏幕适配方案的核心原理在于,根据以下公式算出 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在公式的运算中扮演着至关重要的一步
要看懂下面的内容,还得明白,今日头条的适配方式,今日头条适配方案默认项目中只能以高或宽中的一个作为基准,进行适配,为什么不像 AndroidAutoLayout一样,高以高为基准,宽以宽为基准,同时进行适配呢
这就引出了一个现在比较棘手的问题,大部分市面上的 Android设备的屏幕高宽比都不一致,特别是现在大量全面屏的问世,这个问题更加严重,不同厂商推出的全面屏手机的屏幕高宽比都可能不一致
这时我们只以高或宽其中的一个作为基准进行适配,就会有效的避免布局在高宽比不一致的屏幕上出现变形的问题
明白这个后,我再来说说 density,density在每个设备上都是固定的,DPI / 160 = density,屏幕的总 px 宽度 / density = 屏幕的总 dp 宽度
-
设备 1,屏幕宽度为 1080px,480DPI,屏幕总 dp宽度为 1080 / (480 / 160) = 360dp
-
设备 2,屏幕宽度为 1440,560DPI,屏幕总 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在任何分辨率的屏幕上,与屏幕的比例都是相同的
这时我们该怎么做呢?改变每个 View的 dp值?不现实,在每个设备上都要通过代码动态计算 View的 dp值,工作量太大
如果每个 View的 dp值是固定不变的,那我们只要保证每个设备的屏幕总 dp宽度不变,就能保证每个 View在所有分辨率的屏幕上与屏幕的比例都保持不变,从而完成等比例适配,并且这个屏幕总 dp宽度如果还能保证和设计图的宽度一致的话,那我们在布局时就可以直接按照设计图上的尺寸填写 dp值
屏幕的总 px 宽度 / density = 屏幕的总 dp 宽度
在这个公式中我们要保证 屏幕的总 dp 宽度和 设计图总宽度一致,并且在所有分辨率的屏幕上都保持不变,我们需要怎么做呢?屏幕的总 px 宽度每个设备都不一致,这个值是肯定会变化的,这时今日头条的公式就派上用场了
当前设备屏幕总宽度(单位为像素)/ 设计图总宽度(单位为 dp) = density
这个公式就是把上面公式中的 屏幕的总 dp 宽度换成 设计图总宽度,原理都是一样的,只要 density根据不同的设备进行实时计算并作出改变,就能保证 设计图总宽度不变,也就完成了适配
验证方案可行性
上面已经把原理分析的很清楚了,很多文章只是一笔带过这个公式,公式虽然很简单但我们还是想晓得这是怎么来的,所以我就反向推理了一遍,如果还是看不懂,那我只能说我尽力了,原理讲完了,那我们再来现场验证一下这个方案是否可行?
假设设计图总宽度为 375 dp,一个 View在这个设计图上的尺寸是 50dp * 50dp,这个 View的宽度占整个设计图宽度的 13.3%(50 / 375 = 0.133),那我们就来验证下在使用今日头条屏幕适配方案的情况下,这个 View与屏幕宽度的比例在分辨率不同的设备上是否还能保持和设计图中的比例一致
验证设备 1
屏幕总宽度为 1080 px,根据今日头条的的公式求出 density,1080 / 375 = 2.88 (density)
这个 50dp * 50dp的 View,系统最后会将高宽都换算成 px,50dp * 2.88 = 144 px(根据公式 dp * density = px)
144 / 1080 = 0.133,View实际宽度与 屏幕总宽度的比例和 View在设计图中的比例一致 (50 / 375 = 0.133),所以完成了等比例缩放
某些设备总宽度为 1080 px,但是 DPI可能不同,是否会对今日头条适配方案产生影响?其实这个方案根本没有根据 DPI求出 density,是根据自己的公式求出的 density,所以这对今日头条的方案没有影响
上面只能确定在所有屏幕总宽度为 1080 px的设备上能完成等比例适配,那我们再来试试其他分辨率的设备
验证设备 2
屏幕总宽度为 1440 px,根据今日头条的的公式求出 density,1440 / 375 = 3.84 (density)
这个 50dp * 50dp的 View,系统最后会将高宽都换算成 px,50dp * 3.84 = 192 px(根据公式 dp * density = px)
192 / 1440 = 0.133,View实际宽度与 屏幕总宽度的比例和 View在设计图中的比例一致 (50 / 375 = 0.133),所以也完成了等比例缩放
两个不同分辨率的设备都完成了等比例缩放,证明今日头条屏幕适配方案在不同分辨率的设备上都是有效的,如果大家还心存疑虑,可以再试试其他分辨率的设备,其实到最后得出的比例不会有任何偏差, 都是 0.133
优点
-
使用成本非常低,操作非常简单,使用该方案后在页面布局时不需要额外的代码和操作,这点可以说完虐其他屏幕适配方案
-
侵入性非常低,该方案和项目完全解耦,在项目布局时不会依赖哪怕一行该方案的代码,而且使用的还是 Android官方的 API,意味着当你遇到什么问题无法解决,想切换为其他屏幕适配方案时,基本不需要更改之前的代码,整个切换过程几乎在瞬间完成,会少很多麻烦,节约很多时间,试错成本接近于 0
-
可适配三方库的控件和系统的控件(不止是是 Activity和 Fragment,Dialog、Toast等所有系统控件都可以适配),由于修改的 density在整个项目中是全局的,所以只要一次修改,项目中的所有地方都会受益
-
不会有任何性能的损耗
缺点
暂时没发现其他什么很明显的缺点,已知的缺点有一个,那就是第三个优点,它既是这个方案的优点也同样是缺点,但是就这一个缺点也是非常致命的
只需要修改一次 density,项目中的所有地方都会自动适配,这个看似解放了双手,减少了很多操作,但是实际上反应了一个缺点,那就是只能一刀切的将整个项目进行适配,但适配范围是不可控的
这样不是很好吗?这样本来是很好的,但是应用到这个方案是就不好了,因为我上面的原理也分析了,这个方案依赖于设计图尺寸,但是项目中的系统控件、三方库控件、等非我们项目自身设计的控件,它们的设计图尺寸并不会和我们项目自身的设计图尺寸一样
当这个适配方案不分类型,将所有控件都强行使用我们项目自身的设计图尺寸进行适配时,这时就会出现问题,当某个系统控件或三方库控件的设计图尺寸和和我们项目自身的设计图尺寸差距非常大时,这个问题就越严重
举个栗子
假设一个三方库的 View,作者在设计时,把它设计为 100dp * 100dp,设计图的最大宽度为 1000dp,这个 View在设计图中的比例是 100 / 1000 = 0.1,意思是这个 View的宽度在设计图中占整个宽度的 10%,如果我们要完成等比例适配,那这个三方库 View在所有的设备上与屏幕的总宽度的比例,都必须保持在 10%
这时在一个使用今日头条屏幕适配方案的项目上,设置的设计图最大宽度如果是 1000dp,那这个三方库 View,与项目自身都可以完美的适配,但当我们项目自身的设计图最大宽度不是 1000dp,是 500dp时,100 / 500 = 0.2,可以看到,比例发生了较大的变化,从 10%上升为 20%,明显这个三方库 View高于作者的预期,比之前更大了
这就是两个设计图尺寸不一致导致的非常严重的问题,当两个设计图尺寸差距越大,那适配的效果也就天差万别了
解决方案
方案 1
调整设计图尺寸,因为三方库可能是远程依赖的,无法修改源码,也就无法让三方库来适应我们项目的设计图尺寸,所以只有我们自身作出修改,去适应三方库的设计图尺寸,我们将项目自身的设计图尺寸修改为这个三方库的设计图尺寸,就能完成项目自身和三方库的适配
这时项目的设计图尺寸修改了,所以项目布局文件中的 dp 值,也应该按照修改的设计图尺寸,按比例增减,保持与之前设计图中的比例不变
但是如果为了适配一个三方库修改整个项目的设计图尺寸,是非常不值得的,所以这个方案支持以 Activity为单位修改设计图尺寸,相当于每个 Activity都可以自定义设计图尺寸,因为有些 Activity不会使用三方库 View,也就不需要自定义尺寸,所以每个 Activity都有控制权的话,这也是最灵活的
但这也有个问题,当一个 Activity使用了多个设计图尺寸不一样的三方库 View,就会同样出现上面的问题,这也就只有把设计图改为与几个三方库比较折中的尺寸,才能勉强缓解这个问题
方案 2
第二个方案是最简单的,也是按 Activity为单位,取消当前 Activity的适配效果,改用其他的适配方案
使用中的问题
有些文章中提到了今日头条屏幕适配方案可以将设计图尺寸填写成以 px为单位的宽度和高度,这样我们在布局文件中,也就能直接填写设计图上标注的 px值,省掉了将 px换算为 dp的时间 (大部分公司的设计图都只标注 px值),而且照样能完美适配
但是我建议大家千万不要这样做,还是老老实实的以 dp为单位填写 dp值,为什么呢?
直接填写 px虽然刚开始布局的时候很爽,但是这个坑就已经埋上了,会让你后面很爽,有哪些坑?
第一个坑
这样无疑于使项目强耦合于这个方案,当你遇到无法解决的问题想切换为其他屏幕适配方案的时候,layout文件里曾经填写的 px值都会作为 dp
比如你的设计图实际宽度为 1080px,你不换算为 360dp (1080 / 3 = 360),却直接将 1080px作为这个方案的设计图尺寸,那你在 layout文件中,填写的也都是设计图上标注的 px值,但是单位却是 dp
一个在设计图上 300px * 300px的 View,你可以直接在 layout文件中填写为 300dp,而由于这个方案可以动态改变 density的原因还是可以做到等比例适配,非常爽!
但你不要忘了,这样你就强耦合于这个方案了,因为当你不使用这个方案时,density是不可变的!
举个栗子
使用这个方案时,在屏幕宽度为 1080px的设备上,将设计图宽度直接填写为 1080,根据今日头条公式
当前设备屏幕总宽度 / 设计图总宽度 = density
这时得出 density为 1 (1080 / 1080 = 1),所以你在 layout文件中你填写的 300dp最后转换为 px也是 300px(300dp * 1 = 300px根据公式 dp * density = px)
在这个方案的帮助下非常完美,和设计图一模一样完成了适配
但当你不使用这个方案时,density的换算公式就变为官方的 DPI / 160 = density, 在这个屏幕宽度为 1080px,480dpi的设备上,density就固定为 3 (480 / 160 = 3)
这时再来看看你之前在 layout文件中填写的 dp,换算成 px为 900 px(300dp * 3 = 900px根据公式 dp * density = px)
原本在在设计图上为 300px的 View,这时却达到了惊人的 900px,3倍的差距,恭喜你,你已经强耦合于这个方案了,你要不所有 layout文件都改一遍,要不继续使用这个方案
第二个坑
第二个坑其实就是刚刚在上面说的今日头条适配方案的缺点,当某个系统控件或三方库控件的设计图尺寸和和我们项目自身的设计图尺寸差距非常大时,这个问题就越严重
你如果直接填写以 px为设计图的尺寸,这不用想,肯定和所有的三方库以及系统控件的设计图尺寸都不一样,而且差距都非常之大,至少两三倍的差距,这时你在当前页面弹个 Toast就可以明显看到,比之前小很多,可以说是天差万别,用其他三方库 View,也是一样的,会小很多
因为你以 px为单位填写设计图尺寸,人家却用的 dp,差距能不大吗,你如果老老实实用 dp,哪怕三方库的设计图尺寸和你项目自身的设计图尺寸不一样,那也差距不大,小到一定程度,基本都不用调整,可以忽略不计,而且很多三方库的设计图尺寸其实也都是那几个大众尺寸,很大可能和你项目自身的设计图尺寸一样
总结
今日头条屏幕适配方案优化的屏幕适配框架 https://github.com/JessYanCoding/AndroidAutoSize
今日头条屏幕适配方案官方公布的核心源码只有 30 行不到,但我这个框架的源码有 1500行以上,在保留原有特性的情况下增加了不少功能和特性,功能增加了不少,但是使用上却变简单了
<manifest>
<application>
<meta-data
android:name="design_width_in_dp"
android:value="360"/>
<meta-data
android:name="design_height_in_dp"
android:value="640"/>
</application>
</manifest>
只要这一步填写了设计图的高宽以 dp 为单位,你什么都不做,框架就开始适配了
二、smallestWidth适配
smallestWidth适配,或者叫sw限定符适配。指的是Android会识别屏幕可用高度和宽度的最小尺寸的dp值(其实就是手机的宽度值),然后根据识别到的结果去资源文件中寻找对应限定符的文件夹下的资源文件。
这种机制和上文提到的宽高限定符适配原理上是一样的,都是系统通过特定的规则来选择对应的文件。
举个例子,小米5的dpi是480,横向像素是1080px,根据px=dp(dpi/160),横向的dp值是1080/(480/160),也就是360dp,系统就会去寻找是否存在value-sw360dp的文件夹以及对应的资源文件。
smallestWidth限定符适配和宽高限定符适配最大的区别在于,前者有很好的容错机制,如果没有value-sw360dp文件夹,系统会向下寻找,比如离360dp最近的只有value-sw350dp,那么Android就会选择value-sw350dp文件夹下面的资源文件。这个特性就完美的解决了上文提到的宽高限定符的容错问题。
这套方案是上述几种方案中最接近完美的方案。
首先,从开发效率上,它不逊色于上述任意一种方案。根据固定的放缩比例,我们基本可以按照UI设计的尺寸不假思索的填写对应的dimens引用。
我们还有以375个像素宽度的设计稿为例,在values-sw360dp文件夹下的dimens文件应该怎么编写呢?
这个文件夹下,意味着手机的最小宽度的dp值是360,我们把360dp等分成375等份,每一个设计稿中的像素,大概代表smallestWidth值为360dp的手机中的0.96dp,那么接下来的事情就很简单了,假如设计稿上出现了一个10px*10px的ImageView,那么,我们就可以不假思索的在layout文件中写下对应的尺寸。
而这种diemns引用,在不同的values-sw<N>dp文件夹下的数值是不同的,比如values-sw360dp和values-sw400dp,
当系统识别到手机的smallestWidth值时,就会自动去寻找和目标数据最近的资源文件的尺寸。
其次,从稳定性上,它也优于上述方案。原生的dp适配可能会碰到Pixel 2这种有些特别的手机需要单独适配,但是在smallestWidth适配中,通过计算Pixel 2手机的的smallestWidth的值是411,我们只需要生成一个values-sw411dp(或者取整生成values-sw410dp也没问题)就能解决问题。
smallestWidth的适配机制由系统保证,我们只需要针对这套规则生成对应的资源文件即可,不会出现什么难以解决的问题,也根本不会影响我们的业务逻辑代码,而且只要我们生成的资源文件分布合理,,即使对应的smallestWidth值没有找到完全对应的资源文件,它也能向下兼容,寻找最接近的资源文件。
当然,smallestWidth适配方案有一个小问题,那就是它是在Android 3.2 以后引入的,Google的本意是用它来适配平板的布局文件(但是实际上显然用于diemns适配的效果更好),不过目前所有的项目应该最低支持版本应该都是4.0了(糗事百科这么老的项目最低都是4.0哦),所以,这问题其实也不重要了。
还有一个缺陷我忘了提,那就是多个dimens文件可能导致apk变大,这是事实,根据生成的dimens文件的覆盖范围和尺寸范围,apk可能会增大300kb-800kb左右,目前糗百的dimens文件大小是406kb,我认为这是可以接受的。
项目地址:
生成diemns文件的过程以及数据计算方法上面已经讲清楚了,大家完全可以自己去生成这些文件。
这里附赠生成values-sw的项目代码,大家直接拿去用,是Java工程,点击这里获取项目地址