【Android面试速学】安卓屏幕适配

系列介绍

临时抱佛脚(也说急来抱佛脚) 指平时不烧香,遇到危难时才求佛保佑。比喻事到临头才慌忙想办法应付。

少壮没有努力,所以现在知识不给力。

抱佛脚的目的只有一个,就是斩获自己期望中的offer.

灵魂拷问:你们 Android 开发的时候,对于 UI 稿的 px 是如何适配的?

我只会:dp加上自适应布局以及weight布局比例来适配(也就是传统屏幕适配方案)。

我的ui适配知识深度止步于此。

简直是十分非常太菜了。

名词解释

dpi :像素密度是屏幕单位面积内的像素数,称为 dpi(每英寸的点数)。通常以尺寸作为手机大小衡量单位,所以dpi计算公式为 : 对角线px/ 手机尺寸 。也就是如下图所示

img

目录

  1. 大家都在用的屏幕适配方案
  2. 今日头条屏幕适配方案学习
  3. 官方屏幕适配方案
  4. 头条方案的第三方加强版 AndroidAutoSize
  5. 总结

一,大家都在用的屏幕适配方案

传统适配方案解释

Android官方提供了 dp单位来适配屏幕,传统适配方案中,我们通常会结合约束布局和weight比例来实现布局的还原。

dp和px的转换

android中的dp在渲染前会将dp转为px,计算公式:

  • px = density * dp;

  • density = dpi / 160;

  • px = dp * (dpi / 160);

屏幕的dpi则是每单位尺寸像素密度。

dpi计算公式查看名词解释部分。

传统方案问题

由dpi计算方式可知,dp可以实现大部分相似宽高屏幕,以及相似比例像素密度的ui适配。但却不是完全的屏幕等比关系。

同样的20dp可能占用屏幕的宽高比例会因手机而异。

这就导致了不同设备之间通过约束和dp适配可能会呈现不同的效果。

如下图demo中所示:

img
img
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <ImageView
        android:src="@drawable/ic_launcher_background"
        android:layout_width="match_parent"
        android:scaleType="centerCrop"
        android:layout_height="200dp"
        app:layout_constraintTop_toTopOf="parent"
        android:layout_margin="150dp"/>

</androidx.constraintlayout.widget.ConstraintLayout>

可以看到相同的布局文件,在不同尺寸相同dpi的设备中,表现就十分迥异。
可见对于各种形状和大小的安卓手机,ui展示变得奇怪,也就十分常见了。

传统方案的优势

传统适配方案按照像素密度来做显示适配。这样在同样像素密度,不同屏幕大小的设备中,会有这一致的大小体验。

比如一个小屏手机中的块,在大屏设备中也是相似的大小。
这样就可以在大屏设备中也就能显示更多的内容,在做好多布局适配的情况下,能有更符合美学的展示效果。
而不是粗暴的等比放大。

二,今日头条开源屏幕适配方案使用

官方文章链接

方案目标

从文章中可知,今日头条的目标是,以宽度为基准,等比还原设计图。

这样在宽度大同小异的手机设备中,将会有更加优秀的还原体验。

方案原理

该方案是为了以宽度为基准还原ui图。

前面提到 px和dp的转换是 px = dp * density; 而想要让所有手机的宽度都有一样的dp。只需要修改density的值,就能转换出想要的px。

  1. 计算出density

比如 当以360dp为设计宽度基准的时候。

需要的 density 计算方式如下:

dp 为设计dp
sW 为屏幕宽度px

  • \frac{dp}{360}=\frac{px }{ sW}
  • \frac{dp}{360}=\frac{dp*density}{sW}
  • density = \frac{sW}{360}
  1. 找到要替换density的值对象

通过阅读源码,我们知道布局文件中dp转成px

  • 首先会调用 TypedArray#getDimensionPixelSize
  • 然后调用 TypedValue#complexToDimensionPixelSize
  • 最终调用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;
}

可以看到常用的 dp和sp转换是使用的 metrics.densitymetrics.scaledDensity

头条文章中也说,还有些其他dp转换的场景,基本都是通过DisplayMetrics 来计算的。不再赘述。

所以我们只需要替换resource.mMetrics 的density 和 densityDpi 以及 scaledDensity 的值就行了

  1. 替换application和activity的 resource.mMetrics

替换之后,系统转换px的时候自然就会使用该density了,和简单就实现了以屏幕宽度为基准的适配方案。

项目使用

//动态代理减少模板代码
inline fun <reified T> noOpDelegate(): T {
    return Proxy.newProxyInstance(
        ClassLoader.getSystemClassLoader(),
        arrayOf(T::class.java)
    ) { _, _, _ ->

    } as T
}

class App : Application() {
    override fun onCreate() {
        super.onCreate()
        fun setDisplay(resources: Resources) {
            with(resources.displayMetrics) {
                val originDensityRatio = scaledDensity / density
                density = widthPixels / 360f
                densityDpi = (density * 160).toInt()
                scaledDensity *= originDensityRatio
            }
        }
        ///修改app.resources.displayMetrics
        setDisplay(resources)
        ///字体改变回调
        registerComponentCallbacks(object : ComponentCallbacks by noOpDelegate() {
            override fun onConfigurationChanged(newConfig: Configuration) {
                if (newConfig.fontScale > 0) {
                    setDisplay(resources)
                }
            }
        })
        ///修改activity.resources.displayMetrics
        registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks by noOpDelegate() {
            override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
                setDisplay(activity.resources)
            }
        })
    }
}

