Android屏幕适配——使用 dp 实现完美适配

我们一直知道 Android 可以使用 dp、sp 完成简单的适配,那你真的理解了么?先来看几个问题:

  1. dp 是如何进行适配的?
  2. dp 和 px 是如何换算的?
  3. sp 和 dp 的区别?
  4. dp 适配为什么会有偏差?
  5. 如何解决 dp 适配的偏差,达到完美适配?

下面我们就来看下源码,解决这些问题。

概述

在 android.util 包下,有个重要的类就是 DisplayMetrics,它主要是记录显示县官的一些信息,比如大小,密度,缩放系数等等。

如果要想获取到其中的信息很简单,可以通过上下文去拿,例如:

DisplayMetrics dm = context.getResources().getDisplayMetrics();

或者按照源码注释上的示例:

DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);

这个类有一大坨常量:

public static final int DENSITY_LOW = 120;
public static final int DENSITY_MEDIUM = 160;
public static final int DENSITY_TV = 213;
public static final int DENSITY_HIGH = 240;
public static final int DENSITY_260 = 260;
public static final int DENSITY_280 = 280;
public static final int DENSITY_300 = 300;
public static final int DENSITY_XHIGH = 320;
public static final int DENSITY_340 = 340;
public static final int DENSITY_360 = 360;
public static final int DENSITY_400 = 400;
public static final int DENSITY_420 = 420;
public static final int DENSITY_440 = 440;
public static final int DENSITY_XXHIGH = 480;
public static final int DENSITY_560 = 560;
public static final int DENSITY_XXXHIGH = 640;

其实最基本的只有DENSITY_LOWDENSITY_MEDIUMDENSITY_HIGHDENSITY_XHIGHDENSITY_XXHIGHDENSITY_XXXHIGH,这些都是标准的、主要DPI,其它的都是为了适配引申出来的次要的、介于两者之间的DPI。

DPI 就是 dots-per-inch,每一英寸的像素点。

还有个概念是 PPI,全称是 pixel-per-inch,DPI 和 PPI 是两个不同的概念,即便它们的值有时是一样的,区别嘛...很难描述,大家自己找下资料,这里提到的目的是,有些博客把这两个概念混为一谈是不正确的。

这里主要为大家解读三个参数:

  • densityDpi:设备的DPI,也就是上面那些常量值,是由手机厂商设置的。
  • density:缩放系数,这个下面会详细讲解。
  • scaledDensity:字体缩放系数,和density一样,不过可以受用户设置的字体大小的偏好进行调整。

基础知识

我们先来看下这几个值的赋值,我们发现一个方法,看名字应该是设置默认值。

public void setToDefaults() {
    ...
    density =  DENSITY_DEVICE / (float) DENSITY_DEFAULT;
    densityDpi =  DENSITY_DEVICE;
    scaledDensity = density;
    ...
}

1. densityDpi

先从最简单的下手:

public static final int DENSITY_DEFAULT = DENSITY_MEDIUM;

public static int DENSITY_DEVICE = getDeviceDensity();

private static int getDeviceDensity() {
    return SystemProperties.getInt("qemu.sf.lcd_density",
            SystemProperties.getInt("ro.sf.lcd_density", DENSITY_DEFAULT));
}

上面代码很好理解,densityDpi 就是从系统属性取值,如果qemu.sf.lcd_density 的值取不到,就取 ro.sf.lcd_density的,还取不到,就使用默认值,也就是 160 了。

qemu.sf.lcd_densityro.sf.lcd_density,手机厂商会写到系统属性中,这样一来,开发者就可以很方便的获取到设备真是的 DPI 了。

2. DENSITY_DEFAULT

这个还是有必要提一句,不要被名字蒙蔽了,它不仅仅是默认值这么简单!

/**
 * The reference density used throughout the system.
 */
public static final int DENSITY_DEFAULT = DENSITY_MEDIUM;

注释很重要!!!
注释很重要!!!
注释很重要!!!

整个系统使用的参考密度,这个可以理解为基准,后面计算 density 的缩放系数,都是以它为基准的。

3. density

用 DPI 除以基准 DPI,得到的就是density(缩放系数)。

density =  DENSITY_DEVICE / (float) DENSITY_DEFAULT;

那么应该如何理解呢?
如果一个 160dpi 的设备,那么 density 就是 1。
如果一个 120dpi 的设备,那么 density 就是 0.75。
如果一个 240dpi 的设备,那么 density 就是 1.5。

还不理解?
在 1 英寸内有 160 像素点的设备上,缩放系数就是 1。
在 1 英寸内有 120 像素点的设备上,缩放系数就是 0.75。
在 1 英寸内有 240 像素点的设备上,缩放系数就是 1.5。

4. scaledDensity

与 density 一样的,也是缩放系数,默认情况和 density 的值是相等的。

scaledDensity = density;

与 density 的区别是,scaledDensity 会受用户设置显示字体的大小进行缩放。

上面几个关键概念理解清楚了,下面就要说下,如何使用dp和sp完成屏幕的适配了。

dp 和 sp 是如何适配的

1. dp

dp 是 Android 中的一个适配的单位,用来表示大小。

如果设置控件的高度为 20dp,那么会发生什么?如何在不同设备上进行适配的呢?下面会解答这些问题。

