美文网首页屏幕适配Android技术知识程序员
一种非常简单的Android屏幕适配方案

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

作者: 06fd4cf1f427 | 来源:发表于2019-03-15 22:53 被阅读232次

    前言

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

    【附】相关架构及资料
    Android高级技术大纲

    资料领取

    点赞+加群免费获取!Android 架构设计③群

    领取获取往期Android高级架构资料、源码、笔记、视频。高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter全方面的Android进阶实践技术

    相关文章

      网友评论

        本文标题:一种非常简单的Android屏幕适配方案

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