红点计数实现方案
1 前言
在很多APP中,都会有红点计数功能。在开发玩心部落APP时,也需要实现红点计数功能,并且显示的红点比较多,逻辑相对比较复杂。经过一番探究和优化,笔者最终实现了红点计数功能,现把实现方案整理出来,以期给其他开发者提供参考。
玩心部落APP中,红点计数功能比较复杂,为了便于讲解,这里把功能进行了简化,简化后的效果如下图所示。
2 实现方案
红点计数的实现方法,核心原理包括:数据结构中的树和设计模式中的观察者模式。下面以玩心部落APP的红点计数功能为例,进行详细的讲解。
2.1 整体思路
实现红点计数功能,需要建立一个红点树的模型,用于描述各个红点之间的逻辑关系,树中的每个节点,都需要实现两种刷新方式:一种是刷新当前红点对应的控件,一种是刷新父节点的数据。为了对红点树进行存储、管理和维护,可以创建一个头节点。红点树和头节点之间的关系,如图3所示。
2.2 数据结构
实现红点计数功能时,非常重要的两个概念就是红点树和头节点,本小节详细介绍。
2.2.1 红点树
红点树就是表示红点之间关系的树形结构。以玩心部落APP为例,桌面图标显示的红点数,等于“消息”模块和“我的”模块红点数的和,“消息”模块的红点数等于“消息”子模块和“游戏”子模块的红点数的和,“我的”模块红点数等于“粉丝”和“关注”红点数的和,“消息”子模块红点数等于“回复我的”、“@我的”、“收到的赞”、“部落管家”和“系统消息”红点数的和。描述这些节点之间关系的树就是红点树,如图3所示。
红点树中的每个节点,都需要实现两种数据刷新方式:一种是刷新控件,一种是刷新父节点。
刷新控件是指,当节点数据变化时,及时刷新控件,在控件中显示红点的最新数据。这就需要把节点设置为被观察者,控件设置为观察者,当节点数据变化时,通知控件刷新数据。
刷新父节点是指,当前节点数据变化时,要及时通知父节点刷新数据,让父节点的数据始终等于所有子节点的数据之和。这个可以采用递归的方式,当子节点通知父节点刷新数据后,由于父节点的数据变化了,所以父节点会通知自己的父节点刷新数据,依此类推,直到根节点的数据刷新完毕为止。
以“回复我的”节点为例,它的数据更新方式如图4所示。控件是一个观察者,“回复我的”节点是一个被观察者,当“回复我的”节点数据变化时,会通知控件进行刷新,从而实现刷新控件的功能,如图4的蓝色部分所示。当“回复我的”节点数据变化时,会自动更新父节点“消息(子)”节点,“消息(子)”节点数据更新后会自动更新父节点“消息”节点,“消息”节点数据更新后会自动更新父节点“桌面图标”节点,该节点数据更新完毕后,整个更新操作就结束了,这就是刷新父节点的流程,如图4的红色部分所示。
2.3 实现代码
前面介绍了红点计数功能的实现原理,本小节重点介绍具体实现代码。红点计数功能的实现代码包括节点Key常量、子节点和头节点这三部分。
2.3.1 节点Key常量
给红点树中的每个节点编写一个唯一标识字符串Key,然后把这些Key赋值给常量。后面需要使用哪个节点,直接通过常量Key来获取即可。
图5的红点树对应的节点Key常量如下所示:
/*** 节点Key常量*/public interface IRedPointKey {String KEY_UNKNOWN = "key_unknown"; // 节点Key - 未知节点String KEY_ROOT = "key_root"; // 节点Key - 根节点,即:桌面图标节点String KEY_MINE_MAIN = "key_mine_main"; // 节点Key - 【我的】模块String KEY_MINE_FANS = "key_mine_fans"; // 节点Key - 【我的】 - 粉丝String KEY_MINE_FOLLOW = "key_mine_follow"; // 节点Key - 【我的】 - 关注String KEY_MESSAGE_MAIN = "key_message_main"; // 节点Key - 【消息】模块String KEY_MESSAGE_GAME = "key_message_game"; // 节点Key - 【消息】 - 【游戏】子模块String KEY_MESSAGE_SUB_MESSAGE_MAIN = "key_message_sub_message_main"; // 节点Key - 【消息】 - 【消息】子模块String KEY_MESSAGE_SUB_MESSAGE_REPLY = "key_message_sub_message_reply"; // 节点Key - 【消息】 - 【消息】子模块 - 回复我的String KEY_MESSAGE_SUB_MESSAGE_MENTION = "key_message_sub_message_mention"; // 节点Key - 【消息】 - 【消息】子模块 - @我的String KEY_MESSAGE_SUB_MESSAGE_PRAISE ="key_message_sub_message_praise"; // 节点Key - 【消息】 - 【消息】子模块 - 收到的赞String KEY_MESSAGE_SUB_MESSAGE_TRIBAL_MANAGER = "key_message_sub_message_tribal_manager"; // 节点Key - 【消息】 - 【消息】子模块 - 部落管家String KEY_MESSAGE_SUB_MESSAGE_SYSTEM = "key_message_sub_message_system"; // 节点Key - 【消息】 - 【消息】子模块 - 系统消息}
2.3.2 子节点
每个子节点都有3个成员变量:父节点Key、红点数和控件观察者。父节点Key对应于2.3.1中的节点Key常量,红点数就是当前节点的红点总数,控件观察者就是显示该红点数的控件。注意:每个子节点都是一个被观察者,红点数变化时,会通知控件观察者执行刷新操作。代码如下所示。
/*** 红点节点。子节点是被观察者,当子节点的数据变化时,通知刷新UI,并更新父节点的数据。*/public class RedPoint extends Observable {private String parentKey; // 当前节点的父节点在redPointMap中的Keyprivate int count = 0; // 当前节点的红点数private Observer viewObserver; // 当前节点数据变化时,通知该对象,刷新UIpublic RedPoint() {this(IRedPointKey.KEY_UNKNOWN);}/*** 构造方法** @param parentKey 父节点的Key* */public RedPoint(String parentKey) {this.parentKey = parentKey;}/*** 设置父节点在redPointMap(详见 {@link HeadRedPoint})中的Key** @param parentKey 父节点的key* */public void setParentKey(String parentKey) {this.parentKey = parentKey;}/*** 获取父节点的key* */public String getParentKey() {return parentKey;}/*** 设置当前节点的数据** @param count 当前节点的数据*/public void setCount(int count) {this.count = count;notifyChange();}/*** 获取当前节点的数据** @return 当前节点的数据*/public int getCount() {return count;}/*** 增加当前节点的数据** @param count 当前节点的新增数据*/public void addCount(int count) {setCount(this.count + count);notifyChange();}/*** 减少当前节点的数据** @param count 当前节点的减少数据*/public void minusCount(int count) {setCount(this.count - count);notifyChange();}/*** 清空当前节点的数据*/public void clearCount() {setCount(0);notifyChange();}/*** 设置刷新UI的观察者,当前节点数据变化时,通知该观察者,刷新UI。** @param observer UI的观察者*/public void setViewObserver(Observer observer) {this.viewObserver = observer;addObserver(observer);}/*** 获取刷新UI的观察者** @return UI的观察者*/public Observer getViewObserver() {return viewObserver;}/*** 更新数据*/private void notifyChange() {/*更新父节点的数据*/HeadRedPoint.getInstance().updateCount(parentKey);/*通知观察者,数据已更新*/// 标识内容发生变化setChanged();// 通知刷新UInotifyObservers(this.count);}}
2.3.3 头节点
头节点主要维护红点树Map,代码如下所示:
/*** 红点树的头节点,内部维护一个Map:键为每个红点的唯一标识,详见{@link IRedPointKey};值为红点。*/public class HeadRedPoint {private static HeadRedPoint headRedPoint; // 头节点/*** 保存红点的Map:键为每个红点的唯一标识,详见{@link IRedPointKey};值为红点。* */private Map<String, RedPoint> redPointMap = new HashMap<>();/*** 获取头节点实例*/public static HeadRedPoint getInstance() {if (headRedPoint == null) {synchronized (HeadRedPoint.class) {if (headRedPoint == null) {headRedPoint = new HeadRedPoint();}}}return headRedPoint;}private HeadRedPoint() {initRedPoint();}/*** 红点Map初始化* */private void initRedPoint() {/*添加根节点*/redPointMap.put(IRedPointKey.KEY_ROOT, new RedPoint());/*添加【我的】模块的节点,并指定每个节点的父节点的Key*/redPointMap.put(IRedPointKey.KEY_MINE_MAIN, new RedPoint(IRedPointKey.KEY_ROOT));redPointMap.put(IRedPointKey.KEY_MINE_FANS, new RedPoint(IRedPointKey.KEY_MINE_MAIN));redPointMap.put(IRedPointKey.KEY_MINE_FOLLOW, new RedPoint(IRedPointKey.KEY_MINE_MAIN));/*添加【消息】模块的节点,并指定每个节点的父节点的Key*/redPointMap.put(IRedPointKey.KEY_MESSAGE_MAIN, new RedPoint(IRedPointKey.KEY_ROOT));redPointMap.put(IRedPointKey.KEY_MESSAGE_GAME, new RedPoint(IRedPointKey.KEY_MESSAGE_MAIN));redPointMap.put(IRedPointKey.KEY_MESSAGE_SUB_MESSAGE_MAIN, new RedPoint(IRedPointKey.KEY_MESSAGE_MAIN));redPointMap.put(IRedPointKey.KEY_MESSAGE_SUB_MESSAGE_REPLY, new RedPoint(IRedPointKey.KEY_MESSAGE_SUB_MESSAGE_MAIN));redPointMap.put(IRedPointKey.KEY_MESSAGE_SUB_MESSAGE_MENTION, new RedPoint(IRedPointKey.KEY_MESSAGE_SUB_MESSAGE_MAIN));redPointMap.put(IRedPointKey.KEY_MESSAGE_SUB_MESSAGE_PRAISE, new RedPoint(IRedPointKey.KEY_MESSAGE_SUB_MESSAGE_MAIN));redPointMap.put(IRedPointKey.KEY_MESSAGE_SUB_MESSAGE_TRIBAL_MANAGER, new RedPoint(IRedPointKey.KEY_MESSAGE_SUB_MESSAGE_MAIN));redPointMap.put(IRedPointKey.KEY_MESSAGE_SUB_MESSAGE_SYSTEM, new RedPoint(IRedPointKey.KEY_MESSAGE_SUB_MESSAGE_MAIN));}/*** 把红点键值对添加到redPointMap中** @param key 红点的key* @param redPoint 红点*/public void addRedPoint(String key, RedPoint redPoint) {if (!TextUtils.isEmpty(key) && redPoint != null) {redPointMap.put(key, redPoint);}}/*** 获取指定Key的红点** @param key 红点的key* @return 指定key的红点*/public RedPoint getRedPoint(String key) {if (TextUtils.isEmpty(key)) {return null;} else {return redPointMap.get(key);}}/*** 计算父节点所有子节点的数据之和,然后更新父节点的数据。** @param parentKey 父节点的key*/protected void updateCount(String parentKey) {if (TextUtils.isEmpty(parentKey) || redPointMap.get(parentKey) == null) {return;}int totalCount = 0; // 父节点所有子节点的数据之和// 遍历redPointMap,把父节点等于parentKey的节点的数据加起来,得到key为parentKey的节点的最新数据for (RedPoint redPoint : redPointMap.values()) {if (parentKey.equals(redPoint.getParentKey())) {totalCount += redPoint.getCount();}}// 更新key为parentKey的节点的数据redPointMap.get(parentKey).setCount(totalCount);}/*** 清空所有节点的数据*/public void clearAllCount() {for (RedPoint redPoint : redPointMap.values()) {redPoint.clearCount();}}/*** 清除所有红点*/public void clearAllRedPoint() {for (RedPoint redPoint : redPointMap.values()) {redPoint.clearCount();}redPointMap.clear();}}
3 使用示例
使用红点计数时,只需要通过HeadRedPoint.getInstance().getRedPoint()方法,传入IRedPointKey中对应的Key,拿到对应的红点RedPoint对象,然后调用RedPoint的对应方法就可以了。红点的常用操作,主要包括:添加观察者和更新红点数据。
3.1 添加观察者
要根据红点数据实时刷新控件,就要在对应位置,添加红点的观察者,当红点数据变化时,自动通知该观察者,执行刷新UI等操作。
以“回复我的”红点为例,添加观察者的方法如下所示:
// IRedPointKey.KEY_MESSAGE_SUB_MESSAGE_REPLY是“回复我的”节点的Key。获取“回复我的”节点,监听“回复我的”红点数的变化。HeadRedPoint.getInstance().getRedPoint(IRedPointKey.KEY_MESSAGE_SUB_MESSAGE_REPLY).setViewObserver(new Observer() {@Overridepublic void update(Observable o, Object arg) {// “回复我的”节点数据变化时,会调用该方法,arg就是节点的数据。// 执行刷新控件等操作}});
3.2 更新红点数据
RedPoint类中更新红点数据的方法主要有:
setCount(int count):红点数设置为count
addCount(int count):红点数增加count
minusCount(int count):红点数减少count
clearCount():清空红点数
以“回复我的”节点为例,更新红点数的方法如下所示:
HeadRedPoint.getInstance().getRedPoint(IRedPointKey.KEY_MESSAGE_SUB_MESSAGE_REPLY).setCount(1); // 红点数设置为1HeadRedPoint.getInstance().getRedPoint(IRedPointKey.KEY_MESSAGE_SUB_MESSAGE_REPLY).addCount(1); // 红点数+1HeadRedPoint.getInstance().getRedPoint(IRedPointKey.KEY_MESSAGE_SUB_MESSAGE_REPLY).minusCount(1); // 红点数-1HeadRedPoint.getInstance().getRedPoint(IRedPointKey.KEY_MESSAGE_SUB_MESSAGE_REPLY).clearCount(); // 红点数清零
4 维护方法
后续开发中,如果需要新增、删除红点,或者改变红点之间的关系,只需要两步:第一步,更新IRedPointKey中的常量;第二步,在HeadRedPoint类的initRedPoint()方法中更新redPointMap和对应节点的父节点的Key。