美文网首页126安卓爱上Android
[贝聊科技]有关Android应用桌面角标(BadgeNumbe

[贝聊科技]有关Android应用桌面角标(BadgeNumbe

作者: 贝聊科技 | 来源:发表于2017-10-30 16:30 被阅读1846次

    作者:小强 贝聊移动开发部 Android工程师

    前言:本文主要讲述了以下三方面:

    1. 怎么在Android系统下让自家的应用图标像iOS系统那样支持数字角标的显示?
    2. 在网上找不到现成的解决方案的情况下,该如何去寻找问题的突破口?
    3. 一种简洁性和扩展性都比较高的封装思路。

    先放一个传送门:GitHub传送门

    1.Android系统下如何支持应用桌面角标(BadgeNumber)的显示

    iOS系统下的应用桌面角标
    其实本来Android原生系统是不支持应用桌面角标(BadgeNumber)显示的。我们目前看到的能支持应用桌面角标显示的Android系统,都是第三方厂商自己定制的。通过实现一套自己的launcher并且提供外部接口给第三方应用来调用即可。

    我们公司的APP里涉及到IM的功能。所以经常会有用户向客服反馈,为什么QQ、微信都支持应用桌面角标的显示,但你们的APP却不行......本着用户就是上帝的原则,于是应用桌面角标显示的优化就提上了日程。其实,测试部门在之前就已经跟我们提过这事了,只不过当时正忙于项目开发,没时间优化。前段时间需求不多的时候,给公司的Android应用加上了桌面角标显示的支持。现在将这个优化的过程总结一下。

    目前已经存在的开源库

    如果大家有接触过这方面的优化,应该很快就可以在搜索引擎上找到某个被推荐次数较多的开源库 ShortcutBadger

    虽然这个库适配的覆盖机型貌似很多,但在实际的测试中发现,某些方法可能对于目前市面上的国产流行机型已经不奏效了。所以,不建议大家直接将这个开源项目用到项目中去。作为学习和参考倒是一个不错的选择。而且,在实际方案抉择的过程中,我们发现,公司的APP主流机型排行榜中,前十的机型几乎被OPPO、vivo、华为、小米这四个品牌屠榜了。所以,我们的优化目标暂时就先定下来了:先集中精力适配市面上的这四个主流品牌机型。其他的冷门机型,后面再慢慢完善。(其实实际上我们也找不来那么多冷门的机型进行测试,所以对于没自身确认过奏效的方案,即使网上已经有人给出,出于谨慎还是先不采纳)

    国产主流机型应用角标的适配(OPPO、vivo、华为、小米)

    在开始之前,先声明一下。第一,不是所有的国产手机都能找到支持角标显示的方案(即使理论上可以,可能人家只对QQ微信等一些国民级的应用开放设置应用角标的白名单)。第二,本文中涉及到的方案都是经过实际测试且奏效的了(因为测试手机有限,所以不敢说针对这四个品牌的手机机型百分百支持,但支持大部分的机型应该是没问题的)。而且,有些品牌的手机适配方案很容易找到,有些品牌的适配方案则很难找到,这部分我会放到后面的章节来说。下面直接上适配方案:

    华为:

    先在AndroidManifest文件里配置好下面的权限:

     <!--华为手机更新应用桌面角标需要的权限-->
        <uses-permission android:name="com.huawei.android.launcher.permission.CHANGE_BADGE"/>
    

    设置角标的方法如下:

     public static void setBadgeNumber(Context context, int number) {
            try {
                if (number < 0) number = 0;
                Bundle bundle = new Bundle();
                bundle.putString("package", context.getPackageName());
                String launchClassName = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName()).getComponent().getClassName();
                bundle.putString("class", launchClassName);
                bundle.putInt("badgenumber", number);
                context.getContentResolver().call(Uri.parse("content://com.huawei.android.launcher.settings/badge/"), "change_badge", null, bundle);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    

    OPPO:

        public static void setBadgeNumber(Context context, int number) {
            try {
                if (number == 0) {
                    number = -1;
                }
                Intent intent = new Intent("com.oppo.unsettledevent");
                intent.putExtra("pakeageName", context.getPackageName());
                intent.putExtra("number", number);
                intent.putExtra("upgradeNumber", number);
                if (canResolveBroadcast(context, intent)) {
                    context.sendBroadcast(intent);
                } else {
                    try {
                        Bundle extras = new Bundle();
                        extras.putInt("app_badge_count", number);
                        context.getContentResolver().call(Uri.parse("content://com.android.badge/badge"), "setAppBadgeCount", null, extras);
                    } catch (Throwable t) {
                        t.printStackTrace();
                    }
                }
    
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        public static boolean canResolveBroadcast(Context context, Intent intent) {
            PackageManager packageManager = context.getPackageManager();
            List<ResolveInfo> receivers = packageManager.queryBroadcastReceivers(intent, 0);
            return receivers != null && receivers.size() > 0;
        }
    

    vivo:

    public static void setBadgeNumber(Context context, int number) {
            try {
                Intent intent = new Intent("launcher.action.CHANGE_APPLICATION_NOTIFICATION_NUM");
                intent.putExtra("packageName", context.getPackageName());
                String launchClassName = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName()).getComponent().getClassName();
                intent.putExtra("className", launchClassName);
                intent.putExtra("notificationNum", number);
                context.sendBroadcast(intent);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    

    小米:

    小米的设置应用角标方式比较有个性,跟其他厂商的不太一样,是跟Notification绑定在一起的。而且小米系统还有个比较特殊的地方,如果在应用内直接调用设置角标的方法,设置角标会不生效,所以只能在应用在后台并且收到推送的情况下进行角标的设置。另外,即使你设置了角标的显示,只要用户点击应用图标进入到应用内,应用的角标就会自动消失掉,即使应用内还存在新的未读消息。所以,针对小米机型,建议在收到推送后并且进行notification的时机更新应用角标

    //在调用NotificationManager.notify(notifyID, notification)这个方法之前先设置角标显示的数目
    
    public static void setBadgeNumber(Notification notification, int number) {
            try {
                Field field = notification.getClass().getDeclaredField("extraNotification");
                Object extraNotification = field.get(notification);
                Method method = extraNotification.getClass().getDeclaredMethod("setMessageCount", int.class);
                method.invoke(extraNotification, number);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    

    2.在网上找不到现成的解决方案的情况下,该如何去寻找问题的突破口?

    在上面的适配方案中,最容易找到而且奏效的就是华为和小米的适配方案。而OPPO的适配方案,即使找到了,在现有的测试机型上却不奏效;vivo的是适配方案则是最难找的。既然在网上找不到,而QQ和微信貌似又是适配得最好的,这就说明,QQ和微信的源码里肯定有现成的解决方案。那么,不如尝试一下反编译,看看能不能从这两个超级APP中找到一些灵感?

    在对QQ的apk进行反编译后,在某各类下果然找到了设置应用角标的实现类:

    QQ设置桌面角标的实现类

    从上图可以看出,QQ对于各种厂商的适配算是比较完善的了。除了小米、华为、OPPO、vivo,还适配了联想、三星、索尼等。

    不同机型的适配方法也都有具体的实现:(下面是手Q对于OPPO和vivo的适配)

    手Q对于OPPO和vivo的适配

    但是,我们也不能直接拷贝过来就使用。因为说不定有些方法只针对QQ才生效呢是吧?

    在对微信的apk进行反编译后,也能找到关于应用角标适配的代码:

    微信对于vivo手机桌面角标的适配

    总之,对比了一下QQ和微信的源码,在某些机型的适配方式上,可能两边会有些出入。实现方式可能也不太一样。但不得不说,不亏是大厂的APP,看了源码后,实现是学习了很多,特使是一些细节上的处理。

    上面总结出的适配方案,其实就是在参考了网上各种资料以及QQ和微信的源码之后总结出来的可行的适配方案。如果还不满足大家的需求,大家可以去找一下QQ和微信的源码来进行研读,并总结出一套属于自己的适配方案。

    3.一种扩展性比较高的简洁的封装思路

    看完了QQ和微信的源码后,我发现两边都有一个共同点,那就是某个实现类里塞了很多适配的方法。估计也是可能涉及到不同的人在不同时期维护的历史原因。但一个类里面的代码太多了,可能会对查阅以及后续维护造成一些不便。

    这里,我参考了Android源码里面NotificationManagerCompat这个类的实现方式。Android源码中本身就涉及到很多关于不同版本的适配的场景。某个方法,在不同的版本下,可能实现方式不太一样。于是,怎么在不断往某个类增加不同的实现方式的情况下,保持代码的美观以及扩展性易读性变成了一个问题。NotificationManagerCompat这个类的实现就十分简洁美观。下面是一部分源码截图,有兴趣的可以直接去看一下完整的源码。

    下面就是模仿后的实现:

    public class BadgeNumberManager {
    
        private Context mContext;
    
        private BadgeNumberManager(Context context) {
            mContext = context;
        }
    
        public static BadgeNumberManager from(Context context) {
            return new BadgeNumberManager(context);
        }
    
        private static final BadgeNumberManager.Impl IMPL;
    
        /**
         * 设置应用在桌面上显示的角标数字
         * @param number 显示的数字
         */
        public void setBadgeNumber(int number) {
            IMPL.setBadgeNumber(mContext, number);
        }
    
        interface Impl {
    
            void setBadgeNumber(Context context, int number);
    
        }
    
        static class ImplHuaWei implements Impl {
    
            @Override
            public void setBadgeNumber(Context context, int number) {
                BadgeNumberManagerHuaWei.setBadgeNumber(context, number);
            }
        }
    
        static class ImplVIVO implements Impl {
    
            @Override
            public void setBadgeNumber(Context context, int number) {
                BadgeNumberManagerVIVO.setBadgeNumber(context, number);
            }
        }
    
    
        static class ImplBase implements Impl {
    
            @Override
            public void setBadgeNumber(Context context, int number) {
                //do nothing
            }
        }
    
        static {
            String manufacturer = Build.MANUFACTURER;
            if (manufacturer.equalsIgnoreCase("Huawei")) {
                IMPL = new ImplHuaWei();
            } else if (manufacturer.equalsIgnoreCase("vivo")) {
                IMPL = new ImplVIVO();
            } else if (manufacturer.equalsIgnoreCase("XXX")) {
                //其他品牌机型的实现类
                IMPL = new ImplXXX();
                ......
            } else {
                IMPL = new ImplBase();
            }
        }
    }
    

    使用的时候,只需要调用BadgeNumberManager.from(context).setBadgeNumber(num)就行了。BadgeNumberManagerHuaWeiBadgeNumberManagerVIVO等都是针对某个手机品牌的具体实现类。

    当然,这只是一种实现的思路而已。具体去实现的时候,请根据自己项目的实际情况,怎样实现扩展性可读性较高就选哪种。

    4.后记

    如果有关于别的机型的适配方案,欢迎在评论下留言(最好是自己亲自测试过并且有效的)。如果文章中有出现错误的地方,欢迎指正。如果对于文章中的某些部分有不同的理解和想法,或者有更好的想法, 也欢迎留言讨论。

    5.填坑记录(2017.12.11)

    1. 经过测试,目前暂时不支持的机型:华为荣耀6、OPPO A59、OPPO R9,OPPO R11、vivo X9i(截止至2017.12.11)

    2. 一开始以为某些机型不支持可能是少了某些跟角标设置相关的权限,于是反编译微信、QQ、支付宝,从这些App中收集AndroidManifest里配置的可能跟角标设置相关的权限,并添加到Demo中来测试,后来发现还是不行

    3. 针对华为手机,在某些机型上,例如华为 mate9,在manifest里除了需要配置com.huawei.android.launcher.permission.CHANGE_BADGE权限之外,还需要配置android.permission.INTERNET权限才可以正常设置桌面角标(不过一般的App应该都会配置了android.permission.INTERNET权限)

    4. 关于OPPO手机,在一些较旧的机型上可以正常设置桌面角标,但在一些比较新的机型上(例如OPPO R9,OPPO R11等),只有在通知权限管理中,有“在桌面图标上显示角标”这个选项的App才可以正常设置角标。目前就只发现QQ,微信,钉钉有这个权限,就连支付宝都没有这个权限。于是尝试着写了个Demo,将Demo的包名改成了微信的包名,然后在通知权限管理中,就出现了“在桌面图标上显示图标”这个选项。所以,在新的机型上,OPPO应该是根据包名来维护了一个白名单,只针对一些比较大型的IM类型的App开放桌面角标设置的权限。所以,这个问题暂时还没有解决方法

    相关文章

      网友评论

      • 机智的小阿文:楼主求问,oppo r11s手机上,今日头条在系统设置里是没有桌面角标设置项的,但依然能显示角标~不知道是怎么实现的呢😉
      • b3f565d2994d:Bravo,此处有一千个掌声
      • wenzhihao123:vivo 好几款机型都不支持~
      • 苏联老斯基:楼主,在小米手机上,如果新建角标在点击事件里的话,每次都可以,但是如果进来直接设置角标,当退出APP后,再次进入,角标就不再显示出来了,这个有什么好的解决方案么
      • 柴西卡夫卡:楼主写的很好,希望可以继续更新,我试了vivo x7L/ x9 都不行,有支持的机型统计吗?
      • 魏魏魏魏:没有评论了?
      • Allen__Qin:楼主,demo是少配置吗,我这儿所有的手机都不能用呢
        大校啊:确实啊 我这也是都没走通 华为荣耀8 oppoA59 小米5s 还有一个魅族的什么我记不清了 都没跑通啊 建议给个demo下载地址,看下到底怎么回事
        Allen__Qin:@贝聊科技 华为H60-L01 4.4版本 。 红米Note3 5.0 。Galaxy S6 edge 还有个乐视2的
        贝聊科技:是华为,小米,oppo,vivo的手机吗?具体涉及到的手机型号能不能说一下?
      • Android_Romance:求大神解答一下 你怎么反编译微信 QQ的 人家混淆的 看代码 根本看不到 是什么东西 你是怎么解决的
        贝聊科技:直接用最基本的反编译方式就行啦,网上很多关于反编译的教程可以去了解一下,最好自己去尝试一下,这样体会会更深刻点
      • 大校啊:楼主可以留下一个demo的apk下载地址吗?我用了demo的方法无法实现
        大校啊:@贝聊科技 oppo a59
        贝聊科技:请具体描述一下你遇到的问题。在什么手机型号上无法实现角标的显示呢?
      • 魏魏魏魏:小米5第一次成功,再次发送的话就不能显示角标了,但是通知都在,,,,而且我想问的是,在其它手机什么时候来删除角标?比如点击桌面图标的时候怎么得到事件,通知被删除的时候怎么得到事件呢?
        贝聊科技:@天狼0730 你好,我们知道问题所在了。是我们demo考虑得不周到。如果在应用内直接调用设置角标的方法,设置角标会不生效,所以只能在应用在后台并且收到推送的情况下进行角标的设置。demo已经修改了,如果是小米手机,先退出到桌面,延迟几秒再进行角标的设置就没问题了。
        天狼0730:@贝聊科技 他的意思就是你的demo中第一次安装点击设置桌面角标,OK,返回桌面显示角标,再次点击应用图标应用的图标就消失了,然后再次重复上述动作点击设置桌面角标按钮,就不好使了,Toast正常弹出,只有重新运行demo才能再运行一次。
        贝聊科技:再次发送的话就不能显示角标是什么意思呢?小米系统有个比较特殊的地方,那就是即使你设置了角标的显示,但只要用户点击应用图标进去,应用的角标就会自动消失掉,不用手动清除角标。所以,对于小米手机,我的处理方式是,在收到推送的时候同时更新角标的显示。具体请参考demo。至于其他手机,得看你们具体的业务逻辑需求吧。举个例子,微信桌面角标显示了10,那说明里面有10条未读的新消息。但朋友圈的红点提示是不会被统计到未读消息里面的。在未读消息数变化的时候更新角标显示就行了。
      • 淡淡me:我试了oppo r9和三星galaxy a5都不行
        贝聊科技:@水墨天空 我这边确认一下再跟你们反馈。
        贝聊科技:三星不行是因为我们没有对三星做适配,这里只针对华为小米oppo vivo的机型==至于oppo r9,我这边确认一下再跟你们反馈哈。
        45fafb183fba:@淡淡me 那你是怎么解决的,我的oppo也不显示
      • 光头的小逛逛:怎么反编译QQ的?:wink:
        贝聊科技:就,直接用最基本的反编译方式就行啦,网上很多关于反编译的教程可以去了解一下,自己尝试一下就知道了
      • 最最最最醉人:反编译后是怎么把混淆也给还原的呢?
        贝聊科技:其实没有大家想得那么困难,直接用最基本的反编译方式就行了,只是反编译后在源码里面找它们的实现类的时候花了点时间
      • beforenight:很棒的思路,很棒的解决方案~
        45fafb183fba:@beforenight 你的oppo可以显示角标吗

      本文标题:[贝聊科技]有关Android应用桌面角标(BadgeNumbe

      本文链接:https://www.haomeiwen.com/subject/dsfmpxtx.html