今日头条屏幕适配方案落地研究

原文做者:但,我知道,连接: http://www.javashuo.com/article/p-ytgqrafz-en.htmlhtml

目录

前言

你们好,如今给你们推荐一种极低版本的 Android 屏幕适配方案,就是今日头条适配方案,“极低成本”这四个字正是今日头条的适配文章标题。android

众所周知,安卓的屏幕碎片化极其严重,适配一直是从事安卓开发人员十分头疼的事情。前期,因为公司支持的平板款式单一,只须要作几款平板的适配便可,选用了 smalledtWidth(最小宽度)适配,可是这个方案在增长新屏幕时且原 dimens 文件没法很好适配时,就须要增长新屏幕的最小宽度 dimens 文件了,比较麻烦并且会增长项目大小(虽然只是几个文件),并且这种屏幕适配极度依赖设备的屏幕密度,叫density。为了讲解更清楚,这里须要引入几个公式:面试

px = density * dp dp : 安卓开发人员经常挂在嘴上的长度单位 px : 设计人员眼中的长度单位 density = dpi / 160 所以,px = dp * (dpi/160) dpi : 根据屏幕真实分辨率和尺寸计算得出 举个例子:屏幕分辨率为 1920 * 1080,屏幕尺寸为5寸(屏幕斜边长度cm/0.3937), 则 dpi = √(宽度²+ 高度²)/屏幕尺寸app

所以,屏幕密度相当重要,屏幕密度怎么来的?厂商写入一个 system/build.prop 文件,有时还会写错,就咱们一款华为平板,获取的屏幕密度是2,可是手工测量并按公式获得实际屏幕密度是1.56。致使咱们的适配方案在那款平板就失效了。框架

本人一直在寻找能够一劳永逸的屏幕适配方案,今日头条是选定基准分辨率,基于设备屏幕分辨率计算出新的屏幕密度进行适配,保证全部设备的显示效果一致,完美避开上面那款设备的问题。推荐给你们。ide

各平板数据比较

首先,我详细记录了公司主流设备的参数,新方案确定要对主流设备都能完美适配,这才是入门门槛。 q  | 三星N5100-4.1| 三星p355c-6.0(基准) | 华为-8.0 ---|---|---|--- 真实宽度(px)| 800 | 768 | 1200 真实高度(px) | 1280 | 1024| 1852 原始 density | 1.33125 | 1.0 | 2.0(不许,实际1.56 ) new density | 1.04166 | - | 1.5625 new height(px) | 1066 | - | 1600布局

能够看到横向是几种设备,竖向是一些参数,其中中英文混杂,这是为何呢?这是我故意的,中文是设备原始参数,英文是根据今日头条方案原理计算的。由于,今日头条的目的是全部设备的显示效果一致。可是设备的分辨率是不一样的,怎么显示一致呢?简单述之,就是缩放,按宽度缩放的。可能有人会有疑问,缩放后的效果图放不下,显示不完整怎么办?性能

咱们看看上面的数据,能够看到按照三星6.0基准进行缩放,效果图在三星4.1这款设备宽度上的显示,是按768乘以new density ,也就是 1.04166 进行放大,不用按计算器了,就是800px,完美适配。那么高度呢,1024 也乘以 new density,发现是1066px,比实际高度像素值 1280px 小,不会出现显示不全的现象。可能有人会问了,这不是多出来了么,会不会留空白啊?对,好问题,因此合格的开发在竖向布局上增长自适应权重,以应对这种状况。固然,横向也须要考虑自适应权重。学习

同理,可得知效果图在华为8.0设备的宽度像素是 1600px, 也比实际设备宽度 1852px 小,也能显示彻底。测试

为何看起来更小了?(头条方案跟最小宽度方案比较)

对的,跟原先的比起来,是更小了,包括图片更小,文字更小。这是为何呢?且听我细细道来... ...

你们都知道,安卓有 mdpi、hdpi、xhdpi后缀的文件,具体使用有 drawable-mdpi、drawable-hdpi,或者mipmap-mdpi、mipmap-hdpi, 又或者 values-mdpi、values-hdpi, 这些都是安卓自带的屏幕适配方案,只是不太好用吗,常常出问题。那么,这些文件都是怎么使用的呢,这又涉及到了屏幕密度这个属性,关联以下:

dpi 屏幕密度
drawable-ldpi 0.75
drawable-mdpi 1(baseline)
drawable-hdpi 1.5
drawable-xhdpi 2
drawable-xxhdpi 3
drawable-xxxdpi 4
  1. 平板A 三星平板5100 的屏幕密度是1.33125,大于mdpi,小于hdpi,向上取整,因此属于hdpi
  2. 平板B 三星平板P355C 的屏幕密度是1,属于mdpi
  3. ldpi:mdpi:hdpi:xhdpi:xxhdpi:xxxdpi = 0.75:1:1.5:2:3:4 = 3:4:6:8:12:16
  4. 上述比值乘以12,就是 36:48:72:144:192,恰好就是icon尺寸
  5. 咱们会看到,最小宽度适配方案,values-hdpi 的值是 values-mdpi 的值乘以 0.8

