#前言

Andoird屏幕适配一直是安卓开发中一个热点话题,现在市面上也有好几种主流的适配方案被广泛使用,尽管方案不同,但是万变不离其宗,都要达到一个目的:在不同的机器上显示相同的UI效果。在分析几种主流适配方案之前,我们必须要知道屏幕适配的几个概念。

  • px : 即像素,屏幕显示的最小单元,像我们常说的一台手机的分辨率为1280*720,就是指的宽1280px高720px
  • dpi : 每个安卓机器都会拥有这个一个固定数值,其作用是用于dp与px的相互转换
  • dp :密度无关像素,是谷歌推荐我们使用的用于屏幕适配的单位。

上面3个值有一个很重要的转换公式:

px = (dpi/160) * dp

让我们一起来看一下这个读5年级的小学生都能看懂的公式。

android 屏幕 宽高 比例 android屏幕宽度dp_宽高


假设我们有一台手机它的dpi是160,那么1dp在屏幕上实际显示多少像素呢?我们把dpi=160和dp=1代入到公式中可以得到一个结果:1px = 1dp。让我们以此类推,dpi为320的手机,1dp实际显示多少像素呢,很明显是2px = 1dp 。你也许会问这个公式里有个数值160是从哪里冒出来的?其实它是谷歌所规定的最小dpi,在这个公式里面它是固定数值,是不变的。只要你能理解上述公式,你就理解了所有屏幕适配方案的最终原理,无论市面上哪种适配方案,都是通过这个公式进行一些巧妙的转换从而达到适配效果。

#使用dp会导致的问题

为什么明明谷歌都推荐使用dp为单位了,且在大部分机器上使用dp都是没有适配问题的,我们还要使用别的适配方案呢?外界原因是因为很多安卓手机并没有按照谷歌给的规范来生产,所以屏幕素质参差不齐。这就可能会导致这样一种情况:两个同为1920*1080分辨率的手机,却拥有2个不同的dpi:320dpi和480dpi,会出现什么样的情况呢?通过px = (dpi/160) * dp,把320dpi和480dpi分别代入公式中会得到2个结果:2px = 1dp 和 3px = 1dp,也就是说使用同一个dp值在480dpi手机上要比320dpi手机上大1.5倍

android 屏幕 宽高 比例 android屏幕宽度dp_屏幕适配_02

通过上述内容我们了解到

  1. px、dpi、dp三个基本概念
  2. 一个非常重要的公式:px = (dpi/160) * dp
  3. 一个问题:使用dp还是会出现适配问题,那怎么才能得到用一个单位在不同设备上显示同样的效果呢?

#适配方案

一、宽高限定符适配

想详细了解的可以看洪洋大神的文章:Android 屏幕适配方案

这里我们先说一下"设计图基准"这个概念,每个公司的UI设计师设计UI时会按照一个标准的参数进行设计。像我们公司的UI设计时通常会以宽1080px高1920px为单位进行设计。有一个张图片要显示宽300像素,高300像素。如果一台刚好是1080 * 1920的手机,那么还好,直接设置宽高都为300px即可,如果是一台1280 * 720的手机,按照等比缩减 (720/1280) * 300 = 200,宽高应该都为200像素才能达到同样的显示效果。

  • 假设基准为1080*1920,宽度缩减比例 = 设备宽分辨率 / 1080,高度缩减比例 = 设备高分辨率 / 1920

按照基准把宽分成1080份、高分成1920份,可以生成:

android 屏幕 宽高 比例 android屏幕宽度dp_宽高_03

android 屏幕 宽高 比例 android屏幕宽度dp_安卓开发_04

android 屏幕 宽高 比例 android屏幕宽度dp_限定符_05

再通过缩减比例,生成不同分辨率的dimens文件文件:

android 屏幕 宽高 比例 android屏幕宽度dp_宽高_06

values - 1280 x 720:

android 屏幕 宽高 比例 android屏幕宽度dp_宽高_07

android 屏幕 宽高 比例 android屏幕宽度dp_安卓开发_08

1920 * 1080和1280 * 720的lay_x文件中第一行分别为1px和0.66px,刚好对应了720/1080=0.66,所以我们在布局中直接使用合适的x值即可,强大的安卓系统会根据机器本身的分辨率找到对应的dimens文件,lay_y同理。你可能会问为什么要用lay_x和lay_y两套呢?因为宽和高的缩减比例可能是不同的,像1200*720的机器,1200/1920不等于720/1080,适配会有些差别。

这套适配方案基本上能解决绝大部分的适配问题,但是也有它的缺陷:
  1. 多套dimens文件必然增加app体积
  2. 需要准确的values分辨率系统才能找到。我如果只放了values-1280x720和values-1920x1080两套文件,当机器分辨率是2560x1440时就会使用默认的dimens文件,就会出问题。
  3. 顺便提一下,前两年有很多手机带有虚拟按键,你必须要减掉虚拟按键的高度才是该机器的真实高度。一台1920*1080的带虚拟按键高80,那么它的实际分辨率就是1840 * 1080,你的dimens文件就要用values-1840 * 1080

二、smallestWidth限定符适配

该方案应该是由糗百提出来的,详情见:smallestWidth限定符适配

二话不说先上dimens文件:

android 屏幕 宽高 比例 android屏幕宽度dp_android 屏幕 宽高 比例_09

在使用方式上与宽高限定符适配很像,原理上也是类似的,都是利用安卓寻找资源文件的特殊规则。一台机器的分辨率是1920 * 1080,dpi是320,那么根据公式px = (dpi/160) * dp ,该机器屏幕宽度为540dp,安卓系统会找到values-sw540的文件夹中。

这套方案名字叫最小宽度限定适配,它只用了设计图的宽作为基准。无论是哪种机器我们都假想把它的宽分成1080份,是不是有种似曾相识的感觉?没错,第一种适配方式是按照px分成把宽高1080和1920份,这里我们按照dp把宽分成1080份,得到以下dimens文件:

android 屏幕 宽高 比例 android屏幕宽度dp_宽高_10

480dp:

android 屏幕 宽高 比例 android屏幕宽度dp_宽高_11

使用方式也很类似,直接使用qb_px_(value) 就好了。我们在回头看一下这个公式:px = (dpi/160) * dp ,dpi是变动的,宽高限定符适配是从px下手,smallestWidth限定符适配从dp下手,只是实现方式有所变化!

三、autoLayout框架适配

这个方案同样是由鸿洋大神提出并开发。地址:AutoLayout

使用方式:
  1. gradle文件中引入该库
  2. 在AndroidManifest.xml中设定宽高基准,不设置会报错
  3. Activity继承框架中的AutoLayoutActivity

使用起来可以说是非常方便了,开发中可直接使用px来开发,框架会帮你自动转换。该框架的实现源码我没有细看,猜测是框架在获取到布局之后根据机器dpi和分辨率转换成合适的值。

该方案缺点:
  1. 框架在好久之前已经停止维护了。。
  2. 自定义控件需要自己手动适配且不一定有效

四、今日头条方案

详情见:今日头条适配方案

这套方案比起前几种更加简洁,核心代码只有十几行,让我们一起看一下:

private static float sNoncompatDensity;
 private static float sNoncompatScaledDensity;
 private static final float DESIGN_WIDTH_DP = 360;// 设计图宽度总dp

 /**
  * density = dpi/160
  * px = (dpi/160) * dp
  * px = density * dp
  */
 private static void setCustomDensity(Activity activity, Application application) {
     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 / DESIGN_WIDTH_DP;
     final float targetScaledDensity = targetDensity * (sNoncompatScaledDensity / sNoncompatDensity);
     final int targetDensityDpi = (int) (targetDensity * 160);

     appDisplayMetrics.density = targetDensity;
     appDisplayMetrics.scaledDensity = targetScaledDensity;
     appDisplayMetrics.densityDpi = targetDensityDpi;

     final DisplayMetrics activityDisplayMetrics = activity.getResources().getDisplayMetrics();
     activityDisplayMetrics.density = targetDensity;
     activityDisplayMetrics.scaledDensity = targetScaledDensity;
     activityDisplayMetrics.densityDpi = targetDensityDpi;
 }

