在很多APP中,都会有红点计数功能。在开发玩心部落APP时,也需要实现红点计数功能,并且显示的红点比较多,逻辑相对比较复杂。经过一番探究和优化,笔者最终实现了红点计数功能,现把实现方案整理出来,以期给其他开发者提供参考。
玩心部落APP中,红点计数功能比较复杂,为了便于讲解,这里把功能进行了简化,简化后的效果如下图所示。
红点计数的实现方法,核心原理包括:数据结构中的树和设计模式中的观察者模式。下面以玩心部落APP的红点计数功能为例,进行详细的讲解。
实现红点计数功能,需要建立一个红点树的模型,用于描述各个红点之间的逻辑关系,树中的每个节点,都需要实现两种刷新方式:一种是刷新当前红点对应的控件,一种是刷新父节点的数据。为了对红点树进行存储、管理和维护,可以创建一个头节点。红点树和头节点之间的关系,如图3所示。
2.2 数据结构
实现红点计数功能时,非常重要的两个概念就是红点树和头节点,本小节详细介绍。
红点树就是表示红点之间关系的树形结构。以玩心部落APP为例,桌面图标显示的红点数,等于“消息”模块和“我的”模块红点数的和,“消息”模块的红点数等于“消息”子模块和“游戏”子模块的红点数的和,“我的”模块红点数等于“粉丝”和“关注”红点数的和,“消息”子模块红点数等于“回复我的”、“@我的”、“收到的赞”、“部落管家”和“系统消息”红点数的和。描述这些节点之间关系的树就是红点树,如图3所示。
红点树中的每个节点,都需要实现两种数据刷新方式:一种是刷新控件,一种是刷新父节点。
刷新控件是指,当节点数据变化时,及时刷新控件,在控件中显示红点的最新数据。这就需要把节点设置为被观察者,控件设置为观察者,当节点数据变化时,通知控件刷新数据。
刷新父节点是指,当前节点数据变化时,要及时通知父节点刷新数据,让父节点的数据始终等于所有子节点的数据之和。这个可以采用递归的方式,当子节点通知父节点刷新数据后,由于父节点的数据变化了,所以父节点会通知自己的父节点刷新数据,依此类推,直到根节点的数据刷新完毕为止。
以“回复我的”节点为例,它的数据更新方式如图4所示。控件是一个观察者,“回复我的”节点是一个被观察者,当“回复我的”节点数据变化时,会通知控件进行刷新,从而实现刷新控件的功能,如图4的蓝色部分所示。当“回复我的”节点数据变化时,会自动更新父节点“消息(子)”节点,“消息(子)”节点数据更新后会自动更新父节点“消息”节点,“消息”节点数据更新后会自动更新父节点“桌面图标”节点,该节点数据更新完毕后,整个更新操作就结束了,这就是刷新父节点的流程,如图4的红色部分所示。
2.3 实现代码
前面介绍了红点计数功能的实现原理,本小节重点介绍具体实现代码。红点计数功能的实现代码包括节点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 - 【消息】 - 【消息】子模块 - 系统消息
}
每个子节点都有3个成员变量:父节点Key、红点数和控件观察者。父节点Key对应于2.3.1中的节点Key常量,红点数就是当前节点的红点总数,控件观察者就是显示该红点数的控件。注意:每个子节点都是一个被观察者,红点数变化时,会通知控件观察者执行刷新操作。代码如下所示。
/**
* 红点节点。子节点是被观察者,当子节点的数据变化时,通知刷新UI,并更新父节点的数据。
*/
public class RedPoint extends Observable {
private String parentKey; // 当前节点的父节点在redPointMap中的Key
private int count = 0; // 当前节点的红点数
private Observer viewObserver; // 当前节点数据变化时,通知该对象,刷新UI
public 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();
// 通知刷新UI
notifyObservers(this.count);
}
}
头节点主要维护红点树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();
}
}
使用红点计数时,只需要通过HeadRedPoint.getInstance().getRedPoint()方法,传入IRedPointKey中对应的Key,拿到对应的红点RedPoint对象,然后调用RedPoint的对应方法就可以了。红点的常用操作,主要包括:添加观察者和更新红点数据。
要根据红点数据实时刷新控件,就要在对应位置,添加红点的观察者,当红点数据变化时,自动通知该观察者,执行刷新UI等操作。
以“回复我的”红点为例,添加观察者的方法如下所示:
// IRedPointKey.KEY_MESSAGE_SUB_MESSAGE_REPLY是“回复我的”节点的Key。获取“回复我的”节点,监听“回复我的”红点数的变化。
HeadRedPoint.getInstance().getRedPoint(IRedPointKey.KEY_MESSAGE_SUB_MESSAGE_REPLY).setViewObserver(new Observer() {
public void update(Observable o, Object arg) {
// “回复我的”节点数据变化时,会调用该方法,arg就是节点的数据。
// 执行刷新控件等操作
}
});
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); // 红点数设置为1
HeadRedPoint.getInstance().getRedPoint(IRedPointKey.KEY_MESSAGE_SUB_MESSAGE_REPLY).addCount(1); // 红点数+1
HeadRedPoint.getInstance().getRedPoint(IRedPointKey.KEY_MESSAGE_SUB_MESSAGE_REPLY).minusCount(1); // 红点数-1
HeadRedPoint.getInstance().getRedPoint(IRedPointKey.KEY_MESSAGE_SUB_MESSAGE_REPLY).clearCount(); // 红点数清零
后续开发中,如果需要新增、删除红点,或者改变红点之间的关系,只需要两步:第一步,更新IRedPointKey中的常量;第二步,在HeadRedPoint类的initRedPoint()方法中更新redPointMap和对应节点的父节点的Key。