前言
开发板这类设备主要以横屏为主且有的设备在交互上支持触屏和遥控器,屏幕适配相对于手机端的适配可简单也可复杂,如果要达到像扩展性高、维护简单和性能高的要求则需要花费一番功夫。
主流的屏幕适配方案有:
- 限制符适配方案。主要有宽高限定符与
smallestWidth限定符适配方案,具体可以参考《Android 目前稳定高效的 UI 适配方案》、《smallestWidth 限定符适配方案》 - 今日头条适配方案。通过反射修正系统的
density值,具体可以参考《一种极低成本的 Android 屏幕适配方式》、《今日头条适配方案》。
我目前所开发的应用的多分辨率适配方案是基于限制符适配 方案结合开发板 设备特点和三套UI设计图的适配方案,主要从图片适配和布局适配两方面进行。
在UI适配的处理方式上做了一些转变,从一个分辨率设备对应一套资源(图片、布局和尺寸等)的方式到一套资源对应多个分辨率设备的方式,也可以理解为从一对一到一对多解决方案的转变,主要目的是减少资源重复,降低新增分辨率方案时的适配难度。
根据三套UI设计图,我们对二十多种分辨率的设备进行分类,分类的基准就是高/宽的比值更接近三套设计图中的哪一套,因此得出下面的表格:
| 1024*768(H/W≈) | 1280*800(H/W≈) | 1280*720(H/W≈) |
|---|---|---|
| 1024*768(0.75) | 1280*800(0.625) | 1280*720(0.5625) |
| 800*600(0.75) | 758*480(0.633) | 960*540(0.5625) |
| 1280*1024(0.8) | 800*480(0.6) | 1024*552(0.5390) |
| - | 1024*600(0.5859) | 1024*576(0.5625) |
| - | 1136*640(0.5633) | 1280*672(0.525) |
| - | 1280*752(0.58575) | 1366*720(0.527) |
| - | 1280*768(0.6) | 1366*768(0.5622) |
| - | 1440*900(0.625) | 1600*900(0.5625) |
| - | 1680*1050(0.625) | 1920*1080(0.5625) |
根据高/宽 的比值来划分不同设备的UI设计图参考归属,所以就有了范围 1024x768(>=0.75)、1280x800(>0.5625 & <= 0.625)和 1280x720(<= 0.5625)的划分 因此我们得到几个临界点值
java
public static final float SCREEN_RATIO_1280x1024 = 0.8f;
public static final float SCREEN_RATIO_1024x768 = 0.75f;
public static final float SCREEN_RATIO_1280x800 = 0.625f;
public static final float SCREEN_RATIO_1280x720 = 0.5625f;
public static final float SCREEN_RATIO_1366x720 = 0.528f;
1280*1024(0.8)、1366*720(0.527)和 1280*672(0.525)存在个别页面存在不协调的问题,因此单独创建布局做一些特殊处理。
图片适配
手机端的图片适配一般是根据设备的DPI来进行适配,他们之间的对应关系:
| DPI | 比值 | 举例 |
|---|---|---|
| mdpi | 1 | 16 |
| hdpi | 1.5 | 24 |
| xhdpi | 2 | 32 |
| xxhdpi | 3 | 48 |
| xxxhdpi | 4 | 64 |
手机端的设计图一般是按照720px为基准来设计,相应的png图片资源下载对应的倍率的图片放到对应的drawable-xxdpi下即可,如果是矢量图则更简单,用Android Studio的Vector Drawables功能做转换后,直接放到drawable下即可。
开发板的设备特点是大部分的设备的DPI=160,也就是mdpi,如果采用宽高限定符的适配方式会造成同一张图片存在于不同的分辨率的图片配置下,这无疑会增大安装包的大小,这也是我一开始就摈弃一个分辨率方案对应一套资源的适配方案的原因之一。
smallestWidth(简称sw)限定符 适配方案在这里是比较合适的,最小宽度值 的计算方式是min(widht,height)/density,根据分辨率分类后的设备特点定义不同的drawable-swXXXdp图片资源文件夹,然后以720、768和800为基准,在对应的drawable-swXXXdp里放入对应比例大小的png图片,少数设备DPI=223也就是hdpi(≈240)的,最小宽度值 计算后跟现有的drawable-swXXXdp冲突的,只要在资源文件夹后加上相应后后缀即可,例如drawable-sw600dp-mdpi和drawable-sw600dp-hdpi,图片大小的比值按照上述表格里的比值适当放大或缩小即可。
图片适配最典型的适配场景就是控件聚焦 的图片适配,对于中大型的聚焦图片完全是按照以上介绍的适配方式进行,除此之外我根据聚焦控件的特点归纳了三种使用场景,封装了自定义View(可参考MarketFocusView),然后把这三种场景对应的聚焦图片转换成.9图,进一步的降低了这些相似图片的重复率。其实图片适配也可以不按照最小宽度值 来进行等比例的放大或缩小,只保留一份放在drawable-mdpi下即可,项目中很多小型的图片就是这样做的,拿图片占用内存来说,减少重复或相似图片或者缩小图片大小(图片占用内存=宽高 每个像素占用字节)都能起到内存优化的作用,.9则稍微特殊一些,必须要按照最小宽度值来等比例的进行大小转换。
针对
drawable、layout或values等资源文件夹,通过宽高限定符 或**sw限定符会产生很多带有相应 后缀的资源文件夹,对于同一类型的资源 宽高限定符** 和sw限定符 不能同时存在,资源匹配的优先级是sw限定符 > 宽高限定符。
布局适配
跟图片的适配类似,本质上是同一个UI界面在不同分辨率下控件的尺寸、边距和字体大小等等比例的放大或缩小,所以就需要对基础的dp和sp根据不同分辨率的最小宽度值 比例进行换算,这里用到了ScreenMatch插件,可以根据默认values下的dimen.xml文件和插件配置文件来生成对应分辨率的dimens.xml文件,它的生成规则也是按照最小宽度值 的方式来进行适配,不过因为需要适配三套UI设计图,也就是有720、768和800三个参考基准,因此需要根据上述的设备分类来生成三份插件配置文件,又因为dimens.xml里的dp和sp是统一命名的,在尺寸资源的匹配规则是使用宽高限定符。
有了dp和sp的适配后,因为在Fragment中使用了ViewBinding,所以没办法在onCreateView方法里根据设备分类来inflate不同的布局,不过好在我所有布局的根 Layout 都是ConstraintLayout,根据ConstraintLayout的特性,可以在运行时使用ConstraintSet来匹配不同的布局样式
java
// 默认的xml布局是参考1024x768的设计图实现的
float screenRatio = ScreenUtils.getScreenRatio(requireContext());
if(screenRatio >= Constants.SCREEN_RATIO_1280x1024) {
ConstraintSet set1280x1024 = new ConstraintSet();
set1280x1024.clone(requireContext(), R.layout.fragment_xxx_1280x1024);
TransitionManager.beginDelayedTransition(mDetailsBinding.clId);
set1280x1024.applyTo(mDetailsBinding.clId);
} else if (screenRatio <= Constants.SCREEN_RATIO_1366x720) {
ConstraintSet set1366x720 = new ConstraintSet();
float realRatio = ScreenUtils.getScreenRatioByWM(requireContext());
set1366x720.clone(requireContext(), R.layout.fragment_xxx_1366x720);
TransitionManager.beginDelayedTransition(mDetailsBinding.clId);
set1366x720.applyTo(mDetailsBinding.clId);
} else if (screenRatio <= Constants.SCREEN_RATIO_1280x720) {
ConstraintSet set1280x720 = new ConstraintSet();
set1280x720.clone(requireContext(), R.layout.fragment_xxx_1280x720);
TransitionManager.beginDelayedTransition(mDetailsBinding.clId);
set1280x720.applyTo(mDetailsBinding.clId);
} else if (screenRatio <= Constants.SCREEN_RATIO_1280x800) {
ConstraintSet set1280x800 = new ConstraintSet();
set1280x800.clone(requireContext(), R.layout.fragment_xxx_1280x800);
TransitionManager.beginDelayedTransition(mDetailsBinding.clId);
set1280x800.applyTo(mDetailsBinding.clId);
}
一些控件的属性像TextView的textSize的变动是没法通过ConstraintSet来设置的,需要额外使用代码来弥补这方面的不足。
RecyclerView的Item布局则是在自定义ViewGroup中根据分类inflate不同的xml布局实现
java
private void inflateLayout(Context context) {
float screenRatio = ScreenUtils.getScreenRatio(context);
if (screenRatio >= Constants.SCREEN_RATIO_1280x1024) {
LayoutInflater.from(context).inflate(R.layout.layout_xxx_item_1280x1024, this, true);
} else if (screenRatio <= Constants.SCREEN_RATIO_1280x720) {
LayoutInflater.from(context).inflate(R.layout.layout_xxx_item_1280x720, this, true);
} else if (screenRatio <= Constants.SCREEN_RATIO_1280x800) {
LayoutInflater.from(context).inflate(R.layout.layout_xxx_item_1280x800, this, true);
} else {
LayoutInflater.from(context).inflate(R.layout.layout_xxx_item, this, true);
}
}
至此适配方案成型,后续只要对各个界面逐一进行上面的处理即可。
适配困境
在开发板 上做屏幕适配在没有全局审视 的状态下或者不需要一次性适配20+分辨率的情况,一个分辨率方案对应一套资源的适配方式是比较合适的,从现在的时间点来看当时我在屏幕适配上也挣扎了一些时间。
主要原因有:
- 前期不清楚手机端和开发板端的屏幕适配差异
- 适配方案在开发阶段不断的调整,至少经历了
3-4次改进 - 设备紧张,适配在摸索阶段并不能很快的去验证,造成效率比较低
后续通过设备分辨率分类和创建TV模拟器查看UI效果才算真正解决屏幕适配的问题,这套适配方案缺点是在开发期间适配步骤比较繁琐,不过一旦完成配置后续几乎很少改动,并且大大降低了新分辨率的适配周期。