主页 > 开发文档 > Android应用桌面角标红点的实现

Android应用桌面角标红点的实现

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

 

iOS系统下的应用桌面角标
iOS系统下的应用桌面角标

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

 

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

目前已经存在的开源库

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

 

 

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

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

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

华为:

先在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绑定在一起的。而且小米系统还有个比较特殊的地方,那就是即使你设置了角标的显示,但只要用户一点进去,应用的角标就会自动消失掉,即使应用内还存在新的未读消息。除非有新的通知或消息到达。

//在调用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的适配方案则是最难找的。既然在网上找不到,而在国内某Q和某信貌似又是适配得最好的,这就说明,某Q和某信的源码里肯定有现成的解决方案。那么,不如尝试一下反编译,看看能不能从这两个APP中找到一些灵感?

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

 

某Q设置桌面角标的实现类
某Q设置桌面角标的实现类

 

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

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

 

某Q对于OPPO和vivo的适配
某Q对于OPPO和vivo的适配

 

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

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

 

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

 

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

上面总结出的适配方案,其实就是在参考了网上各种资料以及某Q和某信的源码之后总结出来的可行的适配方案。如果还不满足大家的需求,大家可以发挥一下自己的主观能动性,找到自己想要的解决方案,并总结出一套属于自己的适配方案。

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

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

这里,我参考了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等都是针对某个手机品牌的具体实现类。

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