原文出处:用树型模型管理App数字和红点提示

我们平常接触到的大部分App,在收到新消息的时候一般都会以数字或红点的形式提示出来。比如在微信当中,当某位好友给我们发来新的聊天消息的时候,在相应的会话上就会有一个数字来表示未读消息的数目;再比如当微信朋友圈里有人发布新的内容时,朋友圈的入口就会出现一个红点,而当朋友圈里有人给我们点了赞,或者对我们发布的内容进行了评论的时候,朋友圈的入口就会显示一个数字。

但是,我们在试用一些新的App产品时,总会发现它们在数字和红点展示上存在各种各样的问题。比如,红点怎么点击也清除不掉;或者,发现有数字了,点进去却什么也没有;或者,点进去看到的数字和外面看到的不一样。

那这些问题到底是怎样产生的呢?

我猜测,问题产生的根源是:没有对数字和红点的展示逻辑做一个统一的抽象和管理,以至于各种数字和红点之间的关系错综复杂,牵一发而动全身。这样,在App的维护过程中,稍微有一点改动(比如增加几个数字或红点类型),出现问题的概率就很高。

本文会提出一个树型结构模型,来对数字和红点的层次结构进行统一管理,并会在文章最后给出一个可以运行的Android版的Demo程序,以供参考。

如果您现在手头正好有一部Android手机,那么您可以先扫描下面的二维码(或点击二维码下面的下载链接)下载安装这个Demo,花几分钟看看它是否对您有用。

点击下载链接。

朴素的数字红点管理方式

为了讨论方便,我们首先对一般情况下数字和红点展示的需求做一个简单的整理,然后看看根据这样的需求最直观的实现方式可能是怎样的。

Badge Count Demo

Badge Dot Demo

相信以上总结的几点,跟大多数App的展示逻辑大体类似。即使有一些差别,应该也不妨碍我们接下来的讨论。

好,现在我们就以上面App截图中的具体情形来考虑一下实现。“消息”Tab包含“收到的评论”、“收到的赞”和“系统消息”,其中评论和赞是数字,系统消息是红点。

我们单独考虑“消息”这个Tab上的数字红点展示逻辑,不难写出类似如下的代码(伪码):

int count = 评论数 + 赞数;
if (count > 0) {
    展示数字count
}
else if (有系统消息) {
    展示红点
}
else {
    隐藏数字和红点
}

这段代码当然能实现需求,但是缺点也是很明显的。其中最关键的是,它要求在“消息”这个Tab上的展示逻辑要列举下面包含的所有子消息类型(评论、赞、系统消息),并且知道每个类型是数字还是红点。上面只是给出了两级页面的情况,如果出现三级页面甚至更多级呢?那么这些信息就要在各级页面上重复一遍。

这会造成维护和修改变得复杂。想象一下,在“消息”下面又增加了一个新的消息类型,或者某个类型的消息从数字展示变成红点展示了,甚至是某个类型的消息,从一个页面栈移动到了另一个页面栈了。所有这些情况,都要求更高层级的所有页面都对应进行修改。当一个App的消息类型越来越多,达到几十个的时候,可以想象这种修改是很容易出错的。

基于树型模型的数字红点管理方式

上面说的问题,我们在微爱App开发的初期也遇到过。后来,我们重新审视了App中红点和数字展示的结构,使用树型结构来看待它,让维护工作变得简单。

一个App的页面本身就是分级的,对于页面的访问路径本质上就是个树型结构。

Badge Number Tree结构图

如上图所示,节点1代表第1级页面,这个页面下面包含三个更深一级(第2级)的页面入口,分别对应节点2,3,4。再深一级就到了终端页面,以绿色的方形节点表示。

这个树型的模型可以如下表述:

Number类型type分配在同一个类型区间内。比如上面树型结构图中2,3,4节点就分别对应三个业务类型,也就是三个大类,它们对应的类型区间分别为[A, C], [X, Y], [R, T]。再举一个实际的例子,比如微信朋友圈是一个业务大类,里面的Badge Number类型包括:有人评论我(数字),有人给我点赞(数字),好友有新消息发布(红点),等。

为了使得一个大类内的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下载源代码):

/**
 * 用于异步返回结果的接口.
 */
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的代码例子如下:

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);
        }
    }
});
关于实现上的一些补充说明

原创文章,转载请注明出处!
本文链接:http://zhangtielei.com/posts/blog-badge-number-tree.html