你们好,如今给你们推荐一种极低版本的 Android 屏幕适配方案,就是今日头条适配方案,“极低成本”这四个字正是今日头条的适配文章标题。app
众所周知,安卓的屏幕碎片化极其严重,适配一直是从事安卓开发人员十分头疼的事情。前期,因为公司支持的平板款式单一,只须要作几款平板的适配便可,选用了 smalledtWidth(最小宽度)适配,可是这个方案在增长新屏幕时且原 dimens 文件没法很好适配时,就须要增长新屏幕的最小宽度 dimens 文件了,比较麻烦并且会增长项目大小(虽然只是几个文件),并且这种屏幕适配极度依赖设备的屏幕密度,叫density。为了讲解更清楚,这里须要引入几个公式:框架
px = density * dp
dp : 安卓开发人员经常挂在嘴上的长度单位
px : 设计人员眼中的长度单位
density = dpi / 160
所以,px = dp * (dpi/160)
dpi : 根据屏幕真实分辨率和尺寸计算得出
举个例子:屏幕分辨率为 1920 * 1080,屏幕尺寸为5寸(屏幕斜边长度cm/0.3937), 则 dpi = √(宽度²+ 高度²)/屏幕尺寸ide
所以,屏幕密度相当重要,屏幕密度怎么来的?厂商写入一个 system/build.prop 文件,有时还会写错,就咱们一款华为平板,获取的屏幕密度是2,可是手工测量并按公式获得实际屏幕密度是1.56。致使咱们的适配方案在那款平板就失效了。布局
本人一直在寻找能够一劳永逸的屏幕适配方案,今日头条是选定基准分辨率,基于设备屏幕分辨率计算出新的屏幕密度进行适配,保证全部设备的显示效果一致,完美避开上面那款设备的问题。推荐给你们。性能
首先,我详细记录了公司主流设备的参数,新方案确定要对主流设备都能完美适配,这才是入门门槛。
| 三星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 小,也能显示彻底。ui
对的,跟原先的比起来,是更小了,包括图片更小,文字更小。这是为何呢?且听我细细道来... ...
你们都知道,安卓有 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
平板A、B 同时显示一个 100px 的图片:
那么,哪一个更好呢?咱们再来看看一个极端,显示一个 平板B 的填满宽度的图片, 768px:
严谨的你,可能会问了,那显示超过768px呢?
很差意思,咱们的基准就是 768,不会超过他了。
咱们原项目使用的是 smallestWidth 方案,经试验迁移代价很低,经研究有以下两个方案。
今日头条 大厂保证
与 smallestwith 适配方案不兼容,切换回来比较麻烦
在某处,开启今日头条适配方案,全局修改屏幕密度,获取 ImageView 的 Bitmap 的宽高,发现获取的宽高和实际的宽高(布局出来观察)不一致。经查阅源码,发现 Bitmap 也有一个 density, 怀疑未被修改。
public int mDensity = getDefaultDensity(); ... ... static int getDefaultDensity() { if (sDefaultDensity >= 0) { return sDefaultDensity; } sDefaultDensity = DisplayMetrics.DENSITY_DEVICE; return sDefaultDensity; }
随决定,修改 sDefaultDensity 值,查阅代码,发现 sDefaultDensity 是静态私有,因而召唤反射大法
/** * 设置 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(); } }
测试 Ok, 收工。
// * ================================================ // * 本框架核心原理来自于 <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; }