0.8 的参数

  1. 宽高100dp的正方形图片,平板A会显示100px,平板B会乘以1.5,显示成150px,致使偏大
  2. 因为平板B的屏幕密度是 1.33125, 最好 显示成 100* 1.33125
  3. 1.33125/ 1.5 = 0.8875 约为 0.8

sw600dp-dpi

  1. sw : small width,就是最小宽度是600dp,
  2. px -> dp : dp = px / density
  3. 平板A: 800 /1.33125 = 600.93
  4. 平板B: 768/1 = 768 上述两个平板,一个是600dp,一个是768dp,都是大于600dp,平板A使用sw600dp-hdpi,平板B使用sw600dp-mdpi

最后称述

平板A、B 同时显示一个 100px 的图片:

  1. 按最小宽度适配:100 * 1.5 * 0.8 = 120 ,图片会显示成 120px
  2. 按今日头条适配: 100 * 1.04166 = 104.166,图片会显示成 104.166 px
  3. 因此今日头条方案显示的图片就更小了。

那么,哪一个更好呢?咱们再来看看一个极端,显示一个 平板B 的填满宽度的图片, 768px:

  1. 按最小宽度适配:768px * 1.5 * 0.8 = 921.6px ,图片会显示成 921.6px, 远远超出平板A的尺寸,此时开发人员须要手动干预
  2. 按今日头条适配: 768px * 1.04166 = 799.99488,图片能够当作显示成 800 px
  3. 优势很明显,布局更简单

严谨的你,可能会问了,那显示超过768px呢? 很差意思,咱们的基准就是 768,不会超过他了。

smallesWidth 方案迁移

咱们原项目使用的是 smallestWidth 方案,经试验迁移代价很低,经研究有以下两个方案。

  1. 删除全部适配 smallestWidth 的dimens 文件夹,只保留dp 值是1:1 的 dimens 文件便可;
  2. 不想删除亦可,将全部的 dimens 文件都覆盖成 dp 值是1:1 的 dimens 文件便可

优缺点

优势

  1. 使用成本很是低,操做很是简单,使用该方案无需增长dimens 文件,修改代码,完虐其余屏幕适配方案
  2. 侵入性很是低,切换几乎瞬间完成,试错成本接近为0
  3. 修改的 density 是全局的,一次修改,终生受益。
  4. 不会有任何性能的损耗
  5. 今日头条 大厂保证

缺点

一、 第三方布局库, 未按项目效果图布局,全局修改 density 致使修改第三方布局,形成显示界面问题 二、与 smallestwith 适配方案不兼容,切换回来比较麻烦

issue

一个 Bitmap 的density 问题

在某处,开启今日头条适配方案,全局修改屏幕密度,获取 ImageView 的 Bitmap 的宽高,发现获取的宽高和实际的宽高(布局出来观察)不一致。经查阅源码,发现 Bitmap 也有一个 density, 怀疑未被修改。

随决定,修改 sDefaultDensity 值,查阅代码,发现 sDefaultDensity 是静态私有,因而召唤反射大法

测试 Ok, 收工。

附录(适配核心代码)

  • initAppDensity 方法 Application 调用,记录默认屏幕密度
  • setDefault 和 setOrientation 方法 Activity 调用,设置新的屏幕密度
  • resetAppOrientation 方法,恢复屏幕密度