上面讲到的 density(缩放系数) 就派上用场了,通过 dp 值乘以 density 就得到了最终的像素值。

这也就是网上经常流传的换算比例:

在 240 * 320 分辨率,DPI为 120,density 是 0.75,1dp=0.75px。
在 320 * 480 分辨率,DPI为 160,density 是 1,1dp=1px。
在 480 * 800 分辨率,DPI为 240,density 是 1.5,1dp=1.5px。
...
以此类推看下图:


图片来自网络

那么这个DPI是如何计算的?

分辨率为 480*800 的设备举例,在 3.8 寸的屏幕下,DPI 应该约等于 240。
计算公式: \sqrt{(480^2 + 800^2)}\div 3.8=245

通过公式很容易发现一个问题,DPI 受到分辨率和屏幕尺寸两个值影响的,因为它的定义是,每一英寸的像素点,所以相同分辨率下,不同的屏幕尺寸的设备的 DPI 也是有可能不同的。

这也就说明,Android中我们经常提到的 ldpi,mdpi,hdpi 等等这些规格只能与 DPI 挂钩,并不能由设备分辨率进行区分 ldpi,mdpi,hdpi 这些规格,只能说由于手机尺寸比较接近,大多数的情况下,都符合在上面表格。

知道上面的概念了,这就能理解,在不同DPI设备上 20dp 如何进行适配了。

在 120dpi 的设备上,会乘以 0.75 的缩放系数转换为 15px 展示到界面上。
在 160dpi 的设备上,会乘以 1 的缩放系数转换为 20px 展示到界面上。
在 240dpi 的设备上,会乘以 1.5 的缩放系数转换为 30px 展示到界面上。
...
以此类推,实现了适配。

2. sp

sp 是 Android 中的一个适配的单位,用来表示字体大小。

可以理解为和 dp 是一样的,区别是,它是以 scaledDensity 作为缩放系数,默认情况和 density 的值是一样的,但是在调整系统字体大小时,就会随着字体大小的改变而缩放了。

3. TypedValue

上面我说了,dp、sp 转换 px 会乘以缩放比例,计算出真实的 px 值,口说无凭,来看下 TypedValue 的源码:

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;// dp乘以density缩放系数
    case COMPLEX_UNIT_SP:
        return value * metrics.scaledDensity;// sp乘以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;
}

如何使用 dp 达到完美适配

理解了这些知识,有什么用?除了面试时可以装一波,更重要的事,可以完善 Android 的 dp 适配机制

经常使用 dp 适配会发现,只是能达到大致的适配效果,并不完美,会有多个方面造成不准确:

  • 设备厂商设置的 DPI 是否准确。
  • Android 提供的 DPI 有限,不一定会覆盖到所有设备厂商。
  • DPI 的计算涉及到分辨率和屏幕尺寸,计算出来的值,都会与系统提供的 DPI 有所偏差。

问题说完了,那么如何利用这套 dp 适配的机制,达到完美适配呢?

density、densityDpi、scaledDensity 这些属性都是可以进行修改的,我们要利用这个搞点事情。

dp 适配偏差都是由于 DPI 不准确,导致 density 缩放系数不准确,出现的问题,如果我们拿到精准的缩放比例,是不是就能将 dp 完美转换为 px 了啊?

我们可以换一下思路,我们要求美工给出一套以 dp 为单位的基准图,例如:基准宽为 320dp,运行在 1080*1920 的设备上。

1. 运行时获取到设备精准的 density

我们可以很轻易的得到缩放系数是 1080 / 320,也就是3.375。

float newDensity = dm.widthPixels / 320;

不要问我,如何在运行时获取到宽高这样的参数,直接看 DisplayMetrics 里面的属性。

/**
 * The absolute width of the available display size in pixels.
 */
public int widthPixels;
/**
 * The absolute height of the available display size in pixels.
 */
public int heightPixels;

这是就很清晰了吧,有了缩放系数,我们就可以精准的将 1dp 转换为 3.375px 了,达到完美适配。

2. 运行时获取到设备精准的 scaledDensity

得到 newDensity 以后,同样也要获取 newScaledDensity 的值,才能做到对字体 sp 的适配。

float newScaledDensity =  newDensity * (dm.density / dm.scaledDensity);

这个还好理解的吧,因为默认状态 density 和 scaledDensity 是相等的,在修改过系统字体大小后,scaledDensity 会进行缩放,所以要将该比例考虑进去。

3. 计算 densityDpi

得到了缩放系数,在设置到 DisplayMetrics 之前,千万不要忘记对 densityDpi 也要进行计算,如果不修改 densityDpi 的话,程序内部计算会出现问题,因为源码中:

density =  DENSITY_DEVICE / (float) DENSITY_DEFAULT;

所以我们要反向计算出 densityDpi 的值:

int newDensityDpi =  (int) (targetDensity * DisplayMetrics.DENSITY_DEFAULT);

4. 设置 DisplayMetrics 参数

终于到了最后一步,修改属性值:

dm.density = newDensity;
dm.scaledDensity = newScaleDensity;
dm.densityDpi = newDensityDpi;

这样,就可以使用 dp 值完美的适配各种屏幕了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342