Android屏幕适配-终结者

Android屏幕适配专题

Android屏幕适配-必备知识

Android屏幕适配-终结者

前言

屏幕适配问题一直在开发中存在,没有一种完美的解决方案。Android 的碎片化很严重。

下面这张图片所显示的内容足以充分说明当今Android系统碎片化问题的严重性,因为该图片中的每一个矩形都代表着一种Android设备。

109779420.jpg

而随着支持Android系统的设备(手机、平板、电视、手表)的增多,设备碎片化、品牌碎片化、系统碎片化、传感器碎片化和屏幕碎片化的程度也在不断地加深。而我们今天要探讨的,则是对我们开发影响比较大的——屏幕的碎片化。

下面这张图是Android屏幕尺寸的示意图,在这张图里面,蓝色矩形的大小代表不同尺寸,颜色深浅则代表所占百分比的大小。

ab2d4007f2c7e9806436184f8b80a4d0.jpg

而与之相对应的,则是下面这张图。这张图显示了IOS设备所需要进行适配的屏幕尺寸和占比。

109779430.jpg

如何解决

  • Smallest Width适配
  • DisplayMetrics.densityDpi属性修改

1. Smallest Width适配

什么是Smallest Width适配

smallestWidth适配,或者叫sw限定符适配。指的是Android会识别屏幕可用高度和宽度的最小尺寸的dp值(其实就是手机的宽度值),然后根据识别到的结果去资源文件中寻找对应限定符的文件夹下的资源文件。

举个例子,小米5的dpi是480,横向像素是1080px,根据px=dp(dpi/160),横向的dp值是1080/(480/160),也就是360dp,系统就会去寻找是否存在value-sw360dp的文件夹以及对应的资源文件。如果找不到,系统就会去向下寻找,下面的图就会找到 value-sw320dp的文件夹。

image-20190927164627952

这套方案是最接近完美的方案。 首先,从开发效率上,它不逊色于任意一种方案

根据固定的放缩比例,我们基本可以按照UI设计的尺寸不假思索的填写对应的dimens引用。

我们还有以375个像素宽度的设计稿为例(iOS 设计稿),在values-sw375dp文件夹下的diemns文件应该怎么编写呢?

这个文件夹下,意味着手机的最小宽度的dp值是375,直接按着 1:1的比例写就好,那么接下来的事情就很简单了,假如设计稿上出现了一个20dp*20dp的TextView,那么,我们就可以不假思索的在layout文件中写下对应的尺寸。

<TextView
        android:layout_width="@dimen/x20"
        android:layout_height="@dimen/x20"
        android:layout_marginTop="@dimen/x20"
        android:background="@color/colorAccent"
        android:text="Hello World!" />

values-sw375dp 目录下的 dimens.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <dimen name="x20">20dp</dimen>
</resources>

那么设计稿为 375 个像素宽度,那么有没有办法直接可以生成其他限定符文件夹呢?

img

Smallest Width终极解决方案 screen-plugin

使用方法

添加 jcenter()仓库,在项目的根 build.gradle 中添加

buildscript {
    dependencies {
        classpath 'com.android.tools.build:gradle:3.5.0-alpha13'
        // 在此处添加
        classpath 'vip.ruoyun.plugin:screen-plugin:1.0.0'
    }
}

在要使用插件的的子项目的 build.gradle 中添加

apply plugin: 'vip.ruoyun.screen'

screen {
    smallestWidths 320, 360, 384, 392, 400, 410, 411, 432, 480 //生成的目标屏幕宽度的适配文件
    designSmallestWidth 375 //苹果设计稿750 × 1334   屏幕宽度为 375
    decimalFormat "#.#" //设置保留的小数 ( #.## 保留2位) ( #.# 保留1位)
    log false //是否打印日志
    auto false //是否每次 build 项目的时候自动生成 values-sw[]dp 文件
}

如果 auto 设置为 true ,则每次 build 项目的时候自动生成 values-sw[]dp 文件

如果 auto 设置为 false,则可以通过命令行,来生成文件.

./gradlew dimensCovert

也可以在 gradle命令的 窗口中 点击 dimensCovert 的 task.

img

自动生成的sw 文件

img

生成规则:只会生成 dp 后缀的属性值,根据 values 目录下的 dimens.xml,生成具体的文件。 values 目录下的 dimens.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <dimen name="x20">20dp</dimen>
    <dimen name="x30">20sp</dimen>
</resources>

生成的目标文件,values-sw320dp 目录下的 dimens.xml 文件

<?xml version="1.0" encoding="UTF-8"?>
<resources>
    <dimen name="x20">17.1dp</dimen>
</resources>

包体积

因为是按着一对一的方式进行生成,所以对于最后生成的 apk 来说,只是增加了几 k的大小,完全不必担心会增加包体积。�

完美

2. DisplayMetrics.densityDpi 属性修改

img

发现过程

通过阅读源码,我们可以得知,density 是 DisplayMetrics 中的成员变量,而 DisplayMetrics 实例通过 Resources#getDisplayMetrics 可以获得,而Resouces通过Activity或者Application的Context获得。

先来熟悉下 DisplayMetrics 中和适配相关的几个变量:

  • DisplayMetrics#density 就是上述的density

  • DisplayMetrics#densityDpi 就是上述的dpi

  • DisplayMetrics#scaledDensity 字体的缩放因子,正常情况下和density相等,但是调节系统字体大小后会改变这个值

