4

vivo官网APP全机型UI适配方案 - vivo互联网技术

 1 year ago
source link: https://www.cnblogs.com/vivotech/p/16478700.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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种方法:

通过在AndroidManifest.xml中设置:android:screenOrientation属性

a) android:screenOrientation="portrait"强制竖屏;

b) android:screenOrientation="landscape"强制横屏;

c) android:screenOrientation="unspecified"默认值,可以横竖屏切换;

在代码中设置: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)分辨率

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会采用分栏的形式做适配,这也是一种跟用户交互的方式,本方案没有提到分栏,后续分栏落地后,对这部分会再进行补充。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK