android主流屏幕适配方案总结

1.概述

android的碎片化导致了很多问题,屏幕适配算是其中比较头疼的一个问题了,想想你见过的安卓手机的尺寸和比例有多少种,本来应该说手机屏幕大就应该显示更多的内容,但是机型那么多,想要控制所有手机的显示效果是不现实的,所以只能是让页面在所有手机上尽可能的一致,也就是大屏幕拉伸,小屏幕缩放。
Android适配最核心的问题有两个,其一,就是适配的效率,即把设计图转化为App界面的过程是否高效,其二如何保证实现UI界面在不同尺寸和分辨率的手机中UI的一致性。
阅读下面的内容之前需要了解安卓屏幕的基本知识

2.最简单的dp

dp是安卓尺寸的基本单位,相信没有用px做单位的吧(也有这样的方案),那么android是怎么进行适配的呢?举个例子:

1080*720 1920*1080
320dpi 480dpi
2density 3density

根据上面的表格,我们可以发现,720P,和1080P的手机,dpi是不同的,这也就意味着,不同的分辨率中,1dp对应不同数量的px(720P中,1dp=2px,1080P中1dp=3px),这就实现了,当我们使用dp来定义一个控件大小的时候,他在不同的手机里表现出相应大小的像素值。
我们可以说,通过dp加上自适应布局和weight比例布局可以基本解决不同手机上适配的问题,这基本是最原始的Android适配方案。

这种方式存在两个小问题,第一,这只能保证我们写出来的界面适配绝大部分手机,部分手机仍然需要单独适配,为什么dp只解决了90%的适配问题,因为并不是所有的1080P的手机dpi都是480,比如Google 的Pixel2(19201080)的dpi是420,也就是说,在Pixel2中,1dp=2.625px,这样会导致相同分辨率的手机中,这样,一个100dp100dp的控件,在一般的1080P手机上,可能都是300px,而Pixel 2 中 ,就只有262.5px,这样控件的实际大小会有所不同。

3.宽高限定符适配

为了高效的实现UI开发,出现了新的适配方案,我把它称作宽高限定符适配。简单说,就是穷举市面上所有的Android手机的宽高像素值:
在这里插入图片描述
设定一个基准的分辨率,其他分辨率都根据这个基准分辨率来计算,在不同的尺寸文件夹内部,根据该尺寸编写对应的dimens文件。
比如以480x320为基准分辨率

  • 宽度为320,将任何分辨率的宽度分为320份,x1-x320
  • 高度为480,将任何分辨率的高度分为480份,y1-y480

那么在基准分辨率里x1=1px,y1=1px
那么对于800*480的分辨率的dimens文件来说,也把480分为320份,x1=480/320px=1.5px,同样的高800分为480份,y1=800/480px=1.66px。
这个时候,如果我们的UI设计界面使用的就是基准分辨率,那么我们就可以按照设计稿上的尺寸填写相对应的dimens引用了,而当APP运行在不同分辨率的手机中时,这些系统会根据这些dimens引用去该分辨率的文件夹下面寻找对应的值。这样基本解决了我们的适配问题,而且极大的提升了我们UI开发的效率。

但是这个方案有一个致命的缺陷,那就是需要精准命中才能适配,比如1920x1080的手机就一定要找到1920x1080的限定符,否则就只能用统一的默认的dimens文件了。而使用默认的尺寸的话,UI就很可能变形,简单说,就是容错机制很差。而且目前市面上的手机分辨率之多已经无法想象,为每个分辨率的手机提供一套dimens会使app的内存大幅度增加,目前这个方案已经基本废弃。

4.AndroidAutoLayout

鸿洋的适配方案的项目也来自于宽高限定符方案的启发。
首先要在AndroidManifest中注明你的设计稿的尺寸,也就是基准分辨率:

        <meta-data
            android:name="design_width"
            android:value="1080" />
        <meta-data
            android:name="design_height"
            android:value="1920" /> 

