Android屏幕适配的前世今生(一)

自从工作以来,一直以屏幕适配斗智斗勇。由与Android碎片化严重,存在各种奇奇怪怪的分辨率,为了开发高质量的app,必然需要尽肯能的适配多机型,其中屏幕适配就是其中一项。经过多年的磨练,学习到了一些奇技淫巧。借此机会,做个总结,也算是给自己一个交代,如果顺便能帮到一些同学,那就再好不过了。计划分成两篇文章来彻底阐述屏幕适配的前世今生。本篇先介绍下为什么需要适配,以及为下篇怎么适配提供些预备知识。

为什么需要屏幕适配?

我们以实际开发为例:

一般情况下,设计mm只会提供一套设计稿并且还是以4.7英寸的Iphone6为标准。尺寸一般是750x1334

假设我们有小米9Pixel xl两款手机,具体屏幕参数如下表:

机型 分辨率 屏幕尺寸 ppi dpi
小米9 1080x2340 6.39英寸 403 440
Pixel xl 1440x2560 5.5英寸 534 560

设计稿中有个View,宽度为375px,视觉效果为手机宽度的一半。

如果我们直接在布局文件中设计宽度为375px,那么:

小米9上,实际视觉效果仅为手机宽度的35%(375/1080);

Pixel xl上,实际效果更小,仅为手机宽度的26%(375/1440)。

显然实际效果没有还原设计稿要求,设计mm肯定会不开心了.

这时候肯定有小伙伴跳出来说:

怎么能直接用px作为单位呢,应该用Android爸爸提供的dp啊.

  1. 什么是dp

借着这个话题,我们继续来聊聊什么是dp,顺便也说说上表中的ppidpi是什么.

ppi是指每英寸像素点个数。通过对角线上像素点个数除以对角线长度(手机尺寸)得到。

小米9为例,手机尺寸是6.39英寸ppi = 403

计算公式

我们也可以得出结论,ppi是个物理值,与手机的分辨率和屏幕尺寸有关。

dpi是指屏幕像素密度,网上很多人说它也是每英寸像素点个数,实际上我并不想纠结它的定义,可以把dpi理解成人为通过软件设置的值

Android系统中会在build.prop文件中定义这个值,我们可以通过getprop ro.sf.lcd_density命令获取到这个值

getprop

同样Android也提供相应的api来获取这个值

resources.displayMetrics.densityDpi

Android框架的很多行为都是利用这个数值,比较常见的:

  • 决定我们优先使用哪个资源文件;

  • 资源文件的匹配顺序,及使用多大的缩放比例;

  • dp具体转换成多少px

这样说可能比较抽象,我们一个个举例子:

Android定义了标准的dpi及对应的屏幕像素密度等级,当dpi处于某个区间时,就会优先取对应文件夹下的资源,如下表:

dpi数值 120 160 240 320 480 640
屏幕像素密度等级 ldpi mdpi hdpi xhdpi xxhdpi xxxhdpi
缩放比 0.75x 1.0x 基准 1.5x 2.0x 3.0x 4.0x

假设我们资源目录结构如下

res/
          drawable-xxxhdpi/
            awesome-image.png  //尺寸 50x50
          drawable-xxhdpi/
            awesome-image.png  //尺寸 40x40
          drawable-xhdpi/
            awesome-image.png  //尺寸 30x30
          drawable-hdpi/
            awesome-image.png  //尺寸 20x20
          drawable-mdpi/
            awesome-image.png  //尺寸 10x10

布局文件中ImageView定义如下:

<ImageView
            android:id="@+id/mImage"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/awesome-image" />

小米9

我们在小米9手机上运行时,这个ImageView宽高是多少呢?

由与小米9手机的dpi440,处于320~480之间,那么我们的应用就会优先获取限定符为xxhdpi文件夹的资源。 又因为标准xxhpid像素密度dpi应该是480

因此在小米9手机上,这个ImageView的宽高为37x37,计算公式为:(37= 440/480 * 40)

如果我们把drawable-xxhdpi文件夹中的awesome-image.png删除,此时应用会从像素密度等级最高的xxxhdpi依次向下寻找。

由与drawable-xxxhdpi文件夹中存在该资源,因此在小米9手机上,此时ImageView的宽高为35x35,计算公式为:(35= 440/640* 50)

Pixel xl

同样的场景在Pixel xl上呢?

由于Pixel xl手机的dpi560,处于480~640之间,那么我们的应用就会优先获取限定符为xxxhdpi文件夹的资源。 又因为标准xxxhpid像素密度dpi应该是640

因此在Pixel xl手机上,ImageView的宽高为44x44,计算公式为:(44= 560/640 * 50)

如果我们把drawable-xxxhdpi文件夹中的awesome-image.png删除,此时应用会从像素密度等级最高的xxhdpi依次向下寻找。

由于drawable-xxhdpi文件夹中存在该资源,因此在Pixel xl手机上,此时ImageView的宽高为47x47,计算公式为:(47= 560/480 * 40)

我们还剩下两个疑问,dp是什么以及dp如何转为px

dp可以理解为Android设计者为了开发人员更好地做屏幕适配工作,而设计的一种虚拟像素单位。同样sp也一样。

1dp在不同dpi的手机上会表示不同大小的px,比如

