屏幕适配这东西话说我还真没怎么好好研究过,一直用的都是头条那个适配方案,最近有些时间,看了看一些文章,稍稍整理一下吧!不想写太多,大致把各方案从别人那搬运过来(有点像小偷),本文尽量就挑重点,搬运的出处都会写清楚的!
屏幕相关说明
常用单位
- px: pixel,像素,电子屏幕上组成一幅图画或照片的最基本单元
- pt: point,点,印刷行业常用单位,等于1/72英寸,1pt= (DPI / 72) px。
- ppi: pixel per inch,每英寸像素数,该值越高,则屏幕越细腻
- dpi: dot per inch,每英寸多少点,该值越高,则图片越细腻,一般可认为 dpi=ppi
- dp: dip,Density-independent pixel, 是安卓开发用的长度单位,1dp 表示在屏幕像素点密度为160ppi 时 1px 长度
- sp: scale-independent pixel,安卓开发用的字体大小单位,字体设置为正常时 1sp = 1dp。
核心公式
android中 的 dp 在渲染前会将 dp 转为 px,计算公式:
- px = density * dp;
- density = dpi / 160;
- px = dp * (dpi / 160);
屏幕尺寸、分辨率、像素密度三者关系
通常情况下,一部手机的分辨率是宽x高,屏幕大小是以寸为单位,那么三者的关系是:
举个例子:屏幕分辨率为:1920*1080,屏幕尺寸为5吋的话,那么 dpi 为440。
DP 适配
使用原因
虽然 px 这个真实像素单位用的很多,但是 Android 却并不推荐我们使用,因为不同的手机之间,分辨率(单位是 px)是不同的,显示效果会不一致。
DP 概念
dp 指的是设备独立像素,以 dp 为尺寸单位的控件,在不同分辨率和尺寸的手机上代表了不同的真实像素,比如在分辨率较低的手机中,可能 1dp = 1px,而在分辨率较高的手机中,可能 1dp = 2px,这样的话,一个 96 * 96dp 的控件,在不同的手机中就能表现出差不多的大小了。
而 dpi 是像素密度,指的是在系统软件上指定的单位尺寸的像素数量,它往往是写在系统出厂配置文件的一个固定值,形象理解可以参考上面的图。
存在问题
以 1920*1080 dpi 440 设备为例,屏幕宽度 dp 值为 1080/(440/160)=392.7dp,如果我们用屏幕宽度为 360dp 的设计图来来设计页面,那么就会留有空余,其他手机甚至会出现显示不全。
百分比适配
日常开发中,一般可以通过百分比适配页面,比如利用 LinearLayout 的 weight 属性,高级点还可以使用 ConstraintLayout ,但是这样的方法做出来的东西和设计图差的就大了,设计师有话要说的!
头条适配方案
原文链接:一种极低成本的Android屏幕适配方式,下面简单讲下该方案的要点。
大致需求:
- 支持以宽或者高一个维度去适配,保持该维度上和设计图一致;
- 支持 dp 和 sp 单位,控制迁移成本到最小。
解决方案
要保持宽或高 是一个固定值(如 360 dp),根据 px = dp * density,我们想办法修改 density 就可以了。而无论布局还是图片都是通过 Resources#getDisplayMetrics 来转换 dp和px的,所以我们只要修改 DisplayMetrics 的 density,就能实现上面的需求了。
这里还有一个字体的问题,对于字体有一个 scaledDensity ,会根据系统字体大小改变,特殊处理一下就好了。
最终代码
// 系统的Density
private static float sNoncompatDensity;
// 系统的ScaledDensity
private static float sNoncompatScaledDensity;
public static void setCustomDensity(Activity activity, Application application) {
DisplayMetrics displayMetrics = application.getResources().getDisplayMetrics();
if (sNoncompatDensity == 0) {
sNoncompatDensity = displayMetrics.density;
sNoncompatScaledDensity = displayMetrics.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() {
}
});
}
// 此处以360dp的设计图作为例子
float targetDensity=displayMetrics.widthPixels/360;
float targetScaledDensity=targetDensity*(sNoncompatScaledDensity/sNoncompatDensity);
int targetDensityDpi= (int) (160 * targetDensity);
displayMetrics.density = targetDensity;
displayMetrics.scaledDensity = targetScaledDensity;
displayMetrics.densityDpi = targetDensityDpi;
DisplayMetrics activityDisplayMetrics = activity.getResources().getDisplayMetrics();
activityDisplayMetrics.density = targetDensity;
activityDisplayMetrics.scaledDensity = targetScaledDensity;
activityDisplayMetrics.densityDpi = targetDensityDpi;
}
宽高限定符适配
所谓宽高限定符适配,简单说,就是穷举市面上所有的Android手机的宽高像素值:
设定一个基准的分辨率,其他分辨率都根据这个基准分辨率来计算,在不同的尺寸文件夹内部,根据该尺寸编写对应的dimens文件。
比如以480x320为基准分辨率
- 宽度为320,将任何分辨率的宽度整分为320份,取值为x1-x320
- 高度为480,将任何分辨率的高度整分为480份,取值为y1-y480
那么对于800*480的分辨率的dimens文件来说,
x1=(480/320)*1=1.5px
x2=(480/320)*2=3px
…
这个时候,如果我们的UI设计界面使用的就是基准分辨率,那么我们就可以按照设计稿上的尺寸填写相对应的 dimens 引用了,而当APP运行在不同分辨率的手机中时,这些系统会根据这些 dimens 引用去该分辨率的文件夹下面寻找对应的值。这样基本解决了我们的适配问题,而且极大的提升了我们UI开发的效率,
但是这个方案有一个致命的缺陷,那就是需要精准命中才能适配,比如1920x1080的手机就一定要找到1920x1080的限定符,否则就只能用统一的默认的 dimens 文件了。而使用默认的尺寸的话,UI就很可能变形,简单说,就是容错机制很差。
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文件夹下的diemns文件应该怎么编写呢?这个文件夹下,意味着手机的最小宽度的dp值是360,我们把360dp等分成375等份,每一个设计稿中的像素,大概代表smallestWidth值为360dp的手机中的0.96dp,那么接下来的事情就很简单了,假如设计稿上出现了一个10px*10px的ImageView,那么,我们就可以不假思索的在layout文件中写下对应的尺寸。
而这种diemns引用,在不同的values-swdp文件夹下的数值是不同的,比如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,我认为这是可以接受的。
鸿洋的UI适配框架(已经停止维护)
GitHub 地址如下,有详细说明:
https://github.com/hongyangAndroid/AndroidAutoLayout
小结
主要还是推荐头条适配方案和 smallestWidth 适配吧,用的比较多。