那么是不是所有的dp和px的转换都是通过 DisplayMetrics 中相关的值来计算的呢?

首先来看看布局文件中dp的转换,最终都是调用 TypedValue#applyDimension(int unit, float value, DisplayMetrics metrics) 来进行转换:

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;
}

这里用到的DisplayMetrics正是从Resources中获得的。

再看看图片的decode,BitmapFactory#decodeResourceStream方法:

public static Bitmap decodeResourceStream(Resources res, TypedValue value,
        InputStream is, Rect pad, Options opts) {
    validate(opts);
    if (opts == null) {
        opts = new Options();
    }

    if (opts.inDensity == 0 && value != null) {
        final int density = value.density;
        if (density == TypedValue.DENSITY_DEFAULT) {
            opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
        } else if (density != TypedValue.DENSITY_NONE) {
            opts.inDensity = density;
        }
    }

    if (opts.inTargetDensity == 0 && res != null) {
        opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
    }

    return decodeStream(is, pad, opts);
}

PhoneWindow的getDimension方法

public float getDimension(@DimenRes int id) throws NotFoundException {
    final TypedValue value = obtainTempTypedValue();
    try {
        final ResourcesImpl impl = mResourcesImpl;
        impl.getValue(id, value, true);
        if (value.type == TypedValue.TYPE_DIMENSION) {
            return TypedValue.complexToDimension(value.data, impl.getDisplayMetrics());
        }
        throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id)
                + " type #0x" + Integer.toHexString(value.type) + " is not valid");
    } finally {
        releaseTempTypedValue(value);
    }
}

当然还有些其他dp转换的场景,基本都是通过 DisplayMetrics 来计算的,这里不再详述。因此,想要满足上述需求,我们只需要修改 DisplayMetrics 中和 dp 转换相关的变量即可。

这个方案侵入性很低,而且也没有涉及私有API,是个极不错的方案,我暂时也想不到强行修改density是否会有其他影响,既然有今日头条的大厂在用,稳定性应当是有保证的。

根据我的实践,这套方案对任何项目来说都是完美的,因为修改了系统的density值之后,整个布局的实际尺寸都会发生改变,如果想要在老项目文件中使用,那么可以把DisplayMetrics#density设置成原来你项目中的设计图的宽度。因此,如果你是在维护或者改造老项目,直接使用这套方案就可以了。


DisplayMetrics.densityDpi 终极解决方案 screen-helper

在项目的根 build.gradle 中添加 jcenter 仓库

然后在子项目中的 build.gradle 文件中添加

dependencies {
    implementation 'vip.ruoyun.helper:screen-helper:1.0.2'
}

使用,在每个Activity 重写getResources()方法。

public class MainActivity extends AppCompatActivity {
    @Override
    public Resources getResources() {
        return ScreenHelper.applyAdapt(super.getResources(), 450f, ScreenHelper.WIDTH_DP);
    }
}

如果是悬浮窗适配,因为 inflate 用到的 context 是 application 级别的,所以需要在自定义的 Application 中重写 getResource。

public class App extends Application {
    @Override
    public Resources getResources() {
        return ScreenHelper.applyAdapt(super.getResources(), 450f, ScreenHelper.WIDTH_DP);
    }
}

类型

  • ScreenHelper.WIDTH_DP 以 dp 来适配,在 xml 中使用 dp 单位
  • ScreenHelper.WIDTH_PT 以 pt 来适配,在 xml 中使用 pt 单位
  • ScreenHelper.HEIGHT_PT 以 pt 来适配,在 xml 中使用 pt 单位

版本变化

  • 1.0.2 :优化传递 ScreenMode 的参数传递,去除不必要的 log
  • 1.0.1 :优化 Resources.getSystem() 变量获取,由于此方法是 synchronized ,如果频繁调用会影响性能
  • 1.0.0 :正式发版

源码地址

https://github.com/bugyun/ScreenHelper

img

如果你喜欢我的文章,可以关注我的掘金、公众号、博客、简书或者Github!

简书: https://www.jianshu.com/u/a2591ab8eed2

GitHub: https://github.com/bugyun

Blog: https://ruoyun.vip

掘金: https://juejin.im/user/56cbef3b816dfa0059e330a8/posts

CSDN: https://blog.csdn.net/zxloveooo

欢迎关注微信公众号

image

推荐阅读更多精彩内容

  • 背景 之前基于头条的适配方案写了篇文章 Android 屏幕适配从未如斯简单,但后续发现还是有挺多坑的,这些坑都记...
    Blankj阅读 14,939评论 26 286
  • ps: 适配啊对于 Android 来说永远不会过时 相关概念 屏幕尺寸 含义:手机对角线的物理尺寸 单位:英寸(...
    前行的乌龟阅读 1,497评论 0 20
  • 更新:由于该适配方案越来越多人使用,也有很多人遇到不太理解的问题。所以为了大家更好的使用,我将文章很多内容更新了,...
    请叫我代码小王子阅读 1,060评论 0 2
  • 本文记录一些适配问题的研究,基础概念不做过多介绍。 Android在做屏幕适配的时候一般考虑两个因素:分辨率和dp...
    developerzjy阅读 5,463评论 1 24
  • ps:19 年第一篇,新年开头新气象,祝愿大家新的一年有好心情,好工作,事业顺利,感情稳健,身体健康,大吉大利~ ...
    前行的乌龟阅读 376评论 0 10