在小米9上 1dp = 440/160 * 1px = 3px,

在Pixel xl上 1dp = 560/160 * 1px = 4px 。

可以发现在不同手机上(实际是不同dpi)1dp代表的像素点个数是不同的。这也是为什么有些小伙伴说布局文件中不要使用px要使用dp,以做屏幕适配。

可能有小伙伴质疑或者懵逼,凭什么是按照上面的公式计算,你是不是瞎xx编的。

好嘛,我们来看看Android源码


        // step1 ViewGroup.LayoutParams#setBaseAttributes
        protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
            // 进入step2
            width = a.getLayoutDimension(widthAttr, "layout_width");
            height = a.getLayoutDimension(heightAttr, "layout_height");
        }
        // step2 TypedArray#getLayoutDimension
        public int getLayoutDimension(@StyleableRes int index, String name) {
             .......省略无关代码    
             else if (type == TypedValue.TYPE_DIMENSION) {
                // 进入step3
                return TypedValue.complexToDimensionPixelSize(data[index + STYLE_DATA], mMetrics);
            } 
             .......省略无关代码 
        }        
        
        // step3 TypedValue#complexToDimensionPixelSize
        public static int complexToDimensionPixelSize(int data,
                DisplayMetrics metrics)
        {
            final float value = complexToFloat(data);
            // 进入step4
            final float f = applyDimension(
                    (data>>COMPLEX_UNIT_SHIFT)&COMPLEX_UNIT_MASK,
                    value,
                    metrics);
            final int res = (int) ((f >= 0) ? (f + 0.5f) : (f - 0.5f));
            if (res != 0) return res;
            if (value == 0) return 0;
            if (value > 0) return 1;
            return -1;
        }
        
        // step4 TypedValue#applyDimension
        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;
            case COMPLEX_UNIT_SP:
                return value * metrics.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;
        }

我们在xml中设置layout_width属性,属性值最终会映射到ViewGroup.LayoutParamswidth字段,具体数值通过TypedValue#applyDimension方法计算得到。

如果我们设置的单位是dp,会走到如下分支:

return value * metrics.density

显然这里的转换与DisplayMetricsdensity字段有关,我们继续看下这个类中几个关键字段的赋值。

    // DisplayMetrics#setToDefaults
    public static int DENSITY_DEVICE = getDeviceDensity();
    public static final int DENSITY_DEFAULT = DENSITY_MEDIUM;
    public static final int DENSITY_MEDIUM = 160;        
    public void setToDefaults() {
        density =  DENSITY_DEVICE / (float) DENSITY_DEFAULT;
        densityDpi =  DENSITY_DEVICE;
        scaledDensity = density;
    }
    private static int getDeviceDensity() {
        // qemu.sf.lcd_density can be used to override ro.sf.lcd_density
        // when running in the emulator, allowing for dynamic configurations.
        // The reason for this is that ro.sf.lcd_density is write-once and is
        // set by the init process when it parses build.prop before anything else.
        return SystemProperties.getInt("qemu.sf.lcd_density",
                SystemProperties.getInt("ro.sf.lcd_density", DENSITY_DEFAULT));
    }    

从源码可以看出,getDeviceDensity()就是获取build.prop中的厂商设置的dpi,三个关键字段的赋值也很清晰,如下:

densityDpi = build.prop文件中ro.sf.lcd_density的值
density = densityDpi/160
scaledDensity = density

再回到TypedValue#applyDimension方法中,很容易得出dppx的转换公式,顺便无意间也参透了sppx转换公式:

density = densityDpi/160
scaledDensity = density
px = density * dp 
px = scaledDensity * sp

如果不调整手机设置中的字体缩放比例,默认情况下 density = scaledDensity

这里需要注意的是applyDimension方法返回的是float类型,而像素是int类型,在TypedValue#complexToDimensionPixelSize中有floatint的操作:

       final int res = (int) ((f >= 0) ? (f + 0.5f) : (f - 0.5f));

其实就是简单的四舍五入。(插个题外话:我猜你项目也有UIUtils工具类,里面有dp2px方法,而方法的内容也就是类似上面那样)

对照下上面的逻辑,我们再验证之前的转换结果是否正确:

在小米9上 1dp = 440/160 * 1px = 3px,

在Pixel xl上 1dp = 560/160 * 1px = 4px 

是不是千真万确,我没骗你。

回到文章开头的例子:

设计稿尺寸为750*1334,其中有个View,宽度为375px,视觉效果为手机宽度的一半.

Iphone6理论上来讲,density是2,因此375px转换为dp为375/2=187.5dp

我们尝试使用dp作为单位,在小米9和Pixel xl上实际是什么样子呢。

// 怕你忘了公式,再来一次:
px = densityDpi/160 * dp

小米9516px = 440/160 * 187.5px,实际视觉效果仅为手机宽度的48%(516/1080);

Pixel xl656px = 560/160 * 187.5px,实际视觉效果仅为手机宽度的46%(656/1440);

是不是都比较接近50%,最起码比使用px的效果要好得多。

但还是不可避免的存在误差,设计mm肯定还是会不开心。

那我们到底怎么做才会完美还原设计稿的视觉效果呢,因为篇幅原因,下篇再来揭晓。

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