android 中的屏幕适配是一个老生常谈的问题,也是一个经久不衰的话题,同时随着android 生态的不断发展,适配方案也不断进行了调整,今天我们就来聊一聊android中的适配到底有哪些奥秘。
屏幕适配顾名思义就是针对不同的手机屏幕做合理的呈现展示,再专业一点的说法就是根据不同的手机尺寸、不同的手机屏幕分辨率去进行合理的代码编写以达到同一套代码生成的APP能够满足在众多手机上面呈现出同样的展示效果。
在开始讲解如何进行屏幕适配之前,我们先要弄明白几个名词的含义。
名词一:屏幕尺寸
屏幕尺寸指的是屏幕的物理尺寸,单位为英寸,和我们熟知的物理单位换算公式为:
1英寸 = 2.54厘米。而在手机上,我们也会经常听到英寸单位,目前市面上面流行的手机尺寸5.X英寸、6.X英寸居多。同时需要说明的是手机上面所说的手机英寸是以手机的对角线来计算的。
名词二:手机分辨率(屏幕分辨率)
手机分辨率也是我们经常听到的名称,其实更专业一点的叫法是屏幕分辨率,通常情况下常见的屏幕分辨率有 320x480 480x800 720x1080 1080x1920 1080x2160 1440x2960 等等。其中第一个数字代表的是横向(宽),第二个数字代表的是纵向(高),屏幕分辨率的基本单位是像素,即PX。假设现在有一个手机的屏幕分辨率为1080*1920,即意味着在这个手机横向上面有1080个像素点,在纵向上有1920个像素点,注意,像素是矢量单位。
名词三:dp/dip(像素密度)
现在我们举个例子来验证一下:
手机型号(Redmi K30 5G):屏幕分辨率 10802400 6.67英寸。对应结果的ppi为 ppi = 395,最终1dp = 2.5px。现在有一个100dp的位图,占据整个横向的比例为:1002.5/1080*100%=23%
手机型号(vivo Y3s):屏幕分辨率 7201544 6.35英寸。对应结果的ppi为 ppi = 268,最终1dp = 1.7px。现在有一个100dp的位图,占据整个横向的比例为:1001.7/720*100%=23%
注意,所有的取值都是取整
看见没有,两个完全不同的手机,不同的屏幕分辨率,不同的尺寸,但是最终相同dp的位图占据的百分比却是一样的,这就是dp的魅力。
名词四:density(1dp占据当前设备多少像素)
根据前面dp的讲解,其实我们不难推断出:
density = ppi/160。由于手机屏幕分辨率与手机尺寸的不同,导致每个手机的density的最终结果值也不一样。而由于我们采用的都是dp作为单位,所以我们得知道我们手机屏幕的总dp是多少,总dp的计算公式为:
手机屏幕的总dp = 屏幕的总px / density。再回到上面的例子,同样分辨率但是不同尺寸的手机最终的density不同,而因为屏幕的总px相同,所以导致手机的总dp不同,于是就会出现屏幕适配问题。既然我们想保证手机屏幕的总dp不变,而屏幕的总px是可变的,那么我们就只能动态去改变density的值,而不能按照ppi/160的值来计算density。
介绍完了几个比较重要的名词,下面我们就开始进入正题了,屏幕适配的方案有很多种,我选取了几种目前比较流行的来讲解,这几种没有好坏之分,需要根据自己的情况选择,毕竟,适合自己的才是最好的。
这是官方推荐的适配方案,为什么采用dp,前面已经讲到了,这里就不再赘述了。那么,什么是自适应布局,什么又是weight呢?我们都知道,android有五大布局,即
·RelativeLayout(相对布局)
·LinearLayout(线性布局)
·AbsoluteLayout(绝对布局):被谷歌废弃
·frameLayout(帧布局)
·TabLayout(表格布局):被GridView代替
关于五大布局的使用不是我们的重点,其中的AbsoluteLayout(绝对布局)和TabLayout(表格布局)由于一些缺陷已经被弃用,我们经常使用的是RelativeLayout(相对布局)和LinearLayout(线性布局),而恰恰这两种布局可以满足自适应的要求,究其原因是它们都是以一个View作为基准,在此基础上进行排列,这样很大程度上能够保证布局最终呈现出来的一致性。那么weight又是什么呢?其实可以将它理解为比重,它没有具体值,是根据比重值来动态计算最终呈现的样子,这样就能保证不论屏幕的尺寸是多少,最终展示出来的都是按照比例来设定的,举一个列子,并排排列两个button按钮,每个设置的比重都是1,也就意味着两个button平分整个宽度,不管屏幕的宽度是多少,最终两个按钮呈现出来的都是1:1的比例。假设将第一个button的weight设置为2,第二个button的weight还是1,那么总的值为3,那么系统会将整个屏幕的横向分为3份,其中第一个button占据2/3的距离,第二个占据1/3的比例。具体的效果大家可以通过代码自行去测试,这里就不展开显示了。
所以说通过dp +自适应布局+weight可以在很大程度上满足屏幕适配的需求,而这也是官方推荐的方式。那么,这种方式有没有缺陷呢?肯定是有的,还记得上面的例子吗?如果我们的手机屏幕分辨率为1080*1920,但是尺寸却是5英寸,这个时候展现出来的界面就会不一样了。
知道了采用dp作为尺寸的不足之处后,我们就来想想有没有办法可以解决这个问题呢?
这个里面就要涉及到另外一个名词了,即density,由上面的讲解可知,如果我们想保证手机屏幕的总dp不变,而屏幕的总px是可变的,那么我们就只能动态去改变density的值,而不能按照ppi/160的值来计算density。那么如何去动态改变density的值而保证总的dp不变呢?我们首先需要看看系统是怎么计算density的。
在源码TypedValue.java类中有这么几行代码
其主要的作用就是将其他尺寸单位(例如dp,sp)转换为像素单位px。现在我们来测试一下,举个例子:
例1:设计图总宽度为375dp,屏幕宽度为1080px,可以得出density,1080/375=2.88。假如一个view为60dp,60dp算成px就是 60dp*2.88=172.8px,所占屏幕宽度比为 172.8/1080=0.16
例2:设计图总宽度为375dp,屏幕宽度为1440px,可以得出density,1440/375=3.84。假如一个view为60dp,60dp那么算成px就是 60dp*3.84=230.4px,所占屏幕宽度比为 172.8/1440=0.16。
由示例1,2可以看出,虽然屏幕宽度不一样,但是都实现了屏幕等比例适配。
那么具体的代码该怎么写呢?其实也很简单:
最后记得在我们的Application里面去配置就好了。
优点:
(1)使用成本非常低,操作非常简单,使用该方案后在页面布局时不需要额外的代码和操作,这点可以说完虐其他屏幕适配方案。
(2)侵入性非常低,该方案和项目完全解耦,在项目布局时不会依赖哪怕一行该方案的代码,而且使用的还是 Android官方的 API,意味着当你遇到什么问题无法解决,想切换为其他屏幕适配方案时,基本不需要更改之前的代码,整个切换过程几乎在瞬间完成,会少很多麻烦,节约很多时间。
(3)可适配三方库的控件和系统的控件(不止是 Activity和 Fragment,Dialog、Toast等所有系统控件都可以适配),由于修改的 density 在整个项目中是全局的,所以只要一次修改,项目中的所有地方都会受益。
(4)不会有任何性能的损耗。
缺点:只需要修改一次 density,项目中的所有地方都会自动适配,这个看似解放了双手,减少了很多操作,但是实际上反应了一个缺点,那就是只能一刀切的将整个项目进行适配,但适配范围是不可控的,当项目中的系统控件、三方库控件等非我们项目自身设计的控件,它们的设计图尺寸并不会和我们项目自身的设计图尺寸一样,这时就会出现问题,当某个系统控件或三方库控件的设计图尺寸和我们项目自身的设计图尺寸差距非常大时,这个问题就越严重。
但是说实话,今日头条团队提供的这个屏幕适配方案依旧非常的优秀。
不知道大家有没有用过鸿洋大神的关于屏幕适配的AndroidAutoLayout,我记得我刚开始工作的时候用的就是这个,那个时候使用这个框架会在src下面生成很多的values文件,就像这样:
其实这个框架的原理也很简单,即在src下面生成多种机型的文件,在我们的APP安装到手机上运行的时候,系统会自动根据当前手机的屏幕分辨率去找到对应的values文件,从而保证在众多的机型上能够完美的适配,而我们的基准就是根据美工画图时的手机屏幕分辨率来确定。这个应该很容易理解的,至于具体的如何操作这里就不展开讲了,因为这个库作者已经停止维护了,所以我们应该去寻找更好的替代。
SmallestWidth限定符方案其实可以看作是AndroidAutoLayout的进阶版,其二者的区别有:
(1)在使用AndroidAutoLayout的使用,其对应的单位是px,而使用SmallestWidth,使用的单位则是dp。
(2)在使用AndroidAutoLayout框架的时候,如果手机系统的分辨率我们在APP里面没有包含到,会产生异常。而使用SmallestWidth,即使系统没有找到声明的文件,也会自动去寻找相近的文件从而完成适配。
原理
smallestWidth限定符屏幕适配方案的原理也很简单,甚至可以说和AndroidAutoLayout是一样的,开发者先在项目中根据主流屏幕的最小宽度生成一系列的values-sw<N>dp文件夹(含有dimens.xml),当把项目运行到设备上时,系统会根据当前设备屏幕的最小宽度去匹配对应的values-sw<N>dp文件夹,而对应的values-sw<N>dp文件夹中的dimens.xml文字中的值又是根据当前设备屏幕的最小宽度而定制的,所以一定能适配当前屏幕。
如何计算
如何计算就不再赘述了,其基本公式为:
总dp = 总屏幕的最小宽度px / density
density = ppi/160
至于ppi是怎么计算出来的,上面有图。
优点:
(1)非常稳定,极低概率出现意外。
(2)不会有任何性能的损耗。
(3)适配范围可自由控制,不会影响第三方库。
(4)在插件的配合下,集成非常容易,学习成本低。
缺点:
(1)由于维护的values文件非常多,因此维护麻烦
(2)侵入性高,换成其他的匹配方案修改麻烦
但是这种方案总体来说,也是众多优秀可选方案之一。
AndroidAutoSize框架是在今日头条屏幕适配方案基础上优化而来的,今日头条屏幕适配方案官方公布的代码,只实现了修改系统density的相关逻辑,但是这远远不够,而下面要讲的AndroidAutoSize框架则是在更多细致方面做更近一步的处理。
功能介绍:
AndroidAutoSize框架在使用上非常简单,接入步骤为:
(1)在build文件里面添加依赖
(2)在清单文件里面配置属性
到这里基本的接入就结束了,这个框架在APP启动的时候就开始运作了,当然,除此之外,你还可以在Application里面做一些简单的配置。
(3)在Application里面做一些配置(可加可不加)
到这里,AndroidAutoSize框架的使用就结束了,下面我们来细细看一下。在步骤2中,我们给width和height设置的单位是dp,这是android官方推荐的单位,而具体值是怎么来的呢?其实这个值是根据美工设计图的尺寸来的,注意,美工设计的图不管单位是什么,后面也须转换为dp。
关注点一:单位
AndroidAutoSize有两种类型的布局单位可以选择,一个是主单位(dp、sp),一个是副单位(pt、in、mm),两种单位各自有各自的适用场景,也有各自的优缺点。
·主单位:因为android系统本身就是使用dp和sp,所以使用dp和sp为单位进行布局对整个系统的侵入性低,但是这依然会影响其他第三方库和控件的使用。
·副单位:对系统的侵入性高,不会影响到第三方库和控件的使用。
一般情况下建议使用主单位进行适配
关注点二:width和height如何选择
虽然design_width_in_dp和design_height_in_dp都需要填写,但是在实际的适配中只会将高度和宽度当中的一个作为适配的依据,默认情况下以宽度作为适配的基准,当然也可以通过AutoSizeConfig来进行切换
其中true代表的是以宽度作为基准,false代表的是以高度作为基准。
关注点三:自动运行是怎么做到的
在源码里面我们找到这么一段
注意看:
所以说ContentProvider会在Application的onCreate之前调用,而AndroidAutoSize的原理就是只需要声明一个ContentProvider,在它的onCreate方法中启动框架即可,这里需要注意的是,如果你的项目拥有多进程,系统只会在主进程中实例化一个你声明的ContentProvider,并不会在其他的进程中实例化ContentProvider,如果在当前进程中ContentProvider没有被实例化,这时就需要在Application的onCreate方法中去查询一下,所以对于多进程的项目而言,需要在Application的onCreate中调用AutoSize的initCompatMultiProcess,如下:
关注点四:特殊页面适配
在AndroidManifest.xml文件中填写的尺寸是属于全局的尺寸,但是因为一些原因某个页面需要单独的尺寸适配这个时候我们就需要自己手动去适配了。具体怎么操作呢?
·针对Activity(重新设置尺寸适配)
·针对Activity(放弃适配)
·针对Fragment
首先我们需要添加到Fragment的支持
·针对Fragment(重新设置尺寸适配)
·针对Fragment(放弃适配)
关注点五:适配第三方库和控件
在使用主单位时可以使用ExternalAdaptManager来实现在不修改第三方库源码的情况下完成适配,具体实现为:
(1)添加适配
其中TargetActivity.class就是我们需要适配的目标Activity,true代表以宽度作为基准,400代表的是基准宽度为400dp。
(2)取消适配
其中TargetActivity.class就是我们需要取消适配的目标Activity。