美文网首页Android收藏集安卓面试集锦面试题
Android屏幕适配很麻烦吗?不!太简单了。。。

Android屏幕适配很麻烦吗?不!太简单了。。。

作者: 劳尔_ | 来源:发表于2018-06-25 12:20 被阅读3514次

转载请作明出处:https://www.jianshu.com/p/4254ea9d1b27

前言

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


不知道大家有没有看过前一段时间今日头条技术团队发表的一篇关于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 {
        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();
    }

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



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

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()方法中



在BaseActivity中:



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

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

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

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


横向
纵向

敲黑板!!!

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

结语

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

这里是github地址,demo里面还有BaseRecyclerViewAdapterHelperQMUI的使用方法

留个联系方式,方便沟通 QQ:850701071

这篇文章如果帮到你的话,点个喜欢再走呗

相关文章

网友评论

  • f8970f41a301:收藏
    劳尔_:不胜荣幸
  • Ihesong:赞,这的确是个好想法,试了下,使用后不同手机显示效果都是一致的了。当然我觉得还可以改进下,就是在Activity中设置的setDefault(activity);可以去掉,改为在Density的setDensity中这么设置。
    application.registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() {
    @Override
    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
    //给每个Activity设置,适配屏幕分辨率
    setDefault(activity);
    }
    });
    这样一行代码就可以了,望采纳哦。
    劳尔_:@Ihesong 对对对,我一开始就是这个想法,所以没有去做统一处理
    Ihesong:@劳尔_ 理解了,可以有选择的去设置页面
    劳尔_:不不不,我知道你的意思,但是我优化这个适配方案的初衷是要在任意一个Activity里面去自由的选择适配的方向,不知道我这么说你理解了没有
  • 世道无情:不错,很吊!
    劳尔_:@世道无情 过奖了
  • BobEve:BaseActivity中mContext=this,这样不太好吧。因为如果需要Activity直接使用mActivity就好了,如果使用mContext的话,还是给一个getApplicationContext更好一点:smile:
    劳尔_:好的,谢谢指点:joy:
  • 5a2c710355d5:部分手机AlertDialog 显示右边超出屏幕
    劳尔_:@lhhseraph 什么样的情况,可以说一下么
    5a2c710355d5:@劳尔_ 你好,部分手机上(三星C7等) android.support.v7.app.AlertDialog 显示会直接靠右超出屏幕。显示不全。没有自定义View。看CSDN有网友也提出有这种情况
    劳尔_:是什么样的情况呀,能说说么
  • ac90cd96f51b:同样是九年义务教育,为何你如此优秀
    劳尔_:同样是腰间盘,为何你这么突出
  • 8ccc5d106489:华为的虚拟键盘隐藏与打开 适配不行啊
    058bff6daef8:解决了没大佬
  • 黄宏发:米8的全面屏高度适配还是有问题的
    劳尔_:方便加Q沟通一下吗
    劳尔_:什么样的问题
  • 技术特工队:老铁,不错,转载大公众号《技术特工队》,标明来源及作者?
    劳尔_:可以,没问题的
  • 当个海贼多好:不不仅要点个喜欢,我还要关注你一波 你怎么说嘛
    当个海贼多好:@当个海贼多好 如果设计图是1334*750 怎么适配啊
    当个海贼多好:@劳尔_ 话说,这个怎么用啊?就是写几个类就能达到屏幕适配的效果了?
    劳尔_:@当个海贼多好 羞涩
  • 37度开水:横屏app适配有问题
    如果app强制横屏,那么第一次安装的时候适配大小会有问题。
    第一次安装 -- 杀掉程序 -- 再次打开,显示效果是不一样的
    原因是heightPixels和widthPixels在横屏和竖屏是取到的值不一样:
    if (orientation.equals("height")) {
    targetDensity = (appDisplayMetrics.heightPixels - barHeight) / 667f;
    } else {
    targetDensity = appDisplayMetrics.widthPixels / 360f;
    }
    劳尔_:@37度开水 感谢提醒
    37度开水:@劳尔_
    DisplayMetrics dm = new DisplayMetrics();
    WindowManager windowManager = (WindowManager) activity.getSystemService(WINDOW_SERVICE);
    windowManager.getDefaultDisplay().getMetrics(dm);

    int height = 750;

    if (dm.heightPixels > dm.widthPixels) {
    division = division(dm.widthPixels, height);
    } else {
    division = division(dm.heightPixels, height);
    }

    用这种方法取出来的屏幕尺寸是正常的,可以适配横屏
    劳尔_:横屏适配我没有做
  • 03a9db709e59:德语也是被我遇到了:no_mouth:
    03a9db709e59:@劳尔_ 处理好了 主要是用户有一些外语系的学生当时没注意:flushed:
    劳尔_:哈哈哈,我已经处理好了,你那边还有问题吗
  • 60c8e0743465:你好,文中“UI给出的设计图尺寸为1334*720,如果我按照宽度作为适配标准的话,按照设计图720px的宽度,屏幕的宽度应为360dp”,这个从设计图的720px,怎么就得出360dp这个值呢?
    劳尔_:@BingoRiver 这个根据每个手机的硬件参数是不同的,算法如下
    public static int pxToDp(Context context, float px) {
    DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
    return (int) ((px / displayMetrics.density) + 0.5f);
    }
    而且这个具体的宽高也可以根据你自己的喜好去指定,我只是按照我们UI给的设计图尺寸去计算了一下,也方便我去写
    60c8e0743465:@劳尔_ 具体是哪个公式,麻烦告知一下
    劳尔_:你可以看一下px和dp之间的关系和转换公式

本文标题:Android屏幕适配很麻烦吗?不!太简单了。。。

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