// * ================================================
    // * 本框架核心原理来自于 <a href="https://mp.weixin.qq.com/s/d9QCoBP6kV9VSWvVldVVwA">今日头条官方适配方案</a>
    // * <p>
    // * 本框架源码的注释都很详细, 欢迎阅读学习
    // * <p>
    // * 任何方案都不可能完美, 在成本和收益中作出取舍, 选择出最适合本身的方案便可, 在没有更好的方案出来以前, 只有继续忍耐它的不完美, 或者本身做出改变
    // * 既然选择, 就不要抱怨, 感谢 今日头条技术团队 和 张鸿洋 等人对 Android 屏幕适配领域的的贡献
    // * <p>
    // * ================================================
    // */

    private static final int WIDTH = 1;
    private static final int HEIGHT = 2;
    private static final float DEFAULT_WIDTH = 768f; //默认宽度
    private static final float DEFAULT_HEIGHT = 1024f; //默认高度
    private static float appDensity;
    /**
     * 字体的缩放因子,正常状况下和density相等,可是调节系统字体大小后会改变这个值
     */
    private static float appScaledDensity;
    /**
     * 状态栏高度
     */
    private static int barHeight;
    private static DisplayMetrics appDisplayMetrics;
    private static float densityScale = 1.0f;

    /**
     * application 层调用,存储默认屏幕密度
     *
     * @param application application
     */
    public static void initAppDensity(@NonNull final Application application) {
        //获取application的DisplayMetrics
        appDisplayMetrics = application.getResources().getDisplayMetrics();
        //获取状态栏高度
        barHeight = getStatusBarHeight(application);
        if (appDensity == 0) {
            //初始化的时候赋值
            appDensity = appDisplayMetrics.density;
            appScaledDensity = appDisplayMetrics.scaledDensity;

            //添加字体变化的监听
            application.registerComponentCallbacks(new ComponentCallbacks() {
                @Override
                public void onConfigurationChanged(Configuration newConfig) {
                    //字体改变后,将appScaledDensity从新赋值
                    if (newConfig != null && newConfig.fontScale > 0) {
                        appScaledDensity = application.getResources().getDisplayMetrics().scaledDensity;
                    }
                }

                @Override
                public void onLowMemory() {
                }
            });
        }
    }

    /**
     * 此方法在BaseActivity中作初始化(若是不封装BaseActivity的话,直接用下面那个方法就行了)
     *
     * @param activity activity
     */
    public static void setDefault(Activity activity) {
        setAppOrientation(activity, WIDTH);
    }

    /**
     * 好比页面是上下滑动的,只须要保证在全部设备中宽的维度上显示一致便可,
     * 再好比一个不支持上下滑动的页面,那么须要保证在高这个维度上都显示一致
     *
     * @param activity    activity
     * @param orientation WIDTH HEIGHT
     */
    public static void setOrientation(Activity activity, int orientation) {
        setAppOrientation(activity, orientation);
    }

    /**
     * 重设屏幕密度
     *
     * @param activity    activity
     * @param orientation WIDTH 宽,HEIGHT 高
     */
    private static void setAppOrientation(@NonNull Activity activity, int orientation) {

        float targetDensity;

        if (orientation == HEIGHT) {
            targetDensity = (appDisplayMetrics.heightPixels - barHeight) / DEFAULT_HEIGHT;
        } else {
            targetDensity = appDisplayMetrics.widthPixels / DEFAULT_WIDTH;
        }

        float targetScaledDensity = targetDensity * (appScaledDensity / appDensity);
        int targetDensityDpi = (int) (160 * targetDensity);
        // 最后在这里将修改事后的值赋给系统参数,只修改Activity的density值
        DisplayMetrics activityDisplayMetrics = activity.getResources().getDisplayMetrics();
        activityDisplayMetrics.density = targetDensity;
        activityDisplayMetrics.scaledDensity = targetScaledDensity;
        activityDisplayMetrics.densityDpi = targetDensityDpi;

        densityScale = appDensity / targetDensity;
        setBitmapDefaultDensity(activityDisplayMetrics.densityDpi);
    }

    /**
     * 重置屏幕密度
     *
     * @param activity activity
     */
    public static void resetAppOrientation(@NonNull Activity activity) {
        DisplayMetrics activityDisplayMetrics = activity.getResources().getDisplayMetrics();
        activityDisplayMetrics.density = appDensity;
        activityDisplayMetrics.scaledDensity = appScaledDensity;
        activityDisplayMetrics.densityDpi = (int) (appDensity * 160);

        densityScale = 1.0f;
        setBitmapDefaultDensity(activityDisplayMetrics.densityDpi);
    }

    /**
     * 获取状态栏高度
     *
     * @param context context
     * @return 状态栏高度
     */
    private static int getStatusBarHeight(Context context) {
        int result = 0;
        int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
        if (resourceId > 0) {
            result = context.getResources().getDimensionPixelSize(resourceId);
        }
        return result;
    }

    /**
     * 设置 Bitmap 的默认屏幕密度
     * 因为 Bitmap 的屏幕密度是读取配置的,致使修改未被启用
     * 全部,放射方式强行修改
     * @param defaultDensity 屏幕密度
     */
    private static void setBitmapDefaultDensity(int defaultDensity) {
        //获取单个变量的值
        Class clazz;
        try {
            clazz = Class.forName("android.graphics.Bitmap");
            Field field = clazz.getDeclaredField("sDefaultDensity");
            field.setAccessible(true);
            field.set(null, defaultDensity);
            field.setAccessible(false);
        } catch (ClassNotFoundException e) {
        } catch (NoSuchFieldException e) {
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    /**
     * 屏幕密度缩放系数
     *
     * @return 屏幕密度缩放系数
     */
    public static float getDensityScale() {
        return densityScale;
    }

阅读更多

面试官:你分析过线程池源码吗?

屏幕适配之尺寸的相关概论《一》

Android屏幕适配框架-(今日头条终极适配方案)

高仿安卓「填空题」控件!手撸一个炫酷的View动效

回顾我两个月面试阿里,携程,小红书,美团,网易等等

相关文章
相关标签/搜索