专栏首页Android技术分享一种非常简单的Android屏幕适配方案

一种非常简单的Android屏幕适配方案

前言

作为一个Android开发人员,你还在为了适配各种尺寸的屏幕而苦恼吗?你还在为了出现一个新的机型而修改着数不尽的dimens和layout吗?你还在为了UI给的奇葩尺寸的设计图而绞尽奶汁计算距离吗?如果你为了这些事情而苦恼,那么看完这篇文章,希望可以帮你减少开发时间,减缓生命的流逝速度。。。

image.png

不知道大家有没有看过前一段时间今日头条技术团队发表的一篇关于Android屏幕适配的文章:一种极低成本的Android屏幕适配方式。没有看过的朋友可以先看看了解一下再回来,可以更好的理解。我是无意中点开的这篇文章,但是看过之后眼前一亮——-Android屏幕适配要是真的这么简单,那些辛辛苦苦没日没夜做适配的前辈们是不是死得太惨了。。。。。。。

不比比太多,开搞!

测试与思考

不得不说今日头条的大神们的想法真的非常独到,成本极其低廉,还特别好用。他们给出的最终方案是这样的:

private static float sRoncompatDennsity;
private static float sRoncompatScaledDensity;

private void setCustomDensity(@NonNull Activity activity, final @NonNull Application application) {

    //application
    final DisplayMetrics appDisplayMetrics = application.getResources().getDisplayMetrics();
    if (sRoncompatDennsity == 0) {
        sRoncompatDennsity = appDisplayMetrics.density;
        sRoncompatScaledDensity = appDisplayMetrics.scaledDensity;
        application.registerComponentCallbacks(new ComponentCallbacks() {
            Override
            public void onConfigurationChanged(Configuration newConfig) {
                if (newConfig != null && newConfig.fontScale > 0) {
                    sRoncompatScaledDensity = application.getResources().getDisplayMetrics().scaledDensity;
                }
            }

            @Override
            public void onLowMemory() {

            }
        });
    }

    //计算宽为360dp 同理可以设置高为640dp的根据实际情况
    final float targetDensity = appDisplayMetrics.widthPixels / 360;
    final float targetScaledDensity = targetDensity * (sRoncompatScaledDensity / sRoncompatDennsity);
    final int targetDensityDpi = (int) (targetDensity * 160);

    appDisplayMetrics.density = targetDensity;
    appDisplayMetrics.densityDpi = targetDensityDpi;
    appDisplayMetrics.scaledDensity = targetScaledDensity;

    //activity
    final DisplayMetrics activityDisplayMetrics = activity.getResources().getDisplayMetrics();

    activityDisplayMetrics.density = targetDensity;
    activityDisplayMetrics.densityDpi = targetDensityDpi;
    activityDisplayMetrics.scaledDensity = targetScaledDensity;
}

看到这篇文章之后我赶紧就写了一个demo测试了一下,发现了一点小问题。 我们UI给出的设计图尺寸为1334*720,如果我按照宽度作为适配标准的话,按照设计图720px的宽度,屏幕的宽度应为360dp,也就是这样:

final float targetDensity = appDisplayMetrics.widthPixels / 360;

这样做的话宽度适配的比例是没有任何问的,但是我在想,如果某一个页面需要以高度来做适配(也就是内容刚好纵向填充全屏)的话,是不是改成这样就可以了:

final float targetDensity = appDisplayMetrics.heightPixels / 667;

但是运行之后发现,高度上的差异很大,运行在不同分辨率和尺寸的手机上,页面中的每一部分内容在纵向上的比例不尽相同,没有达到很好的适配的效果。 思考了许久过后我发现一个问题:我手边的测试机的宽度是两个720和两个1080,而高度有1280,1440,1780和一个全面屏的2160。Android的开原性导致了Android设备的尺寸的碎片化太严重,而通过查看测试机的尺寸参数会发现,如果用这四个手机来测试的话,宽度可以直接整除,而高度不可以(并且我手边的测试机的宽度也可以整除,如果有宽度没法整除的手机呢?)。但是用今日头条给出的方法,做除法后结果会取整,那会不会是由于用纵向计算出来的density取整影响了精度,从而导致了效果不尽人意呢?

