前言
作为一个 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();
- }
经测试后发现, 这样取两位小数计算过后, 高度上的适配结果让人非常满意. 可是还有一个问题, 我们一般来说做适配都是以手机的宽度为基准, 但是一个 app 里面避免不了偶尔一两个页面是按照高度为基准 (就是内容纵向填充全屏的页面) 做适配的. 但是上述方法只能保证一个方向, 那我就让它可以自由的切换适配的基准方向不就好了.
最终方案
继续修改之后我得到了最终的方案, 修改过后这个类中的所有内容如下:
- private static float appDensity;
- private static float appScaledDensity;
- private static DisplayMetrics appDisplayMetrics;
- // 此方法在 Application 的 onCreate 方法中调用 Density.setDensity(this);
- public static void setDensity(@NonNull Application application) {
- // 获取 application 的 DisplayMetrics
- appDisplayMetrics = application.getResources().getDisplayMetrics();
- if (appDensity == 0) {
- // 初始化的时候赋值(只在 Application 里面初始化的时候会调用一次)
- 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() {
- }
- });
- }
- // 调用修改 density 值的方法(默认以宽度作为基准)
- setAppOrientation(null, AppUtils.WIDTH);
- }
- // 此方法用于在某一个 Activity 里面更改适配的方向 Density.setOrientation(mActivity, "width/height");
- public static void setOrientation(Activity activity, String orientation) {
- setAppOrientation(activity, orientation);
- }
- /**
- * targetDensity
- * targetScaledDensity
- * targetDensityDpi
- * 这三个参数是统一修改过后的值
- *
- * orientation: 方向值, 传入 width 或 height
- */
- private static void setAppOrientation(@Nullable Activity activity, String orientation) {
- float targetDensity = 0;
- try {
- Double division;
- // 根据带入参数选择不同的适配方向
- if (orientation.equals("height")) {
- //appDisplayMetrics.heightPixels/667
- division = Operation.division(appDisplayMetrics.heightPixels, 667);
- } else {
- division = Operation.division(appDisplayMetrics.widthPixels, 360);
- }
- // 由于手机的长宽不尽相同, 肯定会有除不尽的情况, 有失精度, 所以在这里把所得结果做了一个保留两位小数的操作
- DecimalFormat df = new DecimalFormat("0.00");
- String s = df.format(division);
- targetDensity = Float.parseFloat(s);
- } catch (NumberFormatException e) {
- e.printStackTrace();
- }
- float targetScaledDensity = targetDensity * (appScaledDensity / appDensity);
- int targetDensityDpi = (int) (160 * targetDensity);
- /**
- *
- * 最后在这里将修改过后的值赋给系统参数
- *
- * (因为最开始初始化的时候, activity 为 null, 所以只设置 application 的值就可以了...
- * 所以在这里判断了一下, 如果传有 activity 的话, 再设置 Activity 的值)
- */
- if (activity != null) {
- DisplayMetrics activityDisplayMetrics = activity.getResources().getDisplayMetrics();
- activityDisplayMetrics.density = targetDensity;
- activityDisplayMetrics.scaledDensity = targetScaledDensity;
- activityDisplayMetrics.densityDpi = targetDensityDpi;
- } else {
- appDisplayMetrics.density = targetDensity;
- appDisplayMetrics.scaledDensity = targetScaledDensity;
- appDisplayMetrics.densityDpi = targetDensityDpi;
- }
- }
这是修改之后的所有内容, 不懂的地方可以看一下里面的注释, 在里面我是默认的以宽度来作为基准(这是在 Activity 中设置的方法, 存在于此 Activity 下的 fragment,dialog 和 PopupWindow 都会受到此效果的影响, 也就是说, 在 Activity 中设置一次之后, Activity 下的其他子 View 都无需再设置一次).
使用方法
自己创建一个类, 将最终方案里面的代码复制粘贴就可以使用了
使用方法: 在 Application 的 onCreate()方法中
如果只是适配一个方向的话, 只设置这一句就可以了(我在 utils 里面设置了默认按照宽度适配, 可以根据自己的需求修改默认的适配方向, 见下图)
若 app 中有某一个页面需要纵向适配的话:
- /**
- *
- * 由于是个人封装, 此方法需要写在 onCreate()中的 setContentView()方法前面, 切换方向的效果才会生效
- */
- @Override
- public void setOrientation() {
- Density.setOrientation(this, AppUtils.HEIGHT);
- }
- /**
- *
- * 如果在一个 Activity 里面切换了适配方向的话, 需要在 destroy 里面将方向设置为默认的方向,
- * 因为切换方向修改的是 Activity 的值, 但是 application 的也会覆盖掉(原因还没有搞清楚...),
- * 权衡利弊之后就在 onDestroy 这个生命周期里面重新初始化了一下方向(因为用高度作为适配基准的页面
- * 少之又少, 这样可以最大程度的减少对程序功能性的影响)
- */
- @Override
- protected void onDestroy() {
- super.onDestroy();
- Density.setOrientation(this, AppUtils.WIDTH);
- }
由于在某一个 Activity 里面切换方向之后, 我修改掉的是 Activity 中的值(activityDensity), 但是返回再点击其他页面之后发现其他页面的适配方向也被修改掉了, 于是乎权衡利弊之后我就用了这个相对来说影响最小的办法: 在需要修改适配方向的 Activity 中的 onDetroy 生命周期里面, 再手动将方向改成默认...(捣鼓了很久实在是想不到更好的办法了, 如果各位看官有其他的好办法可以给我留言).
最后贴出纵向适配的效果图, 页面中蓝色背景的 TextView 高度是固定的 150dp(只是我自己写的一个很简单的页面, 不要嫌丑...):
敲黑板!!!
用此方法写适配, 只需要一个 dimens 文件, 一个 layout 文件就足矣, 在 xml 布局中直接只用 dp 就可以了(Android P 的刘海屏需要单独适配 layout, 全面屏手机可以隐藏的虚拟按键似乎也需要单独适配...)
结语
由于是自己写的 demo, 还没有大面积测试, 要是各位看官有条件大范围测试的话, 出现什么问题可以反馈给我, 我们可以一起讨论该如何修改, 共同进步. 这是我入行以来写的第一篇文章, 有写的不好的地方欢迎指正, 以后还会继续努力多写文章的, 好的东西需要分享.
来源: http://mobile.51cto.com/ahot-577347.htm