我们平常接触到的大部分App,在收到新消息的时候一般都会以数字或红点的形式提示出来。比如在微信当中,当某位好友给我们发来新的聊天消息的时候,在相应的会话上就会有一个数字来表示未读消息的数目;再比如当微信朋友圈里有人发布新的内容时,朋友圈的入口就会出现一个红点,而当朋友圈里有人给我们点了赞,或者对我们发布的内容进行了评论的时候,朋友圈的入口就会显示一个数字。
但是,我们在试用一些新的App产品时,总会发现它们在数字和红点展示上存在各种各样的问题。比如,红点怎么点击也清除不掉;或者,发现有数字了,点进去却什么也没有;或者,点进去看到的数字和外面看到的不一样。
那这些问题到底是怎样产生的呢?
我猜测,问题产生的根源是:没有对数字和红点的展示逻辑做一个统一的抽象和管理,以至于各种数字和红点之间的关系错综复杂,牵一发而动全身。这样,在App的维护过程中,稍微有一点改动(比如增加几个数字或红点类型),出现问题的概率就很高。
本文会提出一个树型结构模型,来对数字和红点的层次结构进行统一管理,并会在文章最后给出一个可以运行的Android版的Demo程序,以供参考。
如果您现在手头正好有一部Android手机,那么您可以先扫描下面的二维码(或点击二维码下面的下载链接)下载安装这个Demo,花几分钟看看它是否对您有用。
或者点击下载链接。
为了讨论方便,我们首先对一般情况下数字和红点展示的需求做一个简单的整理,然后看看根据这样的需求最直观的实现方式可能是怎样的。
相信以上总结的几点,跟大多数App的展示逻辑大体类似。即使有一些差别,应该也不妨碍我们接下来的讨论。
好,现在我们就以上面App截图中的具体情形来考虑一下实现。“消息”Tab包含“收到的评论”、“收到的赞”和“系统消息”,其中评论和赞是数字,系统消息是红点。
我们单独考虑“消息”这个Tab上的数字红点展示逻辑,不难写出类似如下的代码(伪码):
1
2
3
4
5
6
7
8
9
10
int count = 评论数 + 赞数;
if (count > 0) {
展示数字count
}
else if (有系统消息) {
展示红点
}
else {
隐藏数字和红点
}
这段代码当然能实现需求,但是缺点也是很明显的。其中最关键的是,它要求在“消息”这个Tab上的展示逻辑要列举下面包含的所有子消息类型(评论、赞、系统消息),并且知道每个类型是数字还是红点。上面只是给出了两级页面的情况,如果出现三级页面甚至更多级呢?那么这些信息就要在各级页面上重复一遍。
这会造成维护和修改变得复杂。想象一下,在“消息”下面又增加了一个新的消息类型,或者某个类型的消息从数字展示变成红点展示了,甚至是某个类型的消息,从一个页面栈移动到了另一个页面栈了。所有这些情况,都要求更高层级的所有页面都对应进行修改。当一个App的消息类型越来越多,达到几十个的时候,可以想象这种修改是很容易出错的。
上面说的问题,我们在微爱App开发的初期也遇到过。后来,我们重新审视了App中红点和数字展示的结构,使用树型结构来看待它,让维护工作变得简单。
一个App的页面本身就是分级的,对于页面的访问路径本质上就是个树型结构。
如上图所示,节点1代表第1级页面,这个页面下面包含三个更深一级(第2级)的页面入口,分别对应节点2,3,4。再深一级就到了终端页面,以绿色的方形节点表示。
这个树型的模型可以如下表述:
为了使得一个大类内的Badge Number能用一个类型区间来表达,我们在为类型分配值的时候,可以采取类似这样的方式:用一个int来表示Badge Number类型,而它的高16位用来表示大类。比如“消息”大类高16位是0x2的话,那么它包含的三种Badge Number类型(type)就可以这样分配:
这样,“消息”这一大类就可以用一个类型区间[(0x2 « 16) + 0x1, (0x2 « 16) + 0x3]来表达。
有了类型区间之后,我们重新看一下树型模型里面的中间节点。它们都可以用一个或多个类型区间来表示。它们的展示逻辑(是展示成数字,还是红点,还是隐藏),需要对所有子树的类型区间求和。具体求和过程是:
树型模型的实现,我们称为Badge Number Tree,本文提供了一个Android版的Demo实现,源码可以从GitHub下载:https://github.com/tielei/BadgeNumberTree 。
下面我们把关键部分分析一下。
Android版本的主要实现类为BadgeNumberTreeManager,它的关键代码如下(为了不影响我们理解主要逻辑,非关键代码在下面忽略了,没有贴出。如需查看请到GitHub下载源代码):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
/**
* 用于异步返回结果的接口.
*/
public interface AsyncResult<ResultType> {
void returnResult(ResultType result);
}
/**
* 树型结构的badge number管理器.
*/
public class BadgeNumberTreeManager {
/**
* 设置badge number
* @param badgeNumber
* @param asyncResult 异步返回结果, 会返回一个Boolean参数, 表示是否设置成功了.
*/
public void setBadgeNumber(final BadgeNumber badgeNumber, final AsyncResult<Boolean> asyncResult) {
...
}
/**
* 累加badge number
* @param badgeNumber
* @param asyncResult 异步返回结果, 会返回一个Boolean参数, 表示是否累加操作成功了.
*/
public void addBadgeNumber(final BadgeNumber badgeNumber, final AsyncResult<Boolean> asyncResult) {
...
}
/**
* 删除指定类型的badge number
* @param type 指定的badge number类型.
* @param asyncResult 异步返回结果, 会返回一个Boolean参数, 表示是否删除成功了.
*/
public void clearBadgeNumber(final int type, final AsyncResult<Boolean> asyncResult) {
...
}
/**
* 获取指定类型的badge number
* @param type 类型。取聊天的badge number时,传0即可。
* @param asyncResult 异步返回结果, 会返回指定类型的badge number的count数.
*/
public void getBadgeNumber(final int type, final AsyncResult<Integer> asyncResult) {
...
}
/**
* 根据一个类型区间列表计算一个树型父节点总的badge number。
* 优先计算数字,其次计算红点。
*
* 一个类型区间列表在实际中对应一个树型父节点。
*
* @param typeIntervalList 指定的badge number类型区间列表, 至少有1一个区间
* @param asyncResult 异步返回结果, 会返回指定类型的badge number的情况(包括显示方式和总数).
*/
public void getTotalBadgeNumberOnParent(final List<BadgeNumberTypeInterval> typeIntervalList, final AsyncResult<BadgeNumberCountResult> asyncResult) {
//先计算显示数字的badge number类型
getTotalBadgeNumberOnParent(typeIntervalList, BadgeNumber.DISPLAY_MODE_ON_PARENT_NUMBER, new AsyncResult<BadgeNumberCountResult>() {
@Override
public void returnResult(BadgeNumberCountResult result) {
if (result.getTotalCount() > 0) {
//数字类型总数大于0,可以返回了。
if (asyncResult != null) {
asyncResult.returnResult(result);
}
}
else {
//数字类型总数不大于0,继续计算红点类型
getTotalBadgeNumberOnParent(typeIntervalList, BadgeNumber.DISPLAY_MODE_ON_PARENT_DOT, new AsyncResult<BadgeNumberCountResult>() {
@Override
public void returnResult(BadgeNumberCountResult result) {
if (asyncResult != null) {
asyncResult.returnResult(result);
}
}
});
}
}
});
}
private void getTotalBadgeNumberOnParent(final List<BadgeNumberTypeInterval> typeIntervalList, final int displayMode, final AsyncResult<BadgeNumberCountResult> asyncResult) {
final List<Integer> countsList = new ArrayList<Integer>(typeIntervalList.size());
for (BadgeNumberTypeInterval typeInterval : typeIntervalList) {
getBadgeNumber(typeInterval.getTypeMin(), typeInterval.getTypeMax(), displayMode, new AsyncResult<Integer>() {
@Override
public void returnResult(Integer result) {
countsList.add(result);
if (countsList.size() == typeIntervalList.size()) {
//类型区间的count都有了
int totalCount = 0;
for (Integer count : countsList) {
if (count != null) {
totalCount += count;
}
}
//返回总数
if (asyncResult != null) {
BadgeNumberCountResult badgeNumberCountResult = new BadgeNumberCountResult();
badgeNumberCountResult.setDisplayMode(displayMode);
badgeNumberCountResult.setTotalCount(totalCount);
asyncResult.returnResult(badgeNumberCountResult);
}
}
}
});
}
}
private void getBadgeNumber(final int typeMin, final int typeMax, final int displayMode, final AsyncResult<Integer> asyncResult) {
...
}
/**
* badge number类型区间。
*/
public static class BadgeNumberTypeInterval {
private int typeMin;
private int typeMax;
public int getTypeMin() {
return typeMin;
}
public void setTypeMin(int typeMin) {
this.typeMin = typeMin;
}
public int getTypeMax() {
return typeMax;
}
public void setTypeMax(int typeMax) {
this.typeMax = typeMax;
}
}
/**
* badge number按照一个类型区间计数后的结果。
*/
public static class BadgeNumberCountResult {
private int displayMode;
private int totalCount;
public int getDisplayMode() {
return displayMode;
}
public void setDisplayMode(int displayMode) {
this.displayMode = displayMode;
}
public int getTotalCount() {
return totalCount;
}
public void setTotalCount(int totalCount) {
this.totalCount = totalCount;
}
}
}
在这段代码中我们需要注意的点包括:
调用getTotalBadgeNumberOnParent的代码例子如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
BadgeNumberTypeInterval typeInterval = new BadgeNumberTypeInterval();
typeInterval.setTypeMin(BadgeNumber.CATEGORY_NEWS_MIN);
typeInterval.setTypeMax(BadgeNumber.CATEGORY_NEWS_MAX);
List<BadgeNumberTypeInterval> typeIntervalList = new ArrayList<BadgeNumberTypeInterval>(1);
typeIntervalList.add(typeInterval);
BadgeNumberTreeManager.getInstance().getTotalBadgeNumberOnParent(typeIntervalList, new AsyncResult<BadgeNumberCountResult>() {
@Override
public void returnResult(BadgeNumberCountResult result) {
if (result.getDisplayMode() == BadgeNumber.DISPLAY_MODE_ON_PARENT_NUMBER && result.getTotalCount() > 0) {
//展示数字
showTabBadgeCount(tabIndex, result.getTotalCount());
} else if (result.getDisplayMode() == BadgeNumber.DISPLAY_MODE_ON_PARENT_DOT && result.getTotalCount() > 0) {
//展示红点
showTabBadgeDot(tabIndex);
} else {
//隐藏数字和红点
hideTabBadgeNumber(tabIndex);
}
}
});