上来直接给一段这样的代码可能会有点蒙蔽,不要慌,先祭出我们的公式:

px = (dpi/160) * dp。

前面说了,宽高限定符适配是用公式中的px做文章,smallestWidth限定符适配是用公式中dp做文章,只剩下一个了,那就是(dpi/160),没错,头条的方案就是用这个来做的文章,变换一下:density = dpi/160,让我们看源码。

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() { }
    });
}

这段代码做了两件事:

  1. 获取当前机器的density和scaledDensity,density就是上述中的dpi/160,scaledDensity是字体的density
  2. 增加系统设置更换系统字体大小的监听,为了在当用户修改系统字体大小时APP的UI也跟着修改,可以说考虑非常细心
final float targetDensity = appDisplayMetrics.widthPixels / DESIGN_WIDTH_DP;
final float targetScaledDensity = targetDensity * (sNoncompatScaledDensity / sNoncompatDensity);
final int targetDensityDpi = (int) (targetDensity * 160);

核心原理就在这3句代码中体现,appDisplayMetrics.widthPixels是机器宽分辨率,DESIGN_WIDTH_DP是设计图中宽度总dp,例如我有一台分辨率1080*1920,dpi为480的手机,代入公式1080 = (480/160) * dp,那么dp = 360,这个就是宽度总dp。第一行代码用appDisplayMetrics.widthPixels / DESIGN_WIDTH_DP,机器屏幕宽度除以宽度总dp,这里为什么要这么做呢?还记得文章开头提到的使用dp会出现的问题吗,相同分辨率不同dpi的机器,根据px = (dpi/160) * dp,dpi不同时,使用同一个dp会算出不同的px,显示效果就会不同。我们何不反着想一下,只要修改density = dpi/160使计算出来的px值是相同的不就行了吗?继续回到上述例子,机器为1080px宽时density = 1080 / 360就是合适的值,机器为720px宽时 density = 720 / 360 就是合适的值,不去考虑dpi,不就解决了我们的问题了吗。不得不说这种想法真的非常巧妙,代码中通过appDisplayMetrics.widthPixels / DESIGN_WIDTH_DP计算出合适的density,再反向推断出合适的dpi ( 再次根据公式,dpi = targetDensity * 160 )

appDisplayMetrics.density = targetDensity;
appDisplayMetrics.scaledDensity = targetScaledDensity;
appDisplayMetrics.densityDpi = targetDensityDpi;

final DisplayMetrics activityDisplayMetrics = activity.getResources().getDisplayMetrics();
activityDisplayMetrics.density = targetDensity;
activityDisplayMetrics.scaledDensity = targetScaledDensity;
activityDisplayMetrics.densityDpi = targetDensityDpi;

剩下的工作就是替换系统的density、scaledDensity、densityDpi三个值了。比起前几种适配方式,这种更加简便简洁,实现方式也非常简单粗暴。由于这个方案的基准是设计图宽度总dp,也就是说不但要知道设计图的宽分辨率,还要知道设计图的dpi才能算出总dp,但是,据我了解很多UI设计师都不清楚安卓中的dp和dpi是什么玩意儿。。不过抛开这些外界因素,这套方案确实是最简便的,没有多套dimens文件,也容易理解。

总结

有啥好总结的,记住px = (dpi/160) * dp不就行了吗!


android 屏幕 宽高 比例 android屏幕宽度dp_限定符_12