在 Android 生态中,应用角标(Badge)适配一直是开发者面临的痛点问题。由于 Android 系统本身未提供统一角标 API,不同厂商设备(如华为、小米、OPPO、vivo 等)均采用私有实现方案。以下是针对主流厂商的角标适配技术指南及未来趋势分析:
一、当前主流厂商适配方案
1. 原生 Android 方案(API 26+)
xml
NotificationCompat.Builder(context, CHANNEL_ID)
.setNumber(unreadCount) // 数字角标
.setBadgeIconType(NotificationCompat.BADGE_ICON_SMALL) // 图标类型
- 局限:仅部分 OEM 厂商支持,且显示逻辑不统一(如 Pixel 设备优先显示数字,三星可能显示红点)。
2. 华为 HMS Core
java
// ================== 华为 ================
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);
//================== 荣耀 ====================
//荣耀从华为独立之后,其设置角标的规则也进行更改,不过整体的改动不大
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.hihonor.android.launcher.settings/badge/"), "change_badge", null, bundle);
- 注意 :需申请
com.huawei.android.launcher.permission.CHANGE_BADGE
权限。
3. 小米 MIUI
java
Intent intent = new Intent("android.intent.action.APPLICATION_MESSAGE_UPDATE");
intent.putExtra("android.intent.extra.update_application_component_name",
context.getPackageName() + "/" + launcherClassName);
intent.putExtra("android.intent.extra.update_application_message_text", count);
context.sendBroadcast(intent);
- 限制 :当
count=0
时需发送空字符串清除角标。 - 注意:小米手机比较特殊,其App角标与App通知相关联,无法脱离通知栏独立设置角标未读数量。
4. OPPO ColorOS
java
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();
}
}
private static boolean canResolveBroadcast(Context context, Intent intent) {
PackageManager packageManager = context.getPackageManager();
List<ResolveInfo> receivers = packageManager.queryBroadcastReceivers(intent, 0);
return receivers != null && receivers.size() > 0;
}
- 要求 :需添加权限
<uses-permission android:name="com.oppo.launcher.permission.WRITE_SETTINGS"/>
。
5. vivo
5.1、Funtouch OS
java
Intent intent = new Intent();
int missedCalls = 10;
intent.setAction("launcher.action.CHANGE_APPLICATION_NOTIFICATION_NUM");
intent.putExtra("packageName", "com.android.xxxx");//接入方自己的包名
intent.putExtra("className", "com.android.xxxx.Mainxxxx");//对应接入方的launcher入口的activity全路径activity名字(AndroidManifest中标识了android.intent.category.LAUNCHER的activity)
intent.putExtra("notificationNum", missedCalls);
intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
sendBroadcast(intent);
-
在ard8.0以后,还需要给Intent加上下面的Flag
Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND
-
如果此Flag获取不到,则改为此方法
java
public static int invokeIntconstants(String CanonicalName, String name, int default_value) {
int value = default_value;
try {
Class<?> c = Class.forName(CanonicalName);
Field Field = c.getField(name);
value = (int) Field.get(c);
} catch (Exception e) {
e.printStackTrace();
} finally {
return value;
}
}
- 限制:仅支持数字角标,且最大显示值为 99。
- 权限 :需申请
com.vivo.notification.permission.BADGE_ICON
权限。
5.2、Origin OS
java
Class<?> spClass = Class.forName("android.os.SystemProperties");
Method method = spClass.getMethod("get", String.class, String.class);
method.setAccessible(true);
currentOsName= (String) method.invoke(null, "ro.vivo.os.name" ,defName);
currentOsVersion=(String) method.invoke(null, "ro.vivo.os.version" ,defVersion);
- 权限 :需申请
com.vivo.abe.permission.launcher.notification.num
权限。
示例代码
java
public static void setBadgeNumber() {
Uri uri = Uri.parse("content://" +"com.vivo.abe.provider.launcher.notification.num");
Bundle extra = new Bundle();
extra.putString("package", String);//接入的App包名
extra.putString("class", String);//接入的App class名
extra.putInt("badgenumber", int);//目标的角标数
/*这里一定要先使用 ContentProviderClient 建立非稳连接,不可以直接通过 getContentResolver()调用 call 方法,会有 Server 端崩溃带崩 Client 端的风险*/
ContentProviderClient client = null;
try {
client = getContentResolver().acquireUnstableContentProviderClient(uri);
if (client != null) {
int result = client.call("change_badge", null, extra).getInt("result");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (client != null) {
if(Build.VERSION.SDK_INT >=Build.VERSION_CODES.N){
client.close();
} else {
client.release();
}
}
}
}
6. 三星
java
public static void setBadgeNumber(Context context, int number) {
Intent localIntent = new Intent("android.intent.action.BADGE_COUNT_UPDATE"); //数字
localIntent.putExtra("badge_count", number); //包名
localIntent.putExtra("badge_count_package_name", context.getPackageName()); //启动页
localIntent.putExtra("badge_count_class_name",
BadgeNumberManager.getLauncherClassName(context));
context.sendBroadcast(localIntent);
}
7. sony
java
public static void setBadgeNumber(Context context, int number) {
boolean isShow = true;
if ("0".equals(number)) {
isShow = false;
}
Intent localIntent = new Intent(); //是否显示
localIntent.putExtra("com.sonyericsson.home.intent.extra.badge.SHOW_MESSAGE", isShow);
localIntent.setAction("com.sonyericsson.home.action.UPDATE_BADGE"); //启动页
localIntent.putExtra("com.sonyericsson.home.intent.extra.badge.ACTIVITY_NAME",
BadgeNumberManager.getLauncherClassName(context)); //数字
localIntent.putExtra("com.sonyericsson.home.intent.extra.badge.MESSAGE", number); //包名
localIntent.putExtra("com.sonyericsson.home.intent.extra.badge.PACKAGE_NAME",
context.getPackageName());
context.sendBroadcast(localIntent);
}
二、统一适配方案建议
1. 使用 ShortcutBadger 库
gradle
implementation 'me.leolin:ShortcutBadger:1.1.22'
java
// 设置角标
ShortcutBadger.applyCount(context, count);
// 清除角标
ShortcutBadger.removeCount(context);
- 优势:封装了 30+ 厂商的私有 API,覆盖华为、小米、三星等主流设备。
- 局限:无法保证 100% 设备兼容性,需定期更新。
2. 分层适配策略
java
public class BadgeUtils {
public static void updateBadge(Context context, int count) {
if (Build.VERSION.SDK_INT >= 26) {
// 使用原生 Notification 角标
updateNotificationBadge(context, count);
} else {
// 厂商私有 API 适配
if (isXiaomi()) {
updateXiaomiBadge(context, count);
} else if (isHuawei()) {
updateHuaweiBadge(context, count);
}
// 其他厂商...
}
}
}
三、未来趋势与挑战
- 折叠屏/多任务场景
折叠屏设备的分屏模式要求角标在多任务界面同步更新,需监听ActivityManager.AppTask
状态变化。 - 动态岛(Dynamic Island)扩展
Android 14 引入的 "Enhanced Notifications" 支持更丰富的交互式角标,需适配新的Notification.Style
。 - 隐私合规要求
Android 13 限制后台广播接收器,需改用JobScheduler
或WorkManager
异步更新角标。 - 统一标准推进
谷歌正在推动 Unified Badge API 提案(预计 Android 15 落地),未来可能通过LauncherApps
服务提供标准化接口。
四、适配检查清单
- ✅ 在
AndroidManifest.xml
中声明所有所需权限 - ✅ 获取 Launcher Activity 的完整类名(如
com.example.MainActivity
) - ✅ 处理厂商 ROM 的版本差异(如 MIUI 12 与 MIUI 13 的角标逻辑变化)
- ✅ 在应用设置中引导用户开启通知权限
- ✅ 使用云真机测试平台验证多设备兼容性
总结:
Android 角标适配本质上是与厂商生态的博弈,开发者需在 统一封装 与 厂商定制 间找到平衡点。随着 Android 15 标准化进程的推进,未来有望通过统一 API 降低适配成本,但短期内仍需保持多路径适配策略。