由于 Android 碎片化严重, 屏幕适配一直是开发中较为头疼的问题. 面对市面上五花八门的屏幕大小与分辨率,Android 基于 dp 与 res 目录名称来适配的方案已无法满足一次编写全屏幕适配的需求, 为了达到最优的视觉效果, 开发过程中总是需要花费较多资源进行适配.
一, 现状
由于 Android 碎片化严重, 屏幕适配一直是开发中较为头疼的问题. 面对市面上五花八门的屏幕大小与分辨率,Android 基于 dp 与 res 目录名称来适配的方案已无法满足一次编写全屏幕适配的需求, 为了达到最优的视觉效果, 开发过程中总是需要花费较多资源进行适配. 也有开发者给出了一些自己的解决方案. 首先来分析一下一些常见的解决方案的现状:
官方适配方案
dp.dp 是 Android 开发中特有的一个单位. 与 px 不同, dp 是基于屏幕像素密度的一种单位. 在密度低的屏幕上或许 1dp=1px, 但在密度高的屏幕上可能 1dp=4px. 编写布局 xml 时, 如果一个控件的长宽都使用 dp 来指定, 那么能确保该控件在各种大小与分辨率的屏幕下的绝对大小都大致相当. 也就是说无论在 pad 下还是大小屏手机下, 我们实际看到的该控件的大小是差不多的:
资源目录名. 上图可见虽然使用 dp 确保了控件在不同屏幕中的绝对大小一致. 这样的好处在于, 在大小相近的屏幕中,无论分辨率多大都不会对布局造成影响; 但是当屏幕大小相差较大时, 仅保证控件的绝对大小看起来就有些问题了. 在 res 目录下可以给各资源目录都加上例如'-1920x1080'等后缀来适配不同的屏幕, 具体规则可见官网文档. 这样可以针对不同的屏幕提供不同的布局, 甚至针对 pad 与手机提供两套完全不同的布局样式. 但是通常情况下, 设计师并不会对不同屏幕提供不同的设计图, 他们的需求仅仅是不同屏幕下控件对屏幕的相对大小一致, 所以 dp 并不能满足这一点, 而对各种屏幕适配一遍又显得略为繁琐, 并且修改也较为麻烦. 通常我们需要的适配是这样的:
百分比布局支持库. 没有使用过, 但是 deprecated in API level 26.0.0-beta1.
ConstraintLayout. 百分比支持库 deprecated 之后推荐使用的布局, 看起来似乎略复杂.
玩家适配方案. 广大玩家的适配目的很明确, 目的就是要确保控件在不同屏幕的相对大小一致, 看起来一毛一样的.以一位大神玩家的两种适配方案为例:
方案一.编写脚本将长度转换成各分辨率下的长度, 缺点是难以覆盖市面上的所有分辨率.
方案二. AutoLayout 支持库. 该库的想法非常好: 对照设计图, 使用 px 编写布局, 不影响预览; 绘制阶段将对应设计图的 px 数值计算转换为当前屏幕下适配的大小; 为简化接入, inflate 时自动将各 Layout 转换为对应的 AutoLayout, 从而不需要在所有的 xml 中更改. 但是同时该库也存在以下等问题:
扩展性较差. 对于每一种 ViewGroup 都要对应编写对应的 AutoLayout 进行扩展, 对于各 View 的每个需要适配的属性都要编写代码进行适配扩展;
在 onMeasure 阶段进行数值计算. 消耗性能, 并且这对于非 LayoutParams 中的属性存在较多不合理之处. 比如在 onMeasure 时对 TextView 的textSize 进行换算并 setTextSize, 那么玩家在代码中动态设置的 textSize 都会失效, 因为在每次 onMesasure 时都会重新被AutoLayout 重新设置覆盖.
issue 较多并且作者已不再维护.
二, 想法
对于大小差异较大的屏幕, 本不该使用同一套设计方案, 否则大屏的优势没有完全体现出来, 从官方的适配方案也似乎是表达了这个意思. 但是在实际设计与开发中, 对于一个普通的 App, 很少有项目有意愿有精力来对各屏幕来分别设计与开发一套设计方案来适配.
通常的一个简单的适配需求是: 假如设计图宽度为 200, 一个控件在设计图上标注的长度为 3, 那么该控件长度相当于总宽度的 3/200, 那么我们希望在任何大小的屏幕上该控件所表现的长度都为屏幕宽度的 3/200.
个人觉得 AutoLayout 的设计思想非常优秀, 但是将 LayoutParams 与属性作为切入口在 mesure 过程中进行转换计算的方案存在效率与扩展性等方面的问题. 那么 Android 计算长度的收口在哪里, 能不能在 Android 计算长度时进行换算呢? 如果能在 Android 计算长度时进行换算, 那么就不需要一系列多余的计算以及适配, 一切问题就都迎刃而解了.
经过一番寻觅, 发现系统进行长度计算的收口为TypedValue 中的 applyDimension 函数, 传入单位与 value 将其计算为对应的 px 数值.
- public static float applyDimension(int unit, float value,
- DisplayMetrics metrics)
- {
- switch (unit) {
- case COMPLEX_UNIT_PX:
- return value;
- case COMPLEX_UNIT_DIP:
- return value * metrics.density;
- case COMPLEX_UNIT_SP:
- return value * metrics.scaledDensity;
- case COMPLEX_UNIT_PT:
- return value * metrics.xdpi * (1.0f/72);
- case COMPLEX_UNIT_IN:
- return value * metrics.xdpi;
- case COMPLEX_UNIT_MM:
- return value * metrics.xdpi * (1.0f/25.4f);
- }
- return 0;
- }
可以看见换算方法非常简单, 而 DisplayMetrics 的所有属性都是 public 的, 不用反射就能修改;
pt 的原意是长度单位磅, 根据当前屏幕与设计图尺寸将 metrics.xdpi进行修改就可以实现将 pt 这个单位重定义成我们所需要的相对长度单位, 使修改之后计算出的 1pt 实际对应的 px / 屏幕宽度 px=1px / 设计图宽度 px.
而这个 DisplayMetrics 从哪来? 从源码中可以看出一般为 mContext.getResources().getDisplayMetrics(), 这个 mContext 即为所在 Activity;
横竖屏切换等 Configuration 的变化会导致 DisplayMetrics的重新计算还原;
px,dp 与 sp 都是平时常用的单位, 而 pt,in 与 mm 几乎没有看见过, 从这些不常见的单位下手正好可以不影响其他常用的单位.
基于以上几点, 便有了以下方案.
三, 方案
本适配方案的目标是: 完全按照设计图上标注的尺寸来编写页面, 所编写的页面在所有大小与分辨率的屏幕上都表现一致, 即控件在所有屏幕上相对于整个屏幕的相对大小都一致(看起来只是将设计图等比缩放至屏幕宽度大小).
核心. 使用冷门的 pt 作为长度单位, 按照上述想法将其重定义为与屏幕大小相关的相对单位, 不会对 dp 等常用单位的使用造成影响.
绘制. 编写 xml 时完全对照设计稿上的尺寸来编写, 只不过单位换为 pt. 假如设计图宽度为 200, 一个控件在设计图上标注的长度为 3, 只需要在初始化时定义宽度为 200, 绘制该控件时长度写为 3pt, 那么在任何大小的屏幕上该控件所表现的长度都为屏幕宽度的 3/200. 如果需要在代码中动态转换成 px 的话, 使用TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PT, value, metrics).
预览. 实时预览时绘制页面是很重要的一个环节. 以 1334x750 的设计图为例, 为了实现于正常绘制时一样的预览功能, 创建一个长为 1334 磅, 宽为 750 磅的设备作为预览, 经换算约为 21.5 英寸((sqrt(1334^2+750^2))/72).预览时选择这个设备即可.
代码处理. 在 activityonCreate 时修改 DisplayMetrics 即可, 推荐写在基类或 ActivityLifecycleCallbacks 中, 参考 github demo.
- Point size = new Point();
- activity.getWindowManager().getDefaultDisplay().getSize(size);
- context.getResources().getDisplayMetrics().xdpi = size.x / designWidth * 72f;
这样绘制出来的页面就跟设计图几乎完全一样, 无论大小屏上看起来就只是将设计图缩放之后的结果.
适配前(左图 API19 400x800, 右图 API24 1440x2560):
适配后(左图 API19 400x800, 右图 API24 1440x2560):
虽然方案比较简单, 但是为了方便使用也整理成了一个 library, 代码及 demo 见 github
来源: http://mobile.51cto.com/ahot-572355.htm