一、背景
由于业务需要,页面中通常会有很多弹窗功能。并且由于叠加展示的用户体验不好的原因,这些弹窗通常会被要求互斥出现。
在之前的设计里,这些弹窗是杂乱没有管理的。每次新添加一个弹窗业务,需要考虑与已有弹窗的互斥出现。这种无管理弹窗的实现方式主要有以下缺点:
1、需要对各弹窗出现逻辑进行状态区分,以达到互斥。每添加一个新弹窗,就需要添加对应的状态进行管理判断。弹窗业务杂糅,且容易遗漏互斥关系。
2、进入页面根据每个弹窗业务的展示逻辑来判断是否触发该弹窗的展示,因此无论触发的弹窗业务是否成功,其余弹窗都没有机会展示。弹窗少了当次可能的曝光机会。
3、目前没支持弹窗的顺序展现,如果要支持,各弹窗逻辑会互相耦合。
针对上述痛点,需要对业务弹窗进行抽象和统一管理,以达到按权重自行进行排序弹出的目的。最终实现以下功能:
1、各弹窗逻辑隔离,是否弹出完全由弹窗管理类决定。高优先级弹窗失败,会按照优先级高低弹出低优先级弹窗。
2、弹窗出现的优先级由接口动态下发,可以根据产品需要,动态调整弹窗出现优先级。
二、实现
1、弹窗抽象
1)弹窗类型定义:PopType
使用枚举实现,用于区分不同的弹窗。
2)弹窗实体对象:PopData
针对弹窗数据的抽象,弹窗的数据实体,定义了弹窗类型、唯一标识id、弹窗内容。
3)弹窗任务:Task
弹窗业务逻辑的具体执行体,包括但不限于网络请求等任务,用于产生弹窗数据和优先级排序。定义了任务唯一标识id、优先级,并且实现了Comparable接口用于排序。
2、弹窗任务管理类TaskManager
用于管理弹窗出现,主要维护一个弹窗任务优先级队列,按优先级顺序决定哪个弹窗可以弹出。
首先弹窗Task实现了Comparable接口 使其具备通过优先级比较大小的能力
TaskManager内部使用PriorityQueue作为弹窗任务容器,PriorityQueue内部使用**堆排序对内部元素进行排序,**每添加或删除一个Task元素都会对队列按优先级进行调整。
每次任务返回数据时,如果该任务是顶部Task,则直接弹出该任务对应的弹窗。否则,将弹窗数据入队列。
如果弹窗Task未返回正常数据,则将任务队列中该Task移除。并判断当前TaskQueue中的顶部Task是否有返回值,有则弹出;否则,继续等待下个任务的返回结果,重复上述过程。
架构设计如下图所示:
三、关键代码实现
弹窗任务管理器TaskManager关键代码实现如下:
public class TaskManager { //每添加完一个元素都会进行堆排序对队列进行优先级调整 先入先出 private PriorityQueue<Task> queue;//占位任务队列 private PriorityQueue<PopData> reserveQueue;//弹窗数据队列 private HashMap<String,PopData> relationMap;//任务与弹窗映射 /** * 将弹窗实体 推入队列中 */ public TaskManager pushTaskToQueue(Task task, PopData popi) { relationMap.put(task.mTaskId,popi); //如果队列中存在此弹窗 不重复加入 if (!isAlreadyInQueue(task, queue)) { queue.add(task); } return this; } /** * 遍历队列 以ID作为唯一标识 */ private boolean isAlreadyInQueue(Task task, Queue<Task> queue) { for (Task item : queue) { if (item.mTaskId == task.mTaskId) { return true; } } return false; } //队列中有元素才显示 public boolean canShow() { return reserveQueue.size() > 0; }//弹窗数据返回成功 public void onTaskSuccess(Task task){ if(task == null){ return; } //目前的task优先级判断是否在队列中是最高的 //是最高的 显示对应的弹窗 PopData popi=relationMap.get(task.mTaskId); if(isTaskFirstPriority(task.mTaskId)){ //显示弹窗 if(popi!=null){ pushToReserve(popi); //展示弹窗 showPop(popi); } }else { //不是优先级最高的 进入保留队列 if(popi!=null) pushToReserve(popi); } }//弹窗数据失败 public void onTaskError(Task task){ if(task == null){ return; } //如果失败 取消关联 从队列中移除 relationMap.remove(task.mTaskId); queue.remove(task); //失败后取队列中第一条弹窗数据,判断是否是当前剩余优先级最高 if(queue.size()>0){ Task peekTask = queue.element(); PopData popData = relationMap.get(peekTask.mTaskId); //如果已经请求成功了,已经在弹窗队列里了 if(isAlreadyInQueue(popData, reserveQueue)){ //显示该弹窗 showPop(popData); } }else { LOGGER.d("fuzc","任务列表为空"); } } private boolean isTaskFirstPriority(String taskId){ //拿到队列中的第一个如果是当前task 说明当前task优先值最高 Task firstTask=queue.peek(); if(firstTask!=null&&TextUtils.equals(firstTask.mTaskId,taskId)){ return true; }else { return false; } }}
业务弹窗管理类DialogHelper主要代码实现如下:
public class HomeDialogHelper { public TaskManager mTaskManager; public Task coldResumeTask;//冷启动任务 public Task resumeTask;//完善简历弹窗任务 public Task operationTask;//运营弹窗任务 public void clearTask(){ if (mTaskManager != null) { mTaskManager.clearTasks(); } } private void initTask() { getPriority(); if (mTaskManager == null) { mTaskManager = new TaskManager(); } mTaskManager.setPopCallback(popData -> { if (popData.data == null || activity == null || activity.isFinishing()) return; if (popData.data.getPopType() == PopType.OPERATING) { IndexAlertBean.Data popup = (IndexAlertBean.Data) popData.data; if (IndexFirstHelper.getInstance().dealWithAlertResponse(popup)){ IndexFirstHelper.getInstance() .showAlert(popup, activity); }else { mTaskManager.onTaskError(operationTask); } } else if (popData.data.getPopType() == PopType.PERFECT_RESUME) { ResumeCompleteDataBean.ResumeCompleteData resumeCompleteData = (ResumeCompleteDataBean.ResumeCompleteData) popData.data; if (dealWithPerfectResumeResponse(resumeCompleteData)){ new HomeResumeSubmitDialog(activity).setData(resumeCompleteData); }else { mTaskManager.onTaskError(resumeTask); } } else if (popData.data.getPopType() == PopType.RESUME_GUIDE) { ResumeCompleteDataBean.ResumeCompleteData resumeCompleteData = (ResumeCompleteDataBean.ResumeCompleteData) popData.data; if (!TextUtils.isEmpty(resumeCompleteData.getAction())) { JobPageTransferManager.jump(resumeCompleteData.getAction()); } } }); }//从接口动态获取弹窗权重 private void getPriority() { List<PopWindow> popWindows = JobWholeConfigManager.getInstance().mPopWindowList; if (popWindows != null && popWindows.size() > 0) { for (PopWindow p : popWindows) { if (TASK_OPERATION_ID.equals(p.getPop_type())) { operationPri = p.getPriority(); } else if (TASK_PERFECT_RESUME_ID.equals(p.getPop_type())) { perfectResumePri = p.getPriority(); } else if (TASK_RESUME_GUIDE_ID.equals(p.getPop_type())) { resumeGuidePri = p.getPriority(); } } } } //简历信息完善弹窗任务 public void requestCompleteResumeTask() { if(LoginPreferenceUtils.isLogin()) { resumeTask = new Task(perfectResumePri, TASK_PERFECT_RESUME_ID); PopData resumeData = new PopData.Builder() .setPriority(perfectResumePri) .setPopId(TASK_PERFECT_RESUME_ID) .build(); mTaskManager.pushTaskToQueue(resumeTask, resumeData); new JobNetHelperV2.Builder(ResumeCompleteDataBean.class) .url(JobHttpApi.CATEGORY_RESUME_GET_INFO) .autoDispose(activity) .addParams(paramMap) .onResponse(new JobSimpleNetResponse<ResumeCompleteDataBean>() { @Override public void onNext(@NonNull ResumeCompleteDataBean resumeCompleteDataBean) { super.onNext(resumeCompleteDataBean); if (resumeCompleteDataBean == null || resumeCompleteDataBean.getData() == null) { mTaskManager.onTaskError(resumeTask); return; } if (TextUtils.equals(resumeCompleteDataBean.getData().getPop_type(), "perfectResume")) { resumeData.data = resumeCompleteDataBean.getData(); mTaskManager.onTaskSuccess(resumeTask); } else { mTaskManager.onTaskError(resumeTask); } } @Override public void onError(Throwable e) { super.onError(e); mTaskManager.onTaskError(resumeTask); } } ).createAndRequest(); } } /** * 运营弹窗任务 */ public void requestPopupDataTask() { operationTask = new Task(operationPri, TASK_OPERATION_ID); PopData operationData = new PopData.Builder() .setPriority(operationPri) .setPopId(TASK_OPERATION_ID) .build(); mTaskManager.pushTaskToQueue(operationTask, operationData); String url = UrlUtils.newUrl(NetworkUtils.DOMAIN_HOST_FORMAL, "api/tagaggre/"); Map<String, String> params = JobHttpApi.getDefaultParams(); params.put("type", "getPopup"); params.put("viewFlag", "B"); new JobNetHelper.Builder<>(IndexAlertBean.class) .url(url) .addParams(params) .netTip(false) .activity(activity) .onResponse(new JobSimpleNetResponse<IndexAlertBean>() { @Override public void onNext(IndexAlertBean bean) { super.onNext(bean); operationData.data = bean.popup; mTaskManager.onTaskSuccess(operationTask); } @Override public void onError(Throwable e) { super.onError(e); mTaskManager.onTaskError(operationTask); } }).createAndRequest(); }}
四、达成成果
- 弹窗逻辑隔离,自动管理任务状态,助力开发提效
- 高优先级弹窗失败情况下,低优先级弹窗有机会展示
- 优先级由接口动态下发,可以根据产品需要,动态调整