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

1,609 阅读9分钟

系列介绍

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

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

抱佛脚的目的只有一个,就是斩获自己期望中的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中所示:

imgimg

<?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

  • dp360=pxsW\frac{dp}{360}=\frac{px }{ sW}
  • dp360=dpdensitysW\frac{dp}{360}=\frac{dp*density}{sW}
  • density=sW360density = \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演示下适配前后的样子:

imgimg

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

字体适配

当然文章中还提到了文本大小的适配,也就是 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. 了解了今日头条屏幕适配的原理