在添加之后,就可以看到手机完美得以360dp为全屏宽度基准了

用我的小米10演示下适配前后的样子:

img
img

可以看到效果还是挺不错的。

字体适配

当然文章中还提到了文本大小的适配,也就是 scaledDensity的值计算。

文章中的计算原理如下:

通过计算之前scaledDensity和density的比获得现在的scaledDensity

最后调用 Application#registerComponentCallbacks 注册下 onConfigurationChanged 监听
,在字体变化的时候去更新scaledDensity。

代码参考前面 项目使用 的代码

头条适配问题思考

头条适配方案以宽度为基准,适合手机设备,按宽度比例还原设计图,达到美观的效果。

然后这个方案的问题也很明显。在大屏设备上,也是按照宽度等比还原设计图。

这就导致了app大屏幕运行就只是手机的放大版,完全没有体验性可言

解决方案:我思考了一下,这就需要在检测屏幕为大屏幕的时候,主动取消该适配方案。并做对应布局更换适配处理。

而更换的大屏适配方案,可以使用 下一节的 smalllest 屏幕适配方案

三,官方屏幕适配方案-限定符 or .9png

首先放上官方说明

smallestWidth 限定符

最小宽度限定符是谷歌官方支持的屏幕多布局多资源方案。

使用“最小宽度”屏幕尺寸限定符,您可以为具有最小宽度(以密度无关像素 dp 或 dip 为度量单位)的屏幕提供备用布局。

比如:

    res/layout/main_activity.xml           # 用于手机设备 (小于 600dp 屏幕宽度的设备)
    res/layout-sw600dp/main_activity.xml   # 用于 7寸平板 (600dp 或者更宽屏幕的设备)

最小尺寸参考

最小宽度限定符指定屏幕两侧的最小尺寸,而不考虑设备当前的屏幕方向,因此这是一种指定布局可用的整体屏幕尺寸的简单方法。

下面是其他最小宽度值与典型屏幕尺寸的对应关系:

  • 320dp:典型手机屏幕(240x320 ldpi、320x480 mdpi、480x800 hdpi 等)。

  • 480dp:约为 5 英寸的大手机屏幕 (480x800 mdpi)。

  • 600dp:7 英寸平板电脑 (600x1024 mdpi)。

  • 720dp:10 英寸平板电脑(720x1280 mdpi、800x1280 mdpi 等)。

再结合屏幕方向限定符

如下:

 res/layout-land/main_activity.xml           # For handsets in landscape
 res/layout-sw600dp/main_activity.xml         # For 7” tablets

这样就能完成大多数场景下的屏幕适配工作

.9 png 九宫格位图

普通位图在放大或者缩小时候,会失真,被拉伸挤压。

解决方案是使用九宫格位图,这种特殊格式的 PNG 文件会指示哪些区域可以拉伸,哪些区域不可以拉伸,以及安全的内容区域。

九宫格位图基本上是一种标准的 PNG 文件,但带有额外的 1 像素边框,指示应拉伸哪些像素(并且带有 .9.png 扩展名,而不只是 .png)。

安卓框架默认支持。

这种开发者基本上很常用。不再赘述,不清楚的可以百度。

屏幕分辨率限定符(不建议使用)

该限定符官方文档并没有说明,我也找到了一篇文章,从解析源码处入手分析了该限定符的用法和逻辑。链接在这。该作者是个大佬,直接扒了框架的源码,羡慕大佬的厉害中。

本人以前使用的时候也是云里雾里的。

资源文件夹用法如下 :

- values-480x320
- layout-480x320

现在总结一下改文章的结论:

  1. 分辨率限定的优先级排序十分靠后,仅仅先于平台版本。
  2. 分辨率限定会排除任一维度大于实际分辨率的配置。比如有 2020x1080、1080x740以及960x540限定的资源。一台1920x1080的手机,会排除掉2020x1080的资源,匹配1080x740或960x540中的一个资源。

限定分辨率总结:

这个限定符生效逻辑十分诡异,有悖常理,不是只适配特定分辨率,而是会影响到所有完全大于该分辨率的屏幕,用了得不偿失,建议不要使用。

四,头条方案的第三方加强版 AndroidAutoSize

首先送上的是作者的介绍文章

AndoridAutoSize库是作者根据头条屏幕适配方案,经过不断的优化和扩展完善的一个屏幕适配库。里面支持了 dp、sp、pt、in、mm 等各种单位进行布局。
因为没有详细查阅该库源码,所以不在这里进行赘述。写在这里作为一个备选方案以备后续使用学习

五,总结

在手机场景下,可以使用头条的适配方案完美还原手机的设计稿。

而在大屏或者特殊屏幕尺寸场景,可以使用sw 和 方向限定符结合,使用不同的layout 或者dimens文件夹,将会有不错的结果。

通过这次屏幕适配方案,我也有不少收获

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

推荐阅读更多精彩内容