前言
桌面图标的角标,看着是个很简单的功能,就是在应用的右上角显示当前有几个未读消息;在网上查了资料之后,发现很多同行说,Android原生是没有此功能,平时使用的手机都有该功能,其实是国内手机厂商自己定制的桌面图标角标,且不同厂商之间,方案还不尽相同;但是此次我还是要看Android源生代码究竟是怎么显示应用有新的未读消息,此篇文章是以为记。
一
很直观,它是显示在应用的右上角的,那么我们要查看它,得先在代码中找到单个应用显示的类,根据上篇文章介绍,应用是在CellLayout平均分配的矩形中显示,可以先查看CellLayout的源码,在CellLayout的源码中,找到了下面的方法:
public boolean addViewToCellLayout(View child, int index, int childId, LayoutParams params,
boolean markCells) {
final LayoutParams lp = params;
// Hotseat icons - remove text
if (child instanceof BubbleTextView) {
BubbleTextView bubbleChild = (BubbleTextView) child;
bubbleChild.setTextVisibility(mContainerType != HOTSEAT);
}
child.setScaleX(mChildScale);
child.setScaleY(mChildScale);
// Generate an id for each view, this assumes we have at most 256x256 cells
// per workspace screen
if (lp.cellX >= 0 && lp.cellX <= mCountX - 1 && lp.cellY >= 0 && lp.cellY <= mCountY - 1) {
// If the horizontal or vertical span is set to -1, it is taken to
// mean that it spans the extent of the CellLayout
if (lp.cellHSpan < 0) lp.cellHSpan = mCountX;
if (lp.cellVSpan < 0) lp.cellVSpan = mCountY;
child.setId(childId);
if (LOGD) {
Log.d(TAG, "Adding view to ShortcutsAndWidgetsContainer: " + child);
}
mShortcutsAndWidgets.addView(child, index, lp);
if (markCells) markCellsAsOccupiedForView(child);
return true;
}
return false;
}
该方法是将子View添加到CellLayout中;而我们知道CellLayout中要么显示应用的快捷方式,要不显示桌面小部件,如果是Hotseat里么的CellLayout,那么它就只能显示应用的快捷方式,且不显示它的名称,从代码得知应用的快捷方式的类就是BubbleTextView。
// Hotseat icons - remove text
if (child instanceof BubbleTextView) {
BubbleTextView bubbleChild = (BubbleTextView) child;
bubbleChild.setTextVisibility(mContainerType != HOTSEAT);
}
在BubbleTextView源码中,确实有看到在该View的右上角画标记,但是没有发现有设置未读消息的代码,经过修改代码运行得知,此标记仅表示有新消息,但是没有消息的条数,具体修改代码如下:
protected void drawBadgeIfNecessary(Canvas canvas) {
//设置mBadgeScale以满足条件,画出右上角标记信息
mBadgeScale = 1.0f;
if (!mForceHideBadge && (hasBadge() || mBadgeScale > 0)) {
getIconBounds(mTempIconBounds);
mTempSpaceForBadgeOffset.set((getWidth() - mIconSize) / 2, getPaddingTop());
final int scrollX = getScrollX();
final int scrollY = getScrollY();
canvas.translate(scrollX, scrollY);
mBadgeRenderer.draw(canvas, mBadgeColor, mTempIconBounds, mBadgeScale,
mTempSpaceForBadgeOffset);
canvas.translate(-scrollX, -scrollY);
}
}
运行结果如下:
所以得出结论,原生图标角标与国内手机的图标角标有出入。
二
接下来要学习一下国内手机的图标角标都是怎么实现的。
小米
本人有部红米手机,所以就以小米手机为例,进行学习,通过查找,在小米开发平台找到相关资料;小米桌面角标
- 默认逻辑
当应用向通知栏发送了一条通知 (除了进度条样式和常驻通知外),应用图标的右上角就会显示「1」。角标的数字代表应用的通知数,即应用发送了「x」条通知,角标就会显示为「x」。
- 开发者如何设置桌面角标
2.1 MIUI6-MIUI11桌面应用角标适配方法
可通过反射调用设置桌面角标,参考代码如下:
try {
Field field = notification.getClass().getDeclaredField(“extraNotification”);
Object extraNotification = field.get(notification);
Method method = extraNotification.getClass().getDeclaredMethod(“setMessageCount”, int.class);
method.invoke(extraNotification, mCount);
} catch (Exception e) {
e.printStackTrace();
}
2.2 MIUI12及以后桌面应用角标适配方法
由于Google屏蔽了hideAPI的反射调用,因此MIUI12及以后可以使用notification.number,可参照Android开发者文档https://developer.android.google.cn/reference/android/app/Notification#number,参考代码如下:
Notification notification = new Notification.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.notification_icon)
.setContentTitle(textTitle)
.setContentText(textContent)
.setNumber(int number)
.build();
- 如何判断MIUI版本
可参考文档https://dev.mi.com/console/doc/detail?pId=1312,其中6.1节有具体方法说明。
由于本人的手机版本是MIUI12.5.6,所以只能参考上述文档中2.2的适配方法,写了如下测试代码:
private void createShortcutBadger() {
//获取通知管理类
NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
//通知在Android8.0之后需要创建通道,才能弹出来
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel("1", "name", NotificationManager.IMPORTANCE_HIGH);
mNotificationManager.createNotificationChannel(channel);
}
Notification notification = new Notification.Builder(this, "1")
.setSmallIcon(R.drawable.ic_launcher_background)
.setContentTitle("通知标题")
.setContentText("通知内容")
.setNumber(1)
.build();
//告诉系统要出现哪个通知
mNotificationManager.notify(0, notification);
}
通知可以创建成功,可应用的角标却没有,在网上查阅资料后,得知原因是,当我们创建通知时,已经在应用内部了,所以创建的通知,应用认为是已读状态,所以并不会在应用图标的右上角显示未读消息个数,所以对代码做了改动如下:
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
createShortcutBadger();
}
},3000);
经过测试,发现此时图标的角标还是未能显示,后想是否是因为手机里的某些权限未开启,经过查找,找到了设置->通知与控制中心->桌面角标,发现自己写的应用的角标权限确实未开启,开启之后,再次验证,角标正常显示。
但是此代码只适用于MIUI12及以后桌面的图标角标显示,为了能够适配所有小米手机的角标显示,根据文档对源码做了如下改动;
private void createShortcutBadger() {
String code = PropUtils.get("ro.miui.ui.version.code", "7");
Integer intCode = Integer.valueOf(code);
/**
* 当前手机code为11,但是不清楚MIUI12的code是多少,所以就以11为临界点。
*/
if (intCode >= 11){
createHighShortcutBadger();
} else {
createLowShortcutBadger();
}
}
private void createLowShortcutBadger() {
//获取通知管理类
NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
//通知在Android8.0之后需要创建通道,才能弹出来
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel("1", "name", NotificationManager.IMPORTANCE_HIGH);
mNotificationManager.createNotificationChannel(channel);
}
Notification notification = new Notification.Builder(this, "1")
.setSmallIcon(R.drawable.ic_launcher_background)
.setContentTitle("通知标题")
.setContentText("通知内容")
.build();
try {
Field field = notification.getClass().getDeclaredField("extraNotification");
Object extraNotification = field.get(notification);
Method method = extraNotification.getClass().getDeclaredMethod("setMessageCount", int.class);
method.invoke(extraNotification, 1);
} catch (Exception e) {
e.printStackTrace();
}
//告诉系统要出现哪个通知
mNotificationManager.notify(0, notification);
}
private void createHighShortcutBadger() {
//获取通知管理类
NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
//通知在Android8.0之后需要创建通道,才能弹出来
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel("1", "name", NotificationManager.IMPORTANCE_HIGH);
mNotificationManager.createNotificationChannel(channel);
}
Notification notification = new Notification.Builder(this, "1")
.setSmallIcon(R.drawable.ic_launcher_background)
.setContentTitle("通知标题")
.setContentText("通知内容")
.setNumber(1)
.build();
//告诉系统要出现哪个通知
mNotificationManager.notify(0, notification);
}
package com.yangshuangyue.myshortcutbadger;
import java.lang.reflect.Method;
class PropUtils {
/**
* 设置属性值
*
* @param key 长度不能超过31,key.length <= 30
* @param value 长度不能超过91,value.length<=90
*/
public static void set(String key, String value) {
// android.os.SystemProperties
// public static void set(String key, String val)
try {
Class<?> cls = Class.forName("android.os.SystemProperties");
Method method = cls.getMethod("set", String.class, String.class);
method.invoke(null, key, value);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 获取属性值
*
* @param key 长度不能超过31,key.length <= 30
* @param defValue
* @return
*/
public static String get(String key, String defValue) {
// android.os.SystemProperties
// public static String get(String key, String def)
try {
Class<?> cls = Class.forName("android.os.SystemProperties");
Method method = cls.getMethod("get", String.class, String.class);
return (String) method.invoke(null, key, defValue);
} catch (Exception e) {
e.printStackTrace();
}
return defValue;
}
}
没有MIUI12以下的手机,未能验证。
参考资料:
Android开发:史上最全Android应用角标适配方法
MIUI6&7桌面角标开源代码简介
桌面应用角标适配说明
关于小米 角标不显示问题
使用SystemProperties的几种方式
华为
1、声明权限
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="com.huawei.android.launcher.permission.CHANGE_BADGE"/>
2、在需要进行角标显示地方,采用如下方法传递数据给华为桌面应用。
Bundle extra = new Bundle();
extra.putString("package", "xxxxxx");
extra.putString("class", "yyyyyyy");
extra.putInt("badgenumber", i);
context.getContentResolver().call(Uri.parse("content://com.huawei.android.launcher.settings/badge/"), "change_badge", null, extra);
关键参数说明:
- package:应用包名
- class:桌面图标对应的应用入口Activity类
- badgenumber:角标数字
示例
boolean mIsSupportedBade = true;
if (mIsSupportedBade) {
setBadgeNum(num);
}
/** set badge number*/
public void setBadgeNum(int num) {
try {
Bundle bunlde = new Bundle();
bunlde.putString("package", "com.test.badge"); // com.test.badge is your package name
bunlde.putString("class", "com.test. badge.MainActivity"); // com.test. badge.MainActivity is your apk main activity
bunlde.putInt("badgenumber", num);
this.getContentResolver().call(Uri.parse("content://com.huawei.android.launcher.settings/badge/"), "change_badge", null, bunlde);
} catch (Exception e) {
mIsSupportedBade = false;
}
}
参考资料
Android开发:史上最全Android应用角标适配方法
华为桌面角标开发指导书
三星
可以通过广播机制直接设置应用角标,且应用在前台和被杀掉后仍可显示
private static boolean setSamsungBadge(int count, Context context) {
try {
String launcherClassName = getLauncherClassName(context);
if (TextUtils.isEmpty(launcherClassName)) {
return false;
}
Intent intent = new Intent("android.intent.action.BADGE_COUNT_UPDATE");
intent.putExtra("badge_count", count);
intent.putExtra("badge_count_package_name", context.getPackageName());
intent.putExtra("badge_count_class_name", launcherClassName);
context.sendBroadcast(intent);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
参考资料
联想
private static boolean setZukBadge(int count, Context context) {
try {
Bundle extra = new Bundle();
ArrayList<String> ids = new ArrayList<>();
// 以列表形式传递快捷方式id,可以添加多个快捷方式id
// ids.add("custom_id_1");
// ids.add("custom_id_2");
extra.putStringArrayList("app_shortcut_custom_id", ids);
extra.putInt("app_badge_count", count);
Uri contentUri = Uri.parse("content://com.android.badge/badge");
Bundle bundle = context.getContentResolver().call(contentUri, "setAppBadgeCount", null,
extra);
return bundle != null;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}