让你的Activity继承自AutoLayoutActivity.
然后我们就可以直接在布局文件里面使用具体的像素值了,比如,设计稿上是96*96,那么我们可以直接写96px,APP运行时,框架会帮助我们根据不同手机的具体尺寸按比例伸缩。
具体实现原理是:

  • 宽:(填写的px值/1080)*屏幕宽实际的px值=适配后的px值
  • 高:(填写的px值/1080)*屏幕高实际的px值=适配后的px值

这可以说是一个极好的方案,因为它在宽高限定符适配的基础上更进一步,并且解决了容错机制的问题,可以说完美的达成了开发高效和适配精准的两个要求。

但是我们能够想到,因为框架要在运行时会在onMeasure里面做变换,我们自定义的控件可能会被影响或限制,可能有些特定的控件,需要单独适配,这里面可能存在的暗坑是不可预见的,还有一个比较重要的问题,那就是整个适配工作是有框架完成的,而不是系统完成的,一旦使用这个框架,未来一旦遇到很难解决的问题,替换起来是非常麻烦的,而且还有一个现在出现的问题是他不能很好的适配全面屏手机,因为AndroidAutoLayout 就是默认所有设备的屏幕宽高比和设计图的宽高比(16:9)都保持一致,但是现在全面屏导致屏幕比例多样化越来越严重,所以这是一个目前不推荐使用的框架。

4.smallestWidth限定符适配(主流)

smallestWidth适配,或者叫sw限定符适配。指的是Android会识别屏幕可用宽度的最小尺寸的dp值,然后根据识别到的结果去资源文件中寻找对应限定符的文件夹下的资源文件。sw限定符适配是宽高限定符的升级版,他解决了宽高限定符容错机制差的问题,是目前主流的适配方式之一。
举个例子,小米5的dpi是480,横向像素是1080px,根据px=dp(dpi/160),横向的dp值是1080/(480/160),也就是360dp,系统就会去寻找是否存在value-sw360dp的文件夹以及对应的资源文件。
在这里插入图片描述
smallestWidth限定符适配和宽高限定符适配最大的区别在于,前者有很好的容错机制,如果没有value-sw360dp文件夹,系统会向下寻找,比如离360dp最近的只有value-sw350dp,那么Android就会选择value-sw350dp文件夹下面的资源文件。这个特性就完美的解决了上文提到的宽高限定符的容错问题。
dimens.xml 生成原理: 和宽高限定符方案一样需要一个最小宽度基准值,假设我们现在把项目的 最小宽度基准值 定为 360,那这个方案就会理解为您想把所有设备的屏幕宽度都分为 360 份,方案会帮您在 dimens.xml 文件中生成 1 到 360 的 dimens 引用,比如 values-sw360dp 中的 dimens.xml 是长这样的:

<?xml version="1.0" encoding="UTF-8"?>
<resources>
	<dimen name="dp_1">1dp</dimen>
	<dimen name="dp_2">2dp</dimen>
	<dimen name="dp_3">3dp</dimen>
	<dimen name="dp_4">4dp</dimen>
	<dimen name="dp_5">5dp</dimen>
	<dimen name="dp_6">6dp</dimen>
	<dimen name="dp_7">7dp</dimen>
	<dimen name="dp_8">8dp</dimen>
	<dimen name="dp_9">9dp</dimen>
	<dimen name="dp_10">10dp</dimen>
	...
	<dimen name="dp_356">356dp</dimen>
	<dimen name="dp_357">357dp</dimen>
	<dimen name="dp_358">358dp</dimen>
	<dimen name="dp_359">359dp</dimen>
	<dimen name="dp_360">360dp</dimen>
</resources>

values-sw360dp 指的是当前设备屏幕的 最小宽度 为 360dp (该设备高度大于宽度,则最小宽度就是宽度,所以该设备宽度为 360dp),把屏幕宽度分为 360 份,刚好每份等于 1dp,所以每个引用都递增 1dp,值最大的 dimens 引用 dp_360 值也是 360dp,刚好覆盖屏幕宽度。
下面再来看看将 最小宽度基准值 定为 400 时,values-sw400dp 中的 dimens.xml 长什么样:

