本期内容
背景
通用弹窗设计
总结
弹窗作为APP与用户交互最基本、最重要的控件之一,在应用的使用过程中占据着十分重要的地位,尤其是在电商应用的首页展示中,重要的信息往往在第一时间以弹窗的形式向用户展现。如:新人引导、氛围活动、重要的通知公告、APP版本更新以及各种广告类弹窗等。
玩物得志APP经过长时间的业务迭代,项目中已经分散着大量各式各样的弹窗和引导,同样在首页的表现最为明显,例如首页目前需要处理的弹窗有:新用户888元优惠券弹窗、APP版本更新弹窗、通知权限引导弹窗、青少年模式引导弹窗、玩物口令弹窗、各种资源位广告弹窗、全局通知弹窗以及一些新功能引导蒙层等等... 需要处理的弹窗越多,弹窗之间的交互逻辑便越复杂,迭代的风险性也越高,例如弹窗可能出现显示顺序错乱、不同弹窗之间重叠展示等问题,从而给用户造成视觉失调或使用困扰,且弹窗开发层面也面临着成本高、风险大、灵活性差等问题。
因此,我们需要重新梳理APP通用弹窗业务架构,做到降低弹窗需求开发成本、快速响应各类业务场景,同时降低弹窗出错风险、保证弹窗展示的稳定性。
一开始由于业务的快速迭代,开发同学对于弹窗的实现也各自为战,且底层实现方式不一,随着迭代的时间越来越久,项目里的弹窗越来越多,老的分散式弹窗实现逐渐暴露出各种各样的潜在隐患,主要表现在以下几个方面:
弹窗开发成本较高:没有统一的基础弹窗组件,开发同学对于弹窗的底层实现不一,导致大量时间浪费在重复的UI层工作之上,且相似代码之间复制粘贴,后期难以维护,牵一发动全身;
需求迭代风险较大:多级弹窗相互控制逻辑处理复杂,随着弹窗越来越多,新需求的改动极易影响到老的逻辑,且对于如UI视觉升级的统一改造,难以评估成本和风险,导致遗漏;
异常情况显示错乱:某些弹窗的显示需要绑定在特定fragment之上,例如首页弹窗,因为异步原因,当用户切换到其他tab页面后,待接口数据回来,可能导致弹窗显示页面错误;
弹窗无法统一管理:多个弹窗的显示、隐藏各自实现,没有统一的收口和优先级管理能力,导致如玩物口令、彩蛋弹窗等全局性弹窗,无法有效的确保能够弹出在页面最上层,存在弹窗之间相互重叠现象;
弹窗业务灵活性差:APP弹窗虽复用性低,但是弹窗业务相似性高,随着端侧弹窗的需求频率越来越高,业务迭代能力往往受限于开发效率,一个弹窗需求需要多端投入开发,难以快速响应业务需求;
Android视图窗口层级:
Activity、Window、View的关系:
Window作为安卓中的窗口概念,是View的直接管理者,不论是页面之间的跳转,还是弹窗、蒙层的展示,本质上都是Window窗口的创建、添加过程,弹窗作为模态框呈现在屏幕之上,给人以窗口的层次感,是以子Window的形式覆盖在父页面之上,因为Window被Activity持有,且子Window可以多次添加,所以可能导致多tab弹窗显示错乱或弹窗之间相互重叠等问题。
我们需要解决主动管控弹窗绑定到特定子视图层级、多个弹窗优先级、互斥显示等控制,以防止弹窗出现显示错乱的问题。
对于APP精细化流量运营的今天,弹窗已经逐渐成为应用内高频需求,业务方面对弹窗需求的生产效率和交互体验要求较高,因此我们将本着提高开发效率、保证弹窗稳定性、提高业务灵活性3个方面对APP通用弹窗进行设计。
提高开发效率:提供全局通用性弹窗+业务弹窗BaseUI层封装,完善弹窗基础实现,满足直接使用、快速复用等场景,且抽象弹窗上层接口,支持底层差异化实现和规范弹窗之间的统一管理;
保证弹窗稳定性:提供弹窗统一管理中间层,修正弹窗显示错乱、弹窗之间相互覆盖等问题,支持多级弹窗插入方式、优先级控制、弹窗数据异步获取、显示队列统一管控等功能;
提高业务灵活性:提供通用运营类弹窗+云端弹窗全局配置研发模式,支持全局页面、事件、接口行为感知和管控,同时规范服务端弹窗业务接口网关统一以及数据格式化,支持后台动态配置全局运营弹窗策略等;
我们整理出APP中现有的通用弹窗类型、功能和使用场景,并与UI同学约定好通用类弹窗的统一UI规范,这样我们在开发过程中无需再开发重复功能/样式相似的Alert弹窗,UI同学也不必每次为新增的弹窗去出重复的UI稿。
我们目前将弹窗使用场景分为2类:
一种是通用提示类弹窗,可直接使用,不包含特殊业务逻辑,如加载中loading弹窗、信息提示类弹窗、确认/取消交互类弹窗、底部弹出选择类弹窗等;
另一种是业务类弹窗,我们需要提供基础的UI/交互封装+底层通用逻辑如埋点、生命周期绑定等,开发同学只需要关注各自的弹窗内部业务逻辑实现即可;
提示类Alert弹窗我们只需要封装好弹窗的各种API功能和底层通用逻辑实现,对外暴露统一API调用即可。如标题、内容、弹窗宽高、背景效果、字体样式、富文本内容、取消方式、显示隐藏回调等;
当UI风格统一改版时,我们也只需要对底层的弹窗替换不同风格的实现即可,对上层各处业务场景的调用是完全无影响的;
业务类弹窗我们抽象出WwdzBaseTipsDiaog和WwdzBaseBottomDialog,弹窗基于DialogFragment实现,分别封装好各自风格弹窗内部的基础样式和底层交互,开发同学一般只需要重写对应方法实现UI和交互的差异化,而重点关注的只是不同弹窗内部的业务逻辑处理,这样大大减少模板性的UI、交互相关代码,做到快速实现不同的业务类弹窗;
同一个页面内可能不止会弹出一个弹窗,且在APP的整个使用过程中,会有一些全局性弹窗诸如口令弹窗、彩蛋弹窗的出现,而全局性弹窗无法和具体页面内的弹窗逻辑冗余在一起,导致命中全局弹窗出现规则的话,可能发生不同弹窗相互覆盖的现象,如APP首页这种存在多个弹窗的同时显示的场景,需要有一层弹窗优先级控制能力,确保不同弹窗按顺序产生、显示、隐藏。
对于应用内弹窗统一管理,我们需要定义出弹窗抽象层,可将特殊弹窗场景如引导、悬浮窗、局部浮层都纳入弹窗管理层进行统一维护;且对同一页面多级弹窗进行统一队列管理,支持按插入方式、优先级顺序显示,简化弹窗数据接口异步准备以及互斥出现等;
考虑到老的弹窗底层实现不一,且新业务的弹窗也不止包含通知类、交互类弹窗,也可能包括功能引导蒙层、引导类气泡View或局部浮窗等,我们需要定义统一的弹窗抽象层,只关注show()、hide()等基础功能,外部实现了IDialogView接口的视图层都可以被管理,实现多级弹窗的统一管控;
/**
* IDialog视图层
*/
public interface IDialogView {
void show(Context context);
void hide();
boolean isShowing();
void setLifecycleCallback(IDialogLifecycleCallback lifecycleCallback);
}
对于弹窗的管理我们需要定义弹窗优先级、弹窗标识、弹窗状态、插入方式、异步任务task等属性,属性值可以封装到基类的BaseDialog中,也可以Wrapper模式对弹窗View进行进一步包裹,考虑到新增逻辑不侵入老代码,且后续新增更多功能时不必每次去修改BaseDialog,我们通过包装类DialogLayer对弹窗的使用描述到最小单位,并通过builder构造模式进行属性按需设置;
/**
* IDialogView属性
*/
public class DialogLayer {
private IDialogView dialogView;//弹窗view
private int level;//弹窗优先级
private String uniqueId;//弹窗标识
private InsertType insertType;//插入方式
private boolean needResume = false;//是否需要恢复
private DialogState dialogState = DialogState.PREPARED;//弹窗状态
private IDialogPrepareTask prepareTask;//异步任务获取task
public static DialogLayer.Builder with(IDialogView dialogView) {
return new DialogLayer.Builder(dialogView);
}
public static class Builder {
...
}
...
}
对于多级弹窗的管控可以单独处理全局管理,并关联不同的页面的生命周期的绑定,但是这种方式对所绑定页面的生命周期控制较为复杂,且同一个弹窗在不同页面上的展示优先级是可能不同的,因此我们参考Lifecycle的内部实现,将弹窗管理关联到每个具体的Activity或Fragment内步,使每个页面都有自己的DialogManager,维护自己页面内的弹窗队列即可。
/**
* BaseActivity基类
*/
public abstract class BaseActivity extends IDialogOwner, IPageDataTrack {
private final IDialogManager dialogManager = new WwdzDialogManager(this);
@Override
public IDialogManager getDialogManager() {
return dialogManager;
}
}
/**
* IDialogManager
*/
public interface IDialogManager {
IDialogManager addDialog(@NonNull DialogLayer dialogLayer);
IDialogManager removeDialog(@NonNull DialogLayer dialogLayer);
void clearAllDialog();
void startShow();
...
}
由于大部分弹窗的展示都需要前置的业务接口请求、数据解析等异步处理,才能决定弹窗是否真正显示以及对下一个弹窗的处理,在没有弹窗管理之前,我们只能通过嵌套调用,在每个弹窗真正处理完展示、隐藏之后在进行下一个弹窗的控制,这样在弹窗越来越多以后,业务逻辑将变得愈发复杂; 我们期望简化多层弹窗接口异步调用逻辑,将弹窗的数据准备过程定义为DialogPrepareTask,并对Dialog设置状态state,当弹窗管理层startShow()之后,将启动需要异步获取弹窗数据的任务,但我们的弹窗管理仍以优先级level为评判标准,当优先级最高的弹窗数据未准备好,则会等待其准备好数据再展示,当有优先级更高的弹窗插入且已准备好数据,则会根据插入规则先显示优先级更高的弹窗; 上层业务对弹窗的使用现在只需要关注DialogLayer属性设置以及addDialog()、startShow(),如需异步获取弹窗数据,则实现DataPrepareTask并在结果回来后驱动弹窗最终是否需要显示还是移除即可。
通过分析APP内弹窗的使用场景,除了通知类、交互类弹窗以外,平时需求场景最多的则是运营类业务弹窗,我们可能在进入某个页面、触发某个动作或完成某个接口调用后,触发一系列的运营弹窗。
这类弹窗是有一定规律可循的,例如UI、交互结构轻量,大多是资源位图片和点击跳转等逻辑,弹窗规则处理复用性强,如进入页面、触发特殊动作等,我们可以实现一套通用的端侧运营弹窗配置方案,负责解析云端运营弹窗配置、全局处理事件感知、动态加载原生或Web容器弹窗显示,以达到全局动态弹窗支持,为运营业务赋能,满足弹窗无迭代、快速上线、动态插拔的解决方案。
APP弹窗域属于既轻量又复杂的业务场景,业务对实时性、迭代速度要求较高,同时弹窗作为强触达用户手段,对体验的要求也较高。
经过第一阶段的APP弹窗治理,我们已经规范了当前弹窗开发模式并大大降低了日常工作中的弹窗开发成本。
第二阶段,我们将在动态弹窗搭投模式上给予更多支持,通过云端配置中心和九宫弹窗后台,满足无迭代多场景动态弹窗投放需求,为业务触达提效的同时,也给用户带来更加丰富的APP使用体验。