来,继续改

问题修复

发现上述问题之后我就着手去修改,将计算结果取余后在赋值给targetDensity,经过反复的测试与实验,我重新修改了targetDensity的计算方法:

float targetDensity = 0;
try {
    //appDisplayMetrics.heightPixels/667
    Double division = Operation.division(appDisplayMetrics.heightPixels, 667);
    //由于手机的长宽不尽相同,肯定会有除不尽的情况,有失精度,所以在这里把所得结果做了一个保留两位小数的操作
    DecimalFormat df = new DecimalFormat("0.00");
    String s = df.format(division);
    targetDensity = Float.parseFloat(s);
} catch (NumberFormatException e) {
    e.printStackTrace();
}

但是有热心的网友给我留言,说如果把系统的语言改成葡萄牙语之后会报异常,页面完全就是惨不忍睹。。。抱着对外国历史友人的疑问我继续去测试,发现了这样的问题:

image.png

仔细看!!!小数点竟然是中文逗号(目前发现的语言当中,葡语和印尼语是这样,其他语言未经测试)!!!没办法,只能继续修改了,后来经过这位热心网友提醒,我将这部分代码修改成:

float targetDensity = appDisplayMetrics.heightPixels / 667f;

这样一来就完全没有问题了,也不需要做保留两位小数处理了(发现问题的朋友们,原谅我学艺不精。。。也感谢这位热心网友的指正)

继续测试后发现,高度上的适配结果让人非常满意。可是还有一个问题,我们一般来说做适配都是以手机的宽度为基准,但是一个app里面避免不了偶尔一两个页面是按照高度为基准(就是内容纵向填充全屏的页面)做适配的。但是上述方法只能保证一个方向,那我就让它可以自由的切换适配的基准方向不就好了。

最终方案

继续修改之后我得到了最终的方案,修改过后这个类中的所有内容如下:

private static float appDensity;
private static float appScaledDensity;
private static DisplayMetrics appDisplayMetrics;
private static int barHeight;

public static void setDensity(@NonNull Application application) {
    //获取application的DisplayMetrics
    appDisplayMetrics = application.getResources().getDisplayMetrics();
    //获取状态栏高度
    barHeight = AppUtils.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的话,直接用下面那个方法就好了)
public static void setDefault(Activity activity) {
    setAppOrientation(activity, AppUtils.WIDTH);
}

//此方法用于在某一个Activity里面更改适配的方向
public static void setOrientation(Activity activity, String orientation) {
    setAppOrientation(activity, orientation);
}

/**
 * targetDensity
 * targetScaledDensity
 * targetDensityDpi
 * 这三个参数是统一修改过后的值
 * <p>
 * orientation:方向值,传入width或height
 */
private static void setAppOrientation(@Nullable Activity activity, String orientation) {

    float targetDensity;

    if (orientation.equals("height")) {
        targetDensity = (appDisplayMetrics.heightPixels - barHeight) / 667f;
    } else {
        targetDensity = appDisplayMetrics.widthPixels / 360f;
    }

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

在这个类的初始化方法里面我是默认的以宽度来作为基准(这是在Activity中设置的方法,存在于此Activity下的fragment,dialog和PopupWindow都会受到此效果的影响,也就是说,在Activity中设置一次之后,Activity下的其他子View都无需再设置一次)。

使用方法

自己创建一个类,将最终方案里面的代码复制粘贴就可以使用了

使用方法:在Application的onCreate()方法中:

image.png

在BaseActivity中:

image.png

如果只是适配一个方向的话,只设置这两句就可以了(我在utils里面设置了默认按照宽度适配,可以根据自己的需求修改默认的适配方向,见下图)

image.png

若app中有某一个页面需要纵向适配的话(注意代码中的注释):

/**
 *
 * 由于是个人封装,此方法需要写在onCreate()中的setContentView()方法前面,切换方向的效果才会生效
 */
@Override
public void setOrientation() {
    Density.setOrientation(this, AppUtils.HEIGHT);
}

image.png

最后贴出适配的效果图(颜色只是为了看的直观一点。。。)

image.png

敲黑板!!!

使用此方法,只需要一个dimens文件,一个layout文件就足矣,在xml布局中直接只用dp就可以了(Android P的刘海屏需要单独适配layout)

结语

由于是自己写的demo,还没有大面积测试(但是今日头条已经启用了该方案,现在一个多月过去了还没听说出现什么问题,那就说明应该还可以),要是各位看官有条件大范围测试的话,出现什么问题可以反馈给我,我们可以一起讨论该如何修改,共同进步。 有写的不好的地方欢迎指正,以后还会继续努力多写文章的,好的东西需要分享。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Android屏幕适配很难嘛?其实也就那么回事

    作为一个Android开发人员,你还在为了适配各种尺寸的屏幕而苦恼吗?你还在为了出现一个新的机型而修改着数不尽的dimens和layout吗?你还在为了UI给的...

    Android技术干货分享
  • Android自定义View,画一个好看带延长线的饼状图

    在Android中,图表的实现是比较麻烦的,基本只能通过自定义View来实现。目前Github上有一些集成度高功能性强的三方库,比如MPAndroidChart...

    Android技术干货分享
  • 2019预备BAT大厂Android研发岗秋招必问30+道高级面试题(附详细答案解析)

    如今安卓开发不像前几年那么热门,但是高级人才依然紧缺,大家看着这句话是不是很熟悉,因为 web 高级人才也紧缺,c++ 高级人才一样紧缺,那么到了人工智能时代,...

    Android技术干货分享
  • main的MySQL系列之:尬聊buffer pool

    MySQL大体分为Server层和存储引擎层,内置函数都是Server层实现,跨存储引擎的功能如存储过程、视图、触发器等也是在Server层实现的。

    老李秀
  • iOS开发系列——纯代码实现iOS开发

    storyboard进行界面设计固然不错,特别是对于初学者经常会使用设计器进行界面设计,但是实际开发过程中我们很多情况下会直接使用代码进行界面布局,特别是对于复...

    Kris大鲨
  • 不可错过的CMS学习笔记

    带着问题去学习一个东西,才会有目标感,我先把一直以来自己对CMS的一些疑惑罗列了下,希望这篇学习笔记能解决掉这些疑惑,希望也能对你有所帮助。

    kirito-moe
  • 纽约时报计划将业务迁移到公有云

    日前据悉,具有165年历史的美国新闻出版机构《纽约时报》为了使其数字化基础设施实现现代化,计划将其业务从三个数据中心迁出,转向公有云。

    云资讯小编
  • 不用甘特图,你做什么项目管理

    当你根据以上的规则绘制好第一版甘特图以后,你会发现有些地方是可以继续调整的,但是这种调整,在你没有画图之前是不能发现的。于是你会在调整甘特图的过程中,让项目的规...

    青南
  • Power BI轻松获取网页信息,小白都能学会的技能

    在大数据,数据分析,商务智能等概念日益火热的今天,仅仅只会用Excel处理表格是远远不够的。今天介绍商务智能分析的一大利器——Power BIDesktop

    CDA数据分析师
  • 被解放的姜戈03 所谓伊人

    在之前的程序中,我们直接生成一个字符串,作为http回复,返回给客户端。这一过程中使用了django.http.HttpResponse()。 在这样的一种回复...

    Vamei

扫码关注云+社区

领取腾讯云代金券