最近一段时间写的都是android源码的文章,前几天在公司做了一个需求是关于前台服务的,在写前台服务的时候深入使用到了通知,今天就先写一篇文章总结一下通知的相关内容,后面有时间了在介绍一下前台服务的相关内容。
本篇文章主要介绍通知的以下知识点:
上面的这么多内容基本覆盖了通知百分之八十的知识点,了解这些足够日常通知的使用了。
Android 4.1(API 级别 16)
Android 5.0(API 级别 21)
Android 7.0(API 级别 24)
Android 8.0(API 级别 26)
为了在不同的android版本中兼容通知,android在support-compat包中提供了NotificationCompat和NotificationManagerCompat来帮助我们更加方便的使用通知。
由于我们是在android10上使用通知,所以我们必须兼容所有的android版本,那么我们创建通知的步骤就如下:
由于我们只需要在android8.0以上创建,所以在代码中进行判断:
private String createNotificationChannel(String channelID, String channelNAME, int level) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
NotificationChannel channel = new NotificationChannel(channelID, channelNAME, level);
manager.createNotificationChannel(channel);
return channelID;
} else {
return null;
}
}
这里封装了一个方法,如果api大于android8.0就创建渠道返回渠道id,否则返回空。这里我只传了三个必要的参数,还有其他的设置在通知渠道那一节单独介绍。
每个通知都应该对点按操作做出响应,通常是在应用中打开对应于该通知的 Activity。为此,您必须指定通过 PendingIntent 对象定义的内容 Intent,并将其传递给 setContentIntent()。
Intent intent = new Intent(this, Main2Activity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0);
Intent intent = new Intent(this, Main2Activity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0);
String channelId = createNotificationChannel("my_channel_ID", "my_channel_NAME", NotificationManager.IMPORTANCE_HIGH);
NotificationCompat.Builder notification = new NotificationCompat.Builder(this, channelId)
.setContentTitle("通知")
.setContentText("收到一条消息")
.setContentIntent(pendingIntent)
.setSmallIcon(R.mipmap.ic_launcher)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true);
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
notificationManager.notify(100, notification.build());
这里通过NotificationCompat.Builder构造通知需要的信息,注意这里调用了setAutoCancel(true),它会在用户点按通知后自动移除通知,如果不调用或者传参数为false就会在点击后依然存在,但是如果用户清除下拉框的所有消息会把这样的通知清理掉。如果不想用户把他清理掉呢?可以调用setOngoing(true)方法,这样除非你的app死掉或者在代码中取消,否则他都不会消失。
一个通知最多可以提供三个操作按钮,让用户可以快速响应,例如暂停提醒,甚或回复短信。但这些操作按钮不应该重复用户在点按通知时执行的操作。
要添加操作按钮,请将 PendingIntent 传递给 addAction() 方法。这就像是设置通知的默认点按操作,不同的是不会启动 Activity,而是可以完成各种其他任务,例如启动在后台执行作业的 BroadcastReceiver,这样该操作就不会干扰已经打开的应用。
Intent snoozeIntent = new Intent(this, MyBroadcastReceiver.class);
snoozeIntent.setAction(ACTION_SNOOZE);
snoozeIntent.putExtra(EXTRA_NOTIFICATION_ID, 0);
PendingIntent snoozePendingIntent =
PendingIntent.getBroadcast(this, 0, snoozeIntent, 0);
NotificationCompat.Builder notification = new NotificationCompat.Builder(this, channelId)
.setContentTitle("通知")
.setContentText("收到一条消息")
.setContentIntent(pendingIntent)
.setSmallIcon(R.mipmap.ic_launcher)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.addAction(R.drawable.icon, "按钮", snoozeIntent )
.setAutoCancel(true);
通知可以包含动画形式的进度指示器,向用户显示正在进行的操作状态。
通过setProgress(max, progress, false)方法来设置和更新进度
NotificationCompat.Builder notification = new NotificationCompat.Builder(this, channelId)
.setContentTitle("通知")
.setContentText("收到一条消息")
.setContentIntent(pendingIntent)
.setSmallIcon(R.mipmap.ic_launcher)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.addAction(R.drawable.icon, "按钮", pendingIntent)
.setAutoCancel(true);
// Issue the initial notification with zero progress
int PROGRESS_MAX = 100;
int PROGRESS_CURRENT = 0;
notification.setProgress(PROGRESS_MAX, PROGRESS_CURRENT, false);
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
notificationManager.notify(100, notification.build());
上面对进度条进行了初始化,然后不断调用下面的方法进行更新:
//更新
notification.setContentText("Download complete")
.setProgress(0,0,false);
notificationManager.notify(100, notification.build());
上面的例子是一个确定性的进度条,这个怎么理解呢,我给你们看一个不确定性的进度条你们就知道了,将第三个参数设置为true,效果如下:
要控制锁定屏幕中通知的可见详情级别,请调用 setVisibility() 并指定以下值之一:
要在发出此通知后对其进行更新,请再次调用 NotificationManagerCompat.notify(),并将之前使用的具有同一 ID 的通知传递给该方法。如果之前的通知已被关闭,则系统会创建一个新通知。
您可以选择性调用 setOnlyAlertOnce(),这样通知只会在通知首次出现时打断用户(通过声音、振动或视觉提示),而之后更新则不会再打断用户。
除非发生以下情况之一,否则通知仍然可见:
通知的点击操作是由PendingIntent实现的,通过PendingIntent可以跳转Activity,开启服务,发送广播,我们着重来讲一下跳转Activity的操作,跳转分为两种类型,一是这个Activity单独在一个任务栈中,点击返回就直接返回到桌面;二是这个Activity拥有一个定义好的返回栈,点击返回会根据任务栈顺序进行回退。
由于从通知启动的“特殊 Activity”不需要返回堆栈,因此您可以通过调用 getActivity() 来创建 PendingIntent,但您还应确保在清单中定义了相应的任务选项。
在清单中,将以下属性添加到 元素中。
android:taskAffinity=""
与您将在代码中使用的 FLAG_ACTIVITY_NEW_TASK 标记结合使用,将此属性设置为空可以确保这类 Activity 不会进入应用的默认任务。具有应用默认相似性的任何现有任务都不会受到影响。
android:excludeFromRecents=“true”
用于从“最近”中排除新任务,以免用户意外返回它。
<activity
android:name=".ResultActivity"
android:launchMode="singleTask"
android:taskAffinity=""
android:excludeFromRecents="true">
</activity>
Intent intent = new Intent(this, Main2Activity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
定义应用的 Activity 层次结构
通过向应用清单文件中的每个 元素添加 android:parentActivityName 属性,定义 Activity 的自然层次结构。例如:
<activity
android:name=".MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".ResultActivity"
android:parentActivityName=".MainActivity" />
...
</activity>
构建包含返回堆栈的 PendingIntent
要启动包含 Activity 的返回堆栈的 Activity,您需要创建 TaskStackBuilder 的实例并调用 addNextIntentWithParentStack(),向其传递您要启动的 Activity 的 Intent。
只要您为每个 Activity 定义了父 Activity(如上文所述),就可以调用 getPendingIntent() 来接收包含整个返回堆栈的 PendingIntent。
Intent resultIntent = new Intent(this, ResultActivity.class);
TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
stackBuilder.addNextIntentWithParentStack(resultIntent);
PendingIntent resultPendingIntent =
stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
如有必要,您可以通过调用 TaskStackBuilder.editIntentAt() 向堆栈中的 Intent 对象添加参数。有时候需要这样做,以确保返回堆栈中的 Activity 在用户向上导航到它时显示有意义的数据。
基本通知通常包括标题、一行文本,以及用户可以执行的一项或多项响应操作。要提供更多信息,您还可以应用本页介绍的多个通知模板之一来创建大型展开式通知。
NotificationCompat.Builder notification = new NotificationCompat.Builder(this, channelId)
.setContentTitle("通知")
.setContentText("收到一条消息")
.setContentIntent(pendingIntent)
.setSmallIcon(R.mipmap.ic_launcher)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setStyle(new NotificationCompat.BigPictureStyle()
.bigPicture(BitmapFactory.decodeResource(getResources(), R.drawable.image)))
.setAutoCancel(true);
这里通过setStyle来使用系统提供的模版创建大图片通知,要使该图片仅在通知收起时显示为缩略图,请调用 setLargeIcon() 并传入该图片,同时调用 BigPictureStyle.bigLargeIcon() 并传入 null,这样大图标就会在通知展开时消失:
NotificationCompat.Builder notification = new NotificationCompat.Builder(this, channelId)
.setContentTitle("通知")
.setContentText("收到一条消息")
.setContentIntent(pendingIntent)
.setSmallIcon(R.mipmap.ic_launcher)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.image))
.setStyle(new NotificationCompat.BigPictureStyle()
.bigPicture(BitmapFactory.decodeResource(getResources(), R.drawable.image))
.bigLargeIcon(null))
.setAutoCancel(true);
应用 NotificationCompat.BigTextStyle,以在通知的展开内容区域显示文本:
NotificationCompat.Builder notification = new NotificationCompat.Builder(this, channelId)
.setContentTitle("通知")
.setContentText("收到一条消息")
.setContentIntent(pendingIntent)
.setSmallIcon(R.mipmap.ic_launcher)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setStyle(new NotificationCompat.BigTextStyle()
.bigText("无论是否意识到 Gradle 的存在,每位 Android 程序员都会直接或间接的与 Gradle 打交道。每当通过 Android Studio 新建一个工程时,AS 都会自动创建一个通用的目录结构,然后就可以进行开发,在 app 的 build.gradle 中添加一些依赖,点击右上角的 Sync Now"))
.setAutoCancel(true);
如果您想要添加多个简短的摘要行(例如收到的电子邮件的片段),可对通知应用 NotificationCompat.InboxStyle。这样,您就可以添加多条内容文本,并且每条文本均截断为一行,而不是显示为 NotificationCompat.BigTextStyle 提供的一行连续文本。
要添加新行,最多可调用 addLine() 6 次。如果添加的行超过 6 行,则仅显示前 6 行。
NotificationCompat.Builder notification = new NotificationCompat.Builder(this, channelId)
.setContentTitle("通知")
.setContentText("收到一条消息")
.setContentIntent(pendingIntent)
.setSmallIcon(R.mipmap.ic_launcher)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setStyle(new NotificationCompat.InboxStyle()
.addLine("无论是否意识到 Gradle 的存在,每位 Android 程序员都会直接或间接的与 Gradle 打交道。每当通过 Android Studio 新建一个工程时")
.addLine("AS 都会自动创建一个通用的目录结构,然后就可以进行开发,在 app 的 build.gradle 中添加一些依赖,点击右上角的 Sync Now")
.addLine("编写代码,点击绿色小箭头 Run 运行代码,一切都这么美好"))
.setAutoCancel(true);
从 Android 8.0(API 级别 26)开始,所有的通知都必须分配到相应的渠道。对于每个渠道,您可以设置应用于其中的所有通知的视觉和听觉行为(也就是通知的级别)。然后,用户可以更改这些设置,并确定您应用中的哪些通知渠道应具有干扰性或应该可见。我认为添加这个功能主要是为了让用户能够更透明的管理通知。(百分之九十九的人都不知道,知道也不会去看,唉!!!)
我们来看一下酷狗音乐的通知渠道情况(不同的手机厂商系统不同显示会有出入):
可以看到酷狗总共有5个通知渠道,我们看一下酷狗推送消息这个渠道:
渠道详情里有重要程度(也就是通知的级别)、提示音、震动等设置,这些都是我们可以在代码中设置的,当然用户也可以主动的修改他们。
前面已经讲了通知渠道的创建,下面来看一下创建的时候可以做哪些额外操作:
NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
NotificationChannel channel = new NotificationChannel(channelID, channelNAME, level);
//设置提示音
channel.setSound();
//开启指示灯
channel.enableLights();
//开启震动
channel.enableVibration();
//设置锁屏展示
channel.setLockscreenVisibility();
//设置渠道描述
channel.setDescription();
manager.createNotificationChannel(channel);
这里只列举了一部分,在Android 7.1(API 级别 25)以下这些都是直接在NotificationCompat.Builder中设置的,只是在 Android 8.0(API 级别 26)上把这些功能拆分到了渠道设置上。
在创建了渠道之后就无法更改这些设置了。 这里需要单独讲一下,无论我们调用多少次创建渠道的方法,对于同一个渠道ID只有第一次创建是有效的,因为创建的时候会去判断是否存在这个渠道,如果存在是不会重新创建的。其实这么说也不完全对,有兴趣的可以看一下我在文章最后做的一个分析。
虽然在创建渠道之后我们不能再去修改他,但是我们可以获取渠道设置,因为渠道设置对用户是透明的,用户可以去随意的设置他,所以我们可以在获取渠道设置之后引导用户设置我们想要的行为。比如我是微信的开发者,微信的通知被用户手动关掉了,我在检测到通知被关之后就会去告诉用户这个通知很重要不能关掉,然后引导用户跳到那个页面进行设置。主要的步骤如下:
1.读取通知渠道设置
2.打开通知渠道设置
Intent intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS);
intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName());
intent.putExtra(Settings.EXTRA_CHANNEL_ID, myNotificationChannel.getId());
startActivity(intent);
注意,该 intent 需要两个提取项,分别用于指定您应用的软件包名称(也称为应用 ID)和要修改的渠道。
其实还有一个渠道分组没讲,但是用的很少就不说了。
通知的级别主要体现在通知的展示方式上,有的通知只在状态栏现在一个图标,有的通知却会弹出来悬浮在屏幕上(例如微信),这就是通过设置通知的级别实现的。在 Android 7.1(API 级别 25)及更低版本上是通过priority这个属性设置,在 Android 8.0(API 级别 26)及更高版本上是通过importance属性设置。
用户可见的重要性级别 | 重要性(Android 8.0 及更高版本) | 优先级(Android 7.1 及更低版本) |
---|---|---|
紧急 发出提示音,并以浮动通知的形式显示 |
IMPORTANCE_HIGH | PRIORITY_HIGH 或 PRIORITY_MAX |
高 发出提示音 |
IMPORTANCE_DEFAULT | PRIORITY_DEFAULT |
中 不发出提示音 |
IMPORTANCE_LOW | PRIORITY_LOW |
低 不发出提示音,且不会在状态栏中显示 |
IMPORTANCE_MIN | PRIORITY_MIN |
1.Android 7.1(API 级别 25)及更低版本
通过setPriority(NotificationCompat.PRIORITY_LOW)方法可以直接设置。
2.Android 8.0(API 级别 26)及更高版本
在Android8.0中这一功能需要在通知渠道中设置
NotificationChannel channel = new NotificationChannel(channelID, channelNAME, NotificationManager.IMPORTANCE_LOW);
最后一个参数就是通知的级别,除了在创建渠道的时候设置,在渠道创建完成之后也可以设置
NotificationChannel channel = new NotificationChannel(channelID, channelNAME, NotificationManager.IMPORTANCE_LOW);
channel.setImportance(NotificationManager.IMPORTANCE_LOW);
兼容
对比上面两个设置方法,我们发现把这两个设置都加上,并且设置相同的等级就可以兼容所有的版本。
使用自定义通知布局时,请特别注意确保您的自定义布局适用于不同的设备屏幕方向和分辨率。虽然对于所有界面布局,此建议都适用,但它对通知布局而言尤为重要,因为抽屉式通知栏中的空间非常有限。自定义通知布局的可用高度取决于通知视图。通常情况下,收起后的视图布局的高度上限为 64 dp,展开后的视图布局的高度上限为 256 dp。
自定义通知有两种,一种是为内容区域创建自定义布局,另一种是创建完全自定义的通知布局。
如果您需要自定义内容区域的布局,可以将 NotificationCompat.DecoratedCustomViewStyle 应用到您的通知。借助此 API,您可以为通常由标题和文本内容占据的内容区域提供自定义布局,同时仍对通知图标、时间戳、子文本和操作按钮使用系统装饰。
自定义布局的使用方式如下:
String channelId = createNotificationChannel("my_channel_ID", "my_channel_NAME", NotificationManager.IMPORTANCE_MAX);
RemoteViews notificationLayout = new RemoteViews(getPackageName(), R.layout.custom_notification_item);
RemoteViews notificationLayoutExpanded = new RemoteViews(getPackageName(), R.layout.custom_notification_large);
NotificationCompat.Builder notification = new NotificationCompat.Builder(this, channelId)
.setSmallIcon(R.mipmap.ic_launcher)
.setStyle(new NotificationCompat.DecoratedCustomViewStyle())
.setCustomContentView(notificationLayout)
.setCustomBigContentView(notificationLayoutExpanded)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setAutoCancel(true);
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
notificationManager.notify(100, notification.build());
如果您不希望使用标准通知图标和标题装饰通知,请按照上述步骤使用 setCustomBigContentView(),但不要调用 setStyle()。
要支持低于 Android 4.1(API 级别 16)的 Android 版本,您还应调用 setContent(),向其传递同一 RemoteViews 对象。
在测试创建了渠道之后还能不能修改的时候我发现有些信息是能修改的,但是大部分是不能的,于是我去看了一下创建渠道的源码,结果发现前面说的渠道在创建之后就不能进行修改了其实不完全对,正确的是大部分主要的都不能修改了,有一些次要的信息还是能修改的,看一下创建渠道时发现渠道已经存在处理的代码:
NotificationChannel existing = r.channels.get(channel.getId());
// Keep most of the existing settings
if (existing != null && fromTargetApp) {
if (existing.isDeleted()) {
existing.setDeleted(false);
needsPolicyFileChange = true;
// log a resurrected channel as if it's new again
MetricsLogger.action(getChannelLog(channel, pkg).setType(
com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_OPEN));
}
//发现渠道名字不同会更新
if (!Objects.equals(channel.getName().toString(), existing.getName().toString())) {
existing.setName(channel.getName().toString());
needsPolicyFileChange = true;
}
//发现渠道描述不同会更新
if (!Objects.equals(channel.getDescription(), existing.getDescription())) {
existing.setDescription(channel.getDescription());
needsPolicyFileChange = true;
}
//我也不知道是什么
if (channel.isBlockableSystem() != existing.isBlockableSystem()) {
existing.setBlockableSystem(channel.isBlockableSystem());
needsPolicyFileChange = true;
}
//发现组不同会更新
if (channel.getGroup() != null && existing.getGroup() == null) {
existing.setGroup(channel.getGroup());
needsPolicyFileChange = true;
}
// Apps are allowed to downgrade channel importance if the user has not changed any
// fields on this channel yet.
//当用户没有手动修改过渠道的信息,并且要更新的通知等级小于现有的等级可以更新
final int previousExistingImportance = existing.getImportance();
if (existing.getUserLockedFields() == 0 &&
channel.getImportance() < existing.getImportance()) {
existing.setImportance(channel.getImportance());
needsPolicyFileChange = true;
}
// system apps and dnd access apps can bypass dnd if the user hasn't changed any
// fields on the channel yet
if (existing.getUserLockedFields() == 0 && hasDndAccess) {
boolean bypassDnd = channel.canBypassDnd();
if (bypassDnd != existing.canBypassDnd()) {
existing.setBypassDnd(bypassDnd);
needsPolicyFileChange = true;
if (bypassDnd != mAreChannelsBypassingDnd
|| previousExistingImportance != existing.getImportance()) {
updateChannelsBypassingDnd(mContext.getUserId());
}
}
}
updateConfig();
return needsPolicyFileChange;
}
通过代码发现setName、setDescription、setGroup是可以被修改的,setImportance也能修改但是必须当用户没有手动修改过渠道的信息,并且要修改的通知等级小于现有的等级可以修改。