<?xml version="1.0" encoding="UTF-8"?>
<resources>
	<dimen name="dp_1">1.1111dp</dimen>
	<dimen name="dp_2">2.2222dp</dimen>
	<dimen name="dp_3">3.3333dp</dimen>
	<dimen name="dp_4">4.4444dp</dimen>
	<dimen name="dp_5">5.5556dp</dimen>
	<dimen name="dp_6">6.6667dp</dimen>
	<dimen name="dp_7">7.7778dp</dimen>
	<dimen name="dp_8">8.8889dp</dimen>
	<dimen name="dp_9">10.0000dp</dimen>
	<dimen name="dp_10">11.1111dp</dimen>
	...
	<dimen name="dp_355">394.4444dp</dimen>
	<dimen name="dp_356">395.5556dp</dimen>
	<dimen name="dp_357">396.6667dp</dimen>
	<dimen name="dp_358">397.7778dp</dimen>
	<dimen name="dp_359">398.8889dp</dimen>
	<dimen name="dp_360">400.0000dp</dimen>
</resources>

alues-sw400dp 指的是当前设备屏幕的 最小宽度 为 400dp (该设备高度大于宽度,则最小宽度就是宽度,所以该设备宽度为 400dp),把屏幕宽度同样分为 360份,这时每份就等于 1.1111dp 了,每个引用都递增 1.1111dp,值最大的 dimens 引用 dp_360 同样刚好覆盖屏幕宽度,为 400dp。
到这里我们发现这个方案的原理和宽高限定符简直一模一样,就是一个是px一个是dp。
总结一下这个方案的优缺点:
优点:

  1. 容错率高
  2. 不会有任何性能的损耗
  3. 适配范围可自由控制,不会影响其他三方库
  4. 在插件的配合下,学习成本低

缺点:

  1. 在布局中引用 dimens 的方式,虽然学习成本低,但是在日常维护修改时较麻烦
  2. 侵入性高,如果项目想切换为其他屏幕适配方案,因为每个 Layout 文件中都存在有大量 dimens 的引用,这时修改起来工作量非常巨大,切换成本非常高昂
  3. 无法覆盖全部机型,想覆盖更多机型的做法就是生成更多的资源文件,但这样会增加 App 体积,在没有覆盖的机型上还会出现一定的误差,所以有时需要在适配效果和占用空间上做一些抉择
  4. 如果想使用 sp,也需要生成一系列的 dimens,导致再次增加 App 的体积
  5. 不能自动支持横竖屏切换时的适配,如上文所说,如果想自动支持横竖屏切换时的适配,需要使用 values-wdp 或 屏幕方向限定符 再生成一套资源文件,这样又会再次增加 App 的体积

现在还有一个问题,前面说过在使用宽高限定符的时候直接将设计图上的px填入就可以了,但是sw限定符是使用dp作为单位的,这样我们还是要将px转化为dp,其实是也可以不用换算,把设计图的px总宽度设置为最小宽度基准值就行了,比如设计图的宽度为750px,然后看看 values-sw360dp 中的 dimens.xml 长什么样:

<?xml version="1.0" encoding="UTF-8"?>
<resources>
	<dimen name="px_1">0.48dp</dimen>
	<dimen name="px_2">0.96dp</dimen>
	<dimen name="px_3">1.44dp</dimen>
	<dimen name="px_4">1.92dp</dimen>
	<dimen name="px_5">2.4dp</dimen>
	...
	<dimen name="px_50">24dp</dimen>
	...
	<dimen name="px_100">48dp</dimen>
	...
	<dimen name="px_746">358.08dp</dimen>
	<dimen name="px_747">358.56dp</dimen>
	<dimen name="px_748">359.04dp</dimen>
	<dimen name="px_749">359.52dp</dimen>
	<dimen name="px_750">360dp</dimen>
</resources>

其实是一样的,等于把屏幕分成750份,在宽度为360dp的设备上1份等于360/750=0.48dp。

