美文网首页
【Android面试速学】安卓屏幕适配

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

作者: 吃人的锅 | 来源:发表于2021-06-26 17:45 被阅读0次

    系列介绍

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

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

    抱佛脚的目的只有一个,就是斩获自己期望中的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. 了解了今日头条屏幕适配的原理

    相关文章

      网友评论

          本文标题:【Android面试速学】安卓屏幕适配

          本文链接:https://www.haomeiwen.com/subject/zfmzyltx.html