vivo 互联网客户端团队 - Xu Jie
日益新增的机型,给开发人员带来了很多的适配工作。代码能不能对立、apk 能不能对立、物料如何选取、款式怎么展现等等都是困扰开发人员的问题,本计划就是介绍不同机型的共线计划,打消开发人员的疑虑。
一、日益纷纷的机型带来的挑战
1.1 背景
科技是提高的,人们对美的要求也是逐步晋升的,所以才有了当初市面上不拘一格的机型
(1)比方 vivo X60 手机采纳纤薄曲面屏设计,属于直板机型。
(2)比方 vivo 折叠屏高端手机,提供更优质的视觉体验,属于折叠屏机型。
(3)比方 vivo pad,领有优良的操作手感和高级的质感,属于平板机型。
1.2 咱们的挑战
在此之前,咱们次要是为直板手机去服务,咱们的开发只有适配这种支流的直板机器,咱们的 UI 次要去设计这种直板手机的效果图,咱们的产品和经营次要为这种直板机型去抉择物料。
可是随着这种不拘一格机型的呈现,那么 问题 就来了:
(1)开发人员的适配老本高了,是不是针对每一种机型,都要做个独自的利用进行适配呢?
(2)UI 设计师要做的效果图要多了,是不是要针对每种机型都要设计一套效果图呢?
(3)产品和经营须要抉择的物料更受限制了,会不会这个物料在一个机器上失常。在其余机器上就不失常了呢?
为什么这么说,上面以开发者的角度来做介绍,把咱们面临的问题,做阐明。
二、开发者的困境
2.1 全机型适配老本太高
日渐丰盛的机型适配让咱们这些 android 开发人员疲于奔命,尽管能够依照要求进行适配,然而大屏幕的机型适配老本仍然比拟高,因为这些机型不同于传统的直板手机的宽高比例(9:16)。所以有的利用罗唆就间接两边留白,内容区域展现在屏幕正地方,这种成果,当然很差。
案例 1:某个视频 APP 页面,未做 pad 上的适配,关上之后的成果如下,两边大量留白,是不可操作的区域。
案例 2:某新闻资讯类 APP,在 pad 上的适配成果如下,可见的范畴内,信息流展现内容较少,图片有拉伸、含糊的问题。
2.2 全机型适配老本高在哪
下面的案例其实只是外表的问题之一,作为开发人员,须要思考的因素有很多,首先要想到这些机型有什么特点:
而后才是须要解决的问题:
三、寻找全机型适配计划之旅
3.1 计划探讨与确定
页面拉伸、左右留白是景象,这也是用户的间接体验。那么这就是咱们要改善的中央,所以当初就有方向了,围绕着“如何在可见区域内,展现更多的信息”。这不是布局的简略从新排列组合,因为 计划相对不是只有开发决定如何实现就能够怎么实现的,一个 apk 承载着性能到用户手里波及了多方角色的染指。产品经理须要整顿需要、经营人员须要配置物料、公布 apk,测试须要测试等等,所以最终的计划不是一方定下来的,而是一个协调对立后的后果。
既然要去探讨计划,那么就要有根据,在此省略探讨、评审、定稿的过程。
先来看看直板、折叠屏、pad 的内部轮廓图,晓得页面状态如何。
3.2 计划落地示意图
每个利用要展现的内容不统一,然而原理统一,此处就以上面几个款式为根底介绍原理。准则也比较简单,尽可能展现更多内容,不要呈现大面积的空白区域。
上面没有介绍分栏模式的适配,因为分栏的模式也可能被用户敞开,最终成为全屏模式,所以说,能够抉择只适配全屏模式,这样的适配老本较低。当然,这个也要依据本人模块的状况来确定,比方微信,更适宜左右屏的分栏模式。
3.2.1 直板机型适配计划骨骼图
直板机型,目前支流的机型,宽高比根本是 9:16,能够最大限度地展现比拟多的内容,比方下图中的模块 1、模块 2、模块 3 的图片。
3.2.2 折叠屏机型适配计划骨骼图
折叠屏机型,屏幕可旋转,然而宽高比根本是 1:1,高度和直板机器根本差不多,能够达到 2000px 的像素,所以在纵向上,也能够最大限度地展现比拟多的内容,比方下图中的模块 1、模块 2、模块 3 的图片。
3.2.3 PAD 机型适配计划骨骼图
pad 平板,屏幕可旋转,并且旋转后的宽高比差别较大,纵向时,宽高比是 5 : 8,横向时,宽高比是 8 : 5。
在 pad 纵向时,其实高度像素是足够展现很多内容的,比方下图中的模块 1、模块 2、模块 3 的图片;
然而在 pad 横向时,没方法展现更多的内容(倒是有个计划,最初再说),只能下图中的模块 1、模块 2 的图片。
3.3 计划落地标准
3.3.1 一套代码适配所有机型
确定一个 apk 能不能适配所有机型,首先要解决的是要合乎不同机型的个性,比方直板手机只能纵向显示,折叠屏和 pad 反对横竖屏旋转。
形容如下:
(1)需要
- 直板屏:强制固定竖屏;
- 折叠屏:外屏固定竖屏、内屏 (大屏) 反对横竖屏切换;
- PAD 端:反对横竖屏切换;
咱们须要在以上三端通过一套代码实现下面的需要。
(2)横竖屏切换
有以下 2 种办法:
- 形式 1)
通过在 AndroidManifest.xml 中设置:android:screenOrientation 属性
a) android:screenOrientation=”portrait” 强制竖屏;
b) android:screenOrientation=”landscape” 强制横屏;
c) android:screenOrientation=”unspecified” 默认值,能够横竖屏切换;
形式 2)
在代码中设置:activity.setRequestedOrientation(**);
a) setRequestedOrientation(ActivityInfo.SCREEN\_ORIENTATION\_PORTRAIT); 设置竖屏;
b)setRequestedOrientation(ActivityInfo.SCREEN\_ORIENTATION\_LANDSCAPE); 设置横屏;
c)setRequestedOrientation(ActivityInfo.SCREEN\_ORIENTATION\_UNSPECIFIED); 能够横竖屏切换;
(3)不同设施反对不同的屏幕横竖屏形式
1)直板屏:
因为是强制竖屏,所以,能够通过在 AndroidManifest.xml 中给 Activity 设置 android:screenOrientation=”portrait”。
2)折叠屏:
外屏与直板屏是保持一致的,暂且不探讨。然而内屏 (大屏) 要反对横竖屏切换。如果是一套代码,显然是无奈通过 AndroidManifest 文件来实现的。这里其实零碎框架曾经帮咱们实现了对应内屏时横竖屏的逻辑。总结就是,折叠屏能够与直板屏保持一致,在 AndroidManifest.xml 中给 Activity 设置 android:screenOrientation=”portrait”,如果切换到内屏时,零碎主动疏忽掉 screenOrientation 属性值,强行反对横竖屏切换。
3)PAD 端:
当然了,并不是所有的我的项目对应的零碎都会主动帮咱们疏忽 screenOrientation 属性值,这时候就须要咱们本人来实现了。
咱们通过在 Activity 的基类中设置 setRequestedOrientation(ActivityInfo.SCREEN\_ORIENTATION\_UNSPECIFIED),发现的确可能使以后页面横竖屏自在切换了。然而在启动 activity 的时候遇到了问题。当咱们从横屏状态 A 界面启动一个 acitivity 的 B 界面时,发现 B 界面先是竖屏,而后切换到了横屏(如图 1 所示)。再试了屡次仍旧如此,肉眼可见的切换过程显然不能满足咱们的需要。这阐明通过 java 代码动静调整横竖屏的技术方向是行不通的。综上所述,通过同一份代码无奈满足 PAD 端和直板屏的互斥的需要。
那还有没有其余形式呢。别忘了,咱们 Android 打包全流程是通过 gradle 实现的,咱们是不是能够通过切面编程的思维,针对不同的设施打出不同的包。
计划确定了,在此进行技术验证。
gradle 编译其中一个重要环节就是对依赖的 aar、本地 module 中的 AndroidManifest 文件进行 merge,最终输入一份长期的残缺清单文件,寄存在/app/build/intermediates/merged_manifest/*Release/ 门路下。
因而,咱们能够在 AndroidManifest 文件 merge 实现之后对该临时文件中的 android:screenOrientation 字段值信息进行动静批改,批改实现之后再存回去。这样针对 pad 端就能够独自打出一份 apk 文件。
外围代码如下:
//pad 反对横竖屏
def processManifestTask = project.tasks.getByName("processDefaultNewSignPadReleaseManifest");
if (processManifestTask != null) {
processManifestTask.doLast { pmt ->
def manifestPath = pmt.getMultiApkManifestOutputDirectory().get().toString() + "/AndroidManifest.xml"
if (new File(manifestPath).exists()) {String manifest = file(manifestPath).getText()
manifest = manifest.replaceAll("android:screenOrientation=\"portrait\"","android:screenOrientation=\"unspecified\"");
file(manifestPath).write(manifest)
println("=============================================================== manifestPath:" + manifestPath)
}
}
}
(4)apk 的数量
到这里为止,java 代码是完全一致,没有辨别的,要害就在于框架有没有提供出疏忽 screenOrientation 的能力,如果提供了,咱们只须要输入一个 apk,就能适配所有机型,
如果没有这个能力,咱们就须要应用 gradle 打出额定的一个 apk,满足可旋转的要求。
3.3.2 一套物料配所有机型
1、等比放大物料
通过下面的落地计划的要求,对于模块 2 的图片,展现成果是不一样的,如下图:
(1)直板手机下面,模块 2 的图片 1 在下面,图片 2、3 散布于左下角和右下角
(2)折叠屏或者 pad 下面,模块 2 的图片 1 在右边,图片 2、3 散布于右侧
(3)折叠屏和 pad 上的模块 2 的图片,绝对于直板手机来说,做了款式的调整,高低的款式改为了左右。图片也做了对应的放大,保障横向上能够填充整个屏幕的宽度。
(4)为了形象地示意解决后的成果,看下上面的示意图即可。
2、高度不变,裁剪物料
对于模块 3 的图片,能够回顾 3.2 中的展现款式,要求是
(1)直板手机下面,模块 3 中图片 1 的高度此处为 300px。
(2)折叠屏或者 pad 下面,模块 3 的图片 1 的高度也是 300px,然而内容不能缩小。
(3)解决方案就是提供一张原始大图,如果规格为 2400px*300px,在直板手机上左右进行裁剪,如下图所示。折叠屏和 pad 下面间接进行展现。而裁剪这一步,放在服务端进行,因为客户端做裁剪,比拟耗时。
(4)为了形象地示意解决后的成果,看下上面的示意图即可。
3.3.4 无感刷新
无感刷新,次要是体现在折叠屏的内外屏切换,pad 的横竖屏旋转这些场景,如何保障页面不会呈现切换、旋转时候的闪现呢?
(1)这就要提前准备好数据源,保障在页面变动时,立刻 notify。
(2)咱们的页面列表最好应用 recyclerview,因为 recyclerview 反对部分刷新。
(3)数据源驱动 UI,千万不要在 UI 层面判断机型做 UI 的动静计算,页面会闪屏,体验不好。
3.4 计划落地实战
下面介绍了不同机型的适配标准,这个没有疑难之后,间接通过案例来看下具体如何施行。
如上图所示,选购页能够大抵分为 分类导航栏区域 和 内容区域,其中内容区域是由多个楼层组成。
3.4.1 UI 如何设计的
如图所示,可能直观地感触到,从直板手机到折叠屏内屏再到 Pad 横屏,当设施的可显示面积增大时,页面充分利用空间展现更多的商品信息。
3.4.2 不同设施的辨别形式
通过后面的简略介绍,对选购页的整体布局及不同设施上的 UI 展现有所理解,上面来看下如何在多个设施上实现一套代码的适配。
首先第一步,要如何辨别不同的设施。
在辨别不同的设施前,先看下可能从设施中取得哪些信息?
1)分辨率
2)机型
3)以后屏幕的横、竖状态
先说论断:
- 直板手机:通过分辨率来辨别
- 折叠屏:通过机型和内外屏状态来辨别
- Pad:通过机型和以后屏幕的横、竖状态来辨别
所以这里依据这几个特点,提供一个工具。
不同设施的辨别形式。
/**
* @function 判断以后手机的屏幕是处于哪个屏幕类型:目前三个屏幕范畴:别离为 <= 528dp、528 ~ 696dp、> 696dp, 对应的别离是失常直板手机、折叠屏手机内屏和 Pad 竖屏、和 Pad 横屏
*/
public class ScreenTypeUtil {
public static final int NORMAL_SCREEN_MAX_WIDTH_RESOLUTION = 1584; // 失常直板手机:屏幕最大宽度分辨率;Pad 的分辨率(1600*2560),1584 = 528 * 3,528dp 是 UI 在精选页标注的直板手机范畴
public static final int MIDDLE_SCREEN_MAX_WIDTH_RESOLUTION = 2088; // 折叠屏手机:屏幕最大宽度分辨率(1916*1964,旋转:1808*2072),2088 = 696 * 3,2088dp 是 UI 在精选页标注的折叠屏开展范畴
public static final int LARGE_SCREEN_MAX_WIDTH_RESOLUTION = 2560; // 大屏幕设施:屏幕宽度暂定为 Pad 的高度
public static final int NORMAL_SCREEN = 0; // 失常直版手机屏幕
public static final int MIDDLE_SCREEN = 1; // 折叠屏手机内屏开展、Pad 竖屏
public static final int LARGE_SCREEN = 2; // Pad 横屏
public static int getScreenType() {Configuration configuration = BaseApplication.getApplication().getResources().getConfiguration();
return getScreenType(configuration);
}
// 留神这里的 newConfig 在 Activity、Fragment、View 中的 onConfigurationChanged 中取得的 newConfig 传入,如果取得不了该值,能够应用 getScreenType()办法
public static int getScreenType(@NonNull Configuration newConfig) {
// Pad 通过机型标记位及以后处于横竖屏状态 来判断以后屏幕类型
if (SystemInfoUtils.isPadDevice()) {return newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE ? LARGE_SCREEN : MIDDLE_SCREEN;}
// Fold 折叠屏 通过机型标记及内外屏状态 来判断以后屏幕类型
if (SystemInfoUtils.isFoldableDevice()) {return SystemInfoUtils.isInnerScreen(newConfig) ? MIDDLE_SCREEN : NORMAL_SCREEN;
}
// 一般手机 通过分辨率判断
return AppInfoUtils.getScreenWidth() <= NORMAL_SCREEN_MAX_WIDTH_RESOLUTION ? NORMAL_SCREEN : (AppInfoUtils.getScreenWidth() <= MIDDLE_SCREEN_MAX_WIDTH_RESOLUTION ? MIDDLE_SCREEN : LARGE_SCREEN);
}
}
3.4.3 实现计划
(1)数据源驱动 UI 扭转的思维
对于直板手机来说,选购页只有一种状态,放弃竖屏展现。
对于折叠屏来说,折叠屏能够由内屏切换到外屏,也就波及到了两种不同状态的切换。
对于 Pad 来说,Pad 反对横竖屏切换,所以也是两种不同状态切换。
当屏幕类型、横竖屏切换、内外屏切换时,Activity\Fragment\View 会调用 onConfigurationChanged 办法,因而针对直板手机、折叠屏及 Pad 能够将数据源的切换放在此处。
无论是哪种设施,最多是只有两种不同的状态,因而,数据源这里能够筹备两套:一种是 Normal、一种是 Width,对直板手机而言:因为只有一种竖屏状态,因而只须要一套数据源即可;对折叠屏而言:Normal 寄存的是折叠屏外屏数据源,Width 寄存的是折叠屏内屏数据源;对 Pad 而言:Normal 寄存的是 Pad 竖屏状态数据源,Width 寄存的是 Pad 横屏状态数据源。
(2)内容区域
右侧的内容区域是一个 Fragment,在这个 Fragment 外面蕴含了一个 RecyclerView。
每个子楼层
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/root_classify_horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<xxx.widget.HeaderAndFooterRecyclerView
android:id="@+id/shop_product_multi_rv"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
每个楼层也是一个独自的 RecyclerView,以楼层 4 为例,楼层 4 的每一行商品都是一个 RecyclerView,每个 RecyclerView 应用 GridLayoutManager 来管制布局的展示列数。
(3)数据源
以折叠屏为例:针对每个子楼层的数据,在解析时,就先筹备两套数据源:一种是 Normal、一种是 Width。
在申请网络数据回来后,在解析数据实现后,寄存两套数据源。这两套数据源要依据 UI 设计的规定来组装,例如以折叠屏的楼层 4 为例:
折叠屏 - 外屏 - 楼层 4:一行展现 2 个商品信息。
折叠屏 - 内屏 - 楼层 4:一行展现 3 个商品信息。
留神:这里的 2、3 数字是 UI 设计之初就定下来的,每行商品都是一个 RecyclerView,并且应用 GridLayoutManager 来管制其列数,因而这个 2、3 也是传入到 GridLayoutManager 的列数值,这里要保持一致。
子楼层的数据源解析
// 这里的 normalProductMultiClassifyUiBeanList 汇合中寄存了 2 个商品信息
for (ProductMultiClassifyUiBean productMultiClassifyUiBean : normalProductMultiClassifyUiBeanList) {productMultiClassifyUiBean.setFirstFloor(isFirstFloor);
shopListDataWrapper.addNormalBaseUiBeans(productMultiClassifyUiBean);
}
// 这里的 normalProductMultiClassifyUiBeanList 汇合中寄存了 3 个商品信息
for (ProductMultiClassifyUiBean productMultiClassifyUiBean : widthProductMultiClassifyUiBeanList) {productMultiClassifyUiBean.setFirstFloor(isFirstFloor);
shopListDataWrapper.addWidthBaseUiBeans(productMultiClassifyUiBean);
}
因而,到这里就曾经获取了所需的数据源局部
(4)屏幕类型切换
还是以折叠屏为例,折叠屏外屏切换到内屏,此时 Fragment 会走 onConfigurationChanged 办法。
屏幕类型切换 - 数据源切换 - 更新 RecyclerView。
public void onConfigurationChanged(@NonNull Configuration newConfig) {super.onConfigurationChanged(newConfig);
//1、首先进行内容区域中的 RecyclerViewAdapter、数据源判空
if (mRecyclerViewAdapter == null || mPageBeanAll == null) {return;}
//2、判断以后的屏幕类型,留神:这个中央是调用 3 提供的办法:ScreenTypeUtil.getScreenType(newConfig)
// 直板手机、折叠屏外屏
if (ScreenTypeUtil.NORMAL_SCREEN == ScreenTypeUtil.getScreenType(newConfig)) {mPageBeanAll.setBaseUiBeans(mPageBeanAll.getNormalBaseUiBeans());
} else if (ScreenTypeUtil.MIDDLE_SCREEN == ScreenTypeUtil.getScreenType(newConfig)) {if (SystemInfoUtils.isPadDevice()) {
// Pad 的竖屏
mPageBeanAll.setBaseUiBeans(mPageBeanAll.getNormalBaseUiBeans());
} else {
// 折叠屏的内屏
mPageBeanAll.setBaseUiBeans(mPageBeanAll.getWidthBaseUiBeans());
}
} else {
// Pad 的横屏、大分辨率屏幕
mPageBeanAll.setBaseUiBeans(mPageBeanAll.getWidthBaseUiBeans());
}
// 获取以后屏幕类型的最新数据源
mRecyclerViewAdapter.setDataSource(mPageBeanAll.getBaseUiBeans());
// 数据源驱动楼层 UI 扭转
mRecyclerViewAdapter.notifyDataSetChanged();}
通过 onConfigurationChanged 办法,可能看到数据源是如何依据不同屏幕类型进行切换的,当数据源切换后,会通过 notifyDataSetChanged 办法来扭转 UI。
四、至简之路的铸就
大道至简,遵循标准和准则,就能够想到如何对多机型进行适配,别陷入细节。
以这个作为指导思想,能够做很多其余的适配。上面做些列举,但不解说实现形式了。
1、文字显示区域放大
如下图所示,题目的长度,在整个容器显示宽度变宽的同时,也跟着一起变动,保障内容的长度能够自适应的变动。
2、弹框款式的兼容
如下图所示,蓝色区域是键盘的高度,在屏幕进行旋转的时候,键盘的高度也是变动的,此时可能会呈现遮挡住本来展现的内容,此处的解决形式是:让内容区域能够高低滑动。
3、摄像头地位的解决
如下图所示,在屏幕旋转之后,摄像头能够呈现在右下角,此时如果不对页面进行设置,那么就可能呈现内容区域无奈占据整个屏幕区域的问题,体验比拟差,此处的解决形式是:设置页面沉迷式,摄像头能够正当地笼罩一部分内容。
五、咱们解脱困扰了吗
5.1 解决原先的问题
通过后面的介绍,咱们晓得了,vivo 官网的团队针对折叠屏和 pad 这种大屏,采取了全屏展现的计划,一开始的时候,咱们遇到的问题也失去了解决:
(1)开发人员的适配老本高了,是不是针对每一种机型,都要做个独自的利用进行适配呢?
Answer:依照全屏模式的设计方案,折叠屏和 pad 也就是一种大尺寸的机器,开发人员判断机型的分辨率和尺寸,抉择一种对应的布局展现就好了,只用一个利用就能搞定。
(2)UI 设计师要做的效果图要多了,是不是要针对每种机型都要设计一套效果图呢?
Answer:制订一套标准,大于某个尺寸时,展现其余款式,所有信息内容都依照这种标准来,不会呈现设计凌乱的状况。
(3)产品和经营须要抉择的物料更受限制了,会不会这个物料在一个机器上失常。在其余机器上就不失常了呢?
Answer:以不变应万变,应用一套物料,适配不同的机型曾经能够落地了,不必再放心在不同的机器上展现不对立的问题。
5.2 咱们还能够做什么
5.2.1 咱们的长处
折叠屏和 pad 两款机器,曾经在市面上应用较长时间,各家厂商也纷纷采取了不同的适配计划来晋升交互体验,然而往往存在上面几个问题:
1、针对不同机型,采纳了不同的安装包。
这种计划,其实会减少保护老本,前期的开发要基于多个安装包去开发,更加耗时。
2、适配了不同的机型,然而在一些场景下的款式不现实。
比方有些 APP 做了分栏的适配,然而没有做全屏的适配,成果就比拟差,这里可能也是思考到了投入产出比。
3、目前的适配领导文档对于开发人员来说指导性较弱。
各种适配领导文档,还是比拟偏差于官网,对于开发人员来说,还是无奈提前辨认问题,遇到问题还是要理论去解决,https://developer.huawei.com/consumer/cn/doc/90101
基于此,咱们的长处如下:
1、咱们只有一个安装包。
咱们是一个安装包适配所有机型,每种机型的 APP 展现的款式尽管不同,对于开发者来说,就是减少了一个款式,思路比拟清晰。
2、全场景适配。
不同机型的纵向、横竖屏切换,都做到了完满适配,一套物料适配所有机型也是咱们的一个特色。
3、有针对性地提供适配计划。
本计划是基于理论开发遇到的问题,进行的梳理,能够帮忙开发人员解决理论可能遇到的问题,具备更好的参考性。
5.2.2 咱们还有什么要改良
回首计划,咱们这里做到的是应用全屏模式去适配不同机型,更多的实用于像京东、淘宝、商城等电商类 APP 上,实际上,当初有些非 APP 会采纳分栏的模式做适配,这也是一种跟用户交互的形式,本计划没有提到分栏,后续分栏落地后,对这部分会再进行补充。