5.今日头条适配方案(主流)

我们知道android系统将pd转为px值的公式是:px = density * dp,而density = dpi / 160,也就是说px的最终大小是取决于dpi,但是dpi是写在系统中无法改变的,如果我们想要从这个公式上做文章只能修改dp或者density ,sw限定符方案其实就算是动态修改dp,而今日头条屏幕适配方案的核心原理在于修改 density。
px = density * dp可以的到density =px/dp=当前设备屏幕总宽度(单位为像素)/ 设计图总宽度(单位为 dp),density 的意思就是 1 dp 占当前设备多少像素,反过来想就是将每种屏幕的像素数分为固定(设计图总宽度)的份数。举个例子:

  • 设计图的总宽度为360dp,当前有一个手机的屏幕宽度为1080px,那么就是将1080分为360份,density =3,我们在界面中填写尺寸为30dp的时候转化过来就是30*3=90px
  • 设计图的总宽度为360dp,当前有一个手机的屏幕宽度为1440px,那么就是将1080分为360份,density =4,我们在界面中填写尺寸为30dp的时候转化过来就是30*4=120px

我们发现这个方案根本就没有用到dpi这个参数,相当于我们把android系统的计算方式抛弃掉了换成我们自己的计算方式,这样就不会因为dpi的不可确定性导致无法预料的结果。

优点:

  1. 使用成本非常低,操作非常简单,使用该方案后在页面布局时不需要额外的代码和操作
  2. 侵入性非常低,该方案和项目完全解耦,在项目布局时不会依赖哪怕一行该方案的代码,而且使用的还是 Android 官方的 API,意味着当你遇到什么问题无法解决,想切换为其他屏幕适配方案时,基本不需要更改之前的代码,整个切换过程几乎在瞬间完成,会少很多麻烦,节约很多时间,试错成本接近于 0
  3. 可适配三方库的控件和系统的控件(不止是是 Activity 和 Fragment,Dialog、Toast 等所有系统控件都可以适配),由于修改的 density 在整个项目中是全局的,所以只要一次修改,项目中的所有地方都会受益
  4. 不会有任何性能的损耗

缺点:

  1. 只需要修改一次 density,项目中的所有地方都会自动适配,这个看似解放了双手,减少了很多操作,但是实际上反应了一个缺点,那就是只能一刀切的将整个项目进行适配,但适配范围是不可控的

今日头条的方案是我一直在用的方案,对于我来说他是最简单也是最有效的,在这里推荐一个第三方库,AndroidAutoSize,这个库目前有6k+的star,应该是目前屏幕适配库中最高的,并且一直在维护(因为没出来多久)。

6.总结

以上就是android从古至今主流的屏幕适配方案,原生的dp由于在部分dpi上显示效果不理想而不能直接使用,宽高限定符由于android机型的逐渐增多使得dimens文件数量过多导致app体积增大以及必须完全适配所有分辨率而退出历史舞台,洪洋的AndroidAutoLayout虽然在宽高限定符的基础上解决了dimens文件数量过多导致app体积增大的问题,但是由于全面屏时代的来临屏幕的宽高比例逐渐增多导致这个方案的效果也不是那么理想,sw限定符方案解决了屏幕的宽高比例不一致以及容错率低的问题,除了会生成少量的dimens增大app体积之外应该是比较完美的适配方案,今日头条适配方案是我非常喜欢的一个方案,因为他用起来太简单了,而且侵入式非常低,你可以随时替换他。
看了这么多方案我们可以发现其实他们的本质都是百分比缩放,这也是我们的最终目的,尽可能的在所有设备中都有相同的显示效果,另外插一句谷歌已经推出新的布局方案ConstraintLayout很久了,我一直在用,这里面就包含百分比布局,使用这种布局基本能解决大部分的显示效果,如果和今日头条适配方案搭配到一起使用简直nice。

发布了65 篇原创文章 · 获赞 24 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/shanshui911587154/article/details/88395425