Android通知学习和总结

2,390 阅读9分钟

本篇笔记主要是学习Android通知,主要包括通知渠道,通知分组,通知样式,创建通知,通知关联等部分。本篇笔记的学习资料主要是Android开发官网文档的通知部分,可以点击连接直接进入到网站学习,下面是本篇笔记的内容。

概述

在之前的笔记中,已经学习了通知栏相关的大部分的内容,这篇笔记主要是对之前的笔记做一个总结和梳理,以及对之前笔记中没有提到过的内容学习一下。

创建通知的流程

通过之前的学习,已经明白了创建通知栏的流程主要包括以下步骤:

  1. 首先创建通知渠道(NotificationChannel):

    虽然通知渠道是8.0版本及以上才需要加上的,但是现在Android版本已经更新到11了,因此对于通知渠道这里默认是需要创建的,创建通知渠道需要设置以下属性:

    • 渠道ID,渠道名称以及重要性是必须要设置的
    • 渠道说明最好加上,方便用户对当前渠道有一个细致的了解,从而避免用户因为不了解渠道信息错误地设置了相应的属性

    创建完通知渠道之后通过createNotificationChannel等方法注册通知渠道。

  2. 接着创建通知

    通知也就是要显示在通知栏的信息,创建通知需要设置以下信息:

    • 必须要设置smallIcon的信息
    • 设置title,content信息
    • 对于音视频通知,会话消息通知等特殊情况下还需要设置style属性来满足不同的需求
    • 为了兼容8.0以下的系统,还需要设置priority属性等,来保持和8.0拥有相同的重要性等。
  3. 显示通知

    首先确定一个通知Id,然后调用NotificationManagerCompatnotify方法将通知显示出来。

下面的代码演示了如何创建一个最基本的通知:

    //发送一个基本的通知栏消息
    private fun sendBasicNotification() {
        //首先创建通知渠道
        val channelId = "basicChannelId"
        if (SdkVersionUtils.checkAndroidO()) { //这个工具类只是判断当前的SDK版本是否大于等于8.0
            val channelName = "基本的通知栏消息渠道"
            val importance = NotificationManager.IMPORTANCE_HIGH
            val channel = NotificationChannel(channelId, channelName, importance)
            channel.description = "这个渠道下的消息都是为了演示基本通知栏消息用的"
            val channelManager = getSystemService(Context.NOTIFICATION_SERVICE)
                    as NotificationManager
            channelManager.createNotificationChannel(channel)
        }
        //接下来创建通知
        val builder = NotificationCompat.Builder(this, channelId)
        builder.setSmallIcon(R.drawable.frank) //smallIcon是必须的
        builder.setContentTitle("基本通知栏消息")
        builder.setContentText("这是一条基本的通知栏消息")
        val notificationInfo = builder.build()
        //最后发送通知
        val notificationId = 1000
        NotificationManagerCompat.from(this).notify(notificationId, notificationInfo)
    }

  1. 补充操作

    在执行完上面的操作之后,我们就已经可以在通知栏显示一条信息了,剩下的基本就对上面的步骤设置一些补充的操作,比如设置style以适应特殊情况下的样式,设置PendingIntent属性来设置跳转页面等。

补充渠道操作

上面已经对如果发送一条通知栏消息有了明确的认识,总共三步即可显示一条通知栏消息,而为了能够适应开发中不同的需求,大多数情况下都需要通过补充上面三步中的某一步。下面是通过补充/扩展渠道相关的信息。

设置属性

通过查看NotificationChannel部分的代码,可以发现渠道属性这里还有很多可以设置的地方,如下演示渠道部分设置。

  1. channel.enableLights(true)

    设置这个属性用于开启当这个渠道的通道发布时显示通知灯。(实际在我的一加手机上尝试并没有效果)

  2. lightColor

    设置这个属性用于当通知灯可用时的颜色(实际测试中发现也没有效果)

  3. enableVibration

    设置这个属性用于当这个渠道的通知发布时是否开启震动。(实际测试中没有效果,在通知渠道设置页面有一个选项可供用户选择是否开启震动)

注意: 渠道的相关设置事实上主要受到用户的影响,开发者并不能进行过多的控制,同时,就算开发者设置了部分属性,最终也会以用户设置的为准。

读取属性

读取属性的意义在于我们可以引导用户进行某项设置,比如上面的设置震动没有效果的时候,此时我们就可以引导用户跳转到设置页面,让用户勾选允许震动,这样当这个渠道的通知发布的时候就会震动了。

通过以下三步读取渠道属性:

  1. 获取NotificationManager,通过getSystemService(Context.NOTIFICATION_SERVICE)来获取
  2. 获取NotificationChannel,通过NotificationManager.getNotificationChannel(channelId)来获取
  3. 获取相应的属性值,通过shouldXXX或者getXXX相关方法来获取。

例如下面的代码演示了获取当前渠道发布通知时是否允许震动:

val channelManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val readChannel = channelManager.getNotificationChannel(channelId)
val vibrate = readChannel.shouldVibrate()
Logs.e("是否开启震动:$vibrate")

跳转到渠道信息设置页面

上面说了,有时候我们设置的渠道属性没有效果,或者有时候我们设置的属性被用户修改了,此时我们可以引导用户跳转到渠道信息设置页面,让用户进行某项操作的设置。

跳转渠道信息设置页面主要通过Intent完成,主要操作步骤如下:

val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
intent.putExtra(Settings.EXTRA_APP_PACKAGE, "应用包名")
intent.putExtra(Settings.EXTRA_CHANNEL_ID, "渠道ID")
startActivity(intent)

渠道分组

渠道分组的意义在于可以对某些渠道进行分组,便于用户管理。

但是通过之前的学习,我个人认为渠道分组的意义并不是很大,而且我个人认为对于渠道的分组应该让用户自己去分组,而不是让开发者分组好了让用户去操作。

要注意的是这里只是我认为意义不是很大。但是对于有多个账户需要管理的人来说这个渠道分组还是很有用的。

比如对于经常使用邮箱的人来说,很多人可能拥有多个邮箱账号(比如我自己有一个163邮箱的,一个微软outlook邮箱的,一个工作用的阿里云邮箱的),此时我通过一个软件,比如Foxmail来管理我这些邮箱账号。每个邮箱账号都会有收到邮件的通知渠道,发送邮件成功的通知渠道,以及其它操作的通知渠道。这个时候,我们发现,我这3个账号有9个通知渠道,此时让我去管理这些通知渠道就很麻烦,因为有时候很难分清哪个通知渠道是属于哪个账号下面的,这个时候渠道分组的优势就体现出来了,我们针对每个账号创建一个渠道分组,每个分组的名字就是这个邮箱账号,每个分组下面分别包含发送邮件,接收邮件,其它操作三个渠道。这样,不管是我想操作某个渠道,还是想操作某个账号下所有的渠道,最起码我不会不知道该去操作哪个渠道了!

同时,渠道分组提供了可以直接关闭或者开启某一个渠道分组下所有渠道的通知,这样,比如我这个163邮箱没什么用,经常收到一些垃圾邮件,那我就把这个渠道分组直接关闭掉,这样163邮箱的通知就不会有了,但是不影响其它两个渠道分组。

针对上面的案例,我们通过以下代码来实现:

//创建渠道分组
    private fun createChannelGroup() {
        if (SdkVersionUtils.checkAndroidO()) {
            //分组id和名称
            //163邮箱的
            val CHANNEL_GROUP_ID_163 = "channelGroupId163"
            val CHANNEL_GROUO_NAME_163 = "163邮箱"
            //outlook邮箱的
            val CHANNEL_GROUP_ID_OUTLOOK = "channelGroupIdOutlook"
            val CHANNEL_GROUO_NAME_OUTLOOK = "outlook邮箱"
            //阿里云邮箱的
            val CHANNEL_GROUP_ID_ALI = "channelGroupIdAli"
            val CHANNEL_GROUP_NAME_ALI = "阿里云邮箱"

            //创建渠道分组
            val channelGroup163 =
                NotificationChannelGroup(CHANNEL_GROUP_ID_163, CHANNEL_GROUO_NAME_163)
            val channelGroupOutlook =
                NotificationChannelGroup(CHANNEL_GROUP_ID_OUTLOOK, CHANNEL_GROUO_NAME_OUTLOOK)
            val channelGroupAli =
                NotificationChannelGroup(CHANNEL_GROUP_ID_ALI, CHANNEL_GROUP_NAME_ALI)
            //通知管理器
            val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
            //创建渠道分组
            manager.createNotificationChannelGroup(channelGroup163)
            manager.createNotificationChannelGroup(channelGroupOutlook)
            manager.createNotificationChannelGroup(channelGroupAli)

            //分别创建通知渠道,这里为了方便我直接循环创建
            val channels = mutableListOf<NotificationChannel>()
            for (i in 0 until 9) {
                val channelId = when (i) {
                    0, 1, 2 -> "channelId_163_$i"
                    3, 4, 5 -> "channelId_outlook_$i"
                    else -> "channelId_ali_$i"
                }
                val channelName = when (i) {
                    0, 1, 2 -> "channelName_163_$i"
                    3, 4, 5 -> "channelName_outlook_$i"
                    else -> "channelName_ali_$i"
                }
                val channel = NotificationChannel(
                    channelId,
                    channelName,
                    NotificationManager.IMPORTANCE_DEFAULT
                )
                channel.group = when (i) {
                    0, 1, 2 -> CHANNEL_GROUP_ID_163
                    3, 4, 5 -> CHANNEL_GROUP_ID_OUTLOOK
                    else -> CHANNEL_GROUP_ID_ALI
                }
                channels.add(channel)
            }
            manager.createNotificationChannels(channels)
        }
    }

这样,关于通知渠道的相关总结大概就是这些。总体来说,通知渠道的作用在于将具有相同行为的通知集合在一起,我们通过通知渠道来统一管理某一个通知渠道下所有通知的行为,比如响铃,震动等。但是需要明确的是,只有用户对通知的最终表现形式拥有决定权,开发者可以设置某些属性,但是并不一定会起作用。在这种情况下读取通知渠道信息就变得很重要,可以通过读取渠道信息引导用户重新设置某些通知的表现形式。

补充创建通知操作

在上面的发送一条普通的通知栏消息的代码中,我们首先是创建通知渠道,创建完通知渠道之后就开始创建通知,我们使用NotificationCompat.Builder来创建一条通知消息。

在上面我们只是简单地创建了一个通知栏消息,甚至连点击之后的操作都没有,其实对于通知消息最重要的就是这一块,下面的内容就是对这一块的内容进行填充,从而实现风格多样的通知栏消息。

创建一组通知

在之前了解了通知的渠道分组,和通知渠道,简单来说,渠道的意义在于将具有相同功能,相同行为的通知进行归类,让开发者和用户能够对某一个渠道的具体行为进行操作。渠道分组的意义则在于按照某一种方法对渠道进行分类总结,这样做的目的在于当渠道相对较多之后用户很难区分某一个渠道的功能,对渠道进行分组,有利于开发者和用户对渠道进行管理。

通知分组在Android 7.0就开始有了,比通知渠道出现的还要早。通知分组主要是为了实现对通知UI的管理。上面说了通知渠道是对通知功能和行为进行分类,这是二者的差别。仍然使用上面提到的邮箱举例:在一个邮箱账号下可能存在多个联系人,同一个时间段,同一个联系人可能会给我们发送多条邮件,在没有通知分组的情况下,每一条通知都以独立的形式显示在通知栏中,通知多了之后很难进行管理。而有了通知分组之后,我们就可以将同一个联系人的邮件归类到同一个分组之下,这样用户就可以在通知栏中对某一个联系人的通知进行管理(可以打开或者合并)。

下面的代码演示了创建通知分组的过程:

  1. 创建一个简单的通知并加入到分组中
        val builder = NotificationCompat.Builder(this,CHANNEL_ID_VIDEO)
        builder.setSmallIcon(R.drawable.frank)
        builder.setContentTitle("通知分组测试")
        builder.setContentText("这是通知分组测试")
        builder.setLargeIcon(BitmapFactory.decodeResource(resources,R.drawable.frank))
        builder.setGroup(NOTIFICATION_GROUP_KEY_VIDEO)

可以看到,上面就是添加了一条普通的通知消息,唯一有区别的就是在最后加入了setGroup()属性,显示这条通知消息也和普通的通知消息没有什么不同。

  1. 设置通知组摘要

通知组摘要说明了当前通知组下面的通知的一些简略信息,在Android7.0以及更高的版本上,系统会使用每条通知中的文本摘要,自动创建通知组的文本摘要,用户可以展开此通知来查看每条单独的通知。而对于低于Android7.0版本的系统,为了能够表现一致,则需要另外创建一条通知来充当通知组摘要。在这种情况下,系统会隐藏这个通知组下的所有通知,而仅仅显示这个通知组的信息。在这样的情况下,这个通知组的摘要就需要包含其它通知的片段,供用户点击来打开应用。

    val NOTIFICATION_GROUP_KEY_VIDEO = "notificationGroupIdVideo"//通知分组的唯一标识
    val SUMMARY_GROUP_ID = 0 //通知组摘要ID 也就是显示这个通知组的时候的ID
    private fun createNotificationGroup() {
        //第一条消息
        val newMessage1 = NotificationCompat.Builder(this, "channelId_163_0").apply {
            setSmallIcon(R.drawable.frank)
            setContentTitle("第一条消息")
            setContentText("这里是第一条消息")
            setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.frank))
            setGroup(NOTIFICATION_GROUP_KEY_VIDEO)
        }.build()
        //第二条消息
        val newMessage2 = NotificationCompat.Builder(this, "channelId_163_0").apply {
            setSmallIcon(R.drawable.frank)
            setContentTitle("第二条新消息")
            setContentText("这里第二条新消息")
            setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.frank))
            setGroup(NOTIFICATION_GROUP_KEY_VIDEO)

        }.build()
        //分组摘要
        val summaryNotification = NotificationCompat.Builder(this, "channelId_163_0").apply {
            setContentTitle("通知分组测试")
            setContentText("两条新消息")
            setSmallIcon(R.drawable.frank)
            setStyle(
                NotificationCompat.InboxStyle()
                    .setBigContentTitle("两条新消息")
                    .setSummaryText("摘要内容")
            )
                .setGroup(NOTIFICATION_GROUP_KEY_VIDEO)
                .setGroupSummary(true)
        }.build()
        NotificationManagerCompat.from(this).apply {
            notify(100, newMessage1)
            notify(101, newMessage2)
            notify(SUMMARY_GROUP_ID, summaryNotification)
        }

    }

运行上面的程序,最终会在通知栏显示这两条消息,但是这两条消息都是处于折叠状态,点击摘要区域或者下滑操作可以打开通知的详细信息。另外需要注意的是,通知分组的显示效果和当前通知所在渠道的重要性是有关系的,如果将当前渠道的重要性设置为高,则最新的消息始终是打开状态,其余的消息会折叠在摘要通知下面(在我的一加手机上是如此)。

从通知启动Activity

普通页面跳转

之前我们已经了解了如何显示一个普通的通知栏消息,但是我们并没有设置点击通知栏消息的时候执行的操作。一般情况下我们都会在点击通知栏后执行一些操作,比如打开Activity,或者发送一个广播之类的。下面的代码演示了如何在点击通知栏之后跳转到一个Activity页面:

    //测试跳转一个普通的Activity
    private fun toActivity() {
        //创建渠道
        createToActivityChannel()
        //构建需要跳转到的Activity
        val intent = Intent(this, NotificationChannelTestActivity::class.java)
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
        val pendingIntent =
            PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
        //创建通知消息
        val notification = NotificationCompat.Builder(this, mToActivityChannel).apply {
            setSmallIcon(R.drawable.frank)
            setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.frank))
            setContentTitle("跳转Activity测试")
            setContentText("用于测试是否能够跳转到Activity")
            setAutoCancel(true)
            setContentIntent(pendingIntent)
            priority = NotificationCompat.PRIORITY_DEFAULT
        }.build()
        //发送通知
        NotificationManagerCompat.from(this).notify(100, notification)
    }

使用上面的代码就可以在通知栏显示一条通知消息,同时,点击这条消息则可以跳转到我们设置的NotificationChannelTestActivity这个页面。

对于一般的页面跳转使用上面的方式指定是没有问题的,但是除了上面的这种方式,Android还为我们提供了另外的两种方式来应对不同的情况。

常规Activity

这类的Activity是指我们在正常使用APP的时候就会使用到的页面,因此,当用户从通知跳转到这类Activity的时候,新的任务应该包含完整的返回堆栈,以便用户可以按返回键返回到之前的页面。

比如我们经常使用的通信APP,有时候当我们收到消息推送的时候很可能APP进程已经被杀死了,那么此时在点击通知栏的时候,虽然我们仍然能够跳转到正常的页面,但是点击返回键的时候此时就没有正常的任务栈了,无法返回到上个页面,而是直接返回到桌面了。这个时候,我们就可以通过TaskStackBuilder设置PendingIntent,这样就可以创建新的返回堆栈,以便用户可以执行正常的返回流程。

通过TaskStackBuilder创建返回堆栈的流程如下:

  1. 首先我这里创建了两个Activity,分别是TestTaskStackBuilderActivity1TestTaskStackBuilderActivity2,正常的流程是SummaryActivity -> TestTaskStackBuilderActivity1 -> TestTaskStackBuilderActivity2,而点击通知栏消息则会直接进入到TestTaskStackBuilderActivity2这个页面,为了能够在点击通知栏消息的时候也能够有正常的返回流程,所以首先需要在Manifest文件中注册每一个ActivityparentActivityName属性:
        <activity
                android:name=".notification.summary.TestTaskStackBuilderActivity2"
                android:parentActivityName=".notification.summary.TestTaskStackBuilderActivity1"></activity>
        <activity
                android:name=".notification.summary.TestTaskStackBuilderActivity1"
                android:parentActivityName=".notification.summary.SummaryActivity" />
  1. 接下来可以通过TaskStackBuilder来创建PendingIntent:
    //使用TaskStackBuilder构建PendingIntent
    private fun toActivityWithTaskStackBuilder() {
        //渠道信息
        val channelId = "testTaskStackBuilder"
        val channelName = "测试返回任务栈"
        val channelDescription = "用于测试返回任务栈的通知栏消息渠道"
        //创建渠道
        if (SdkVersionUtils.checkAndroidO()) {
            val channel =
                NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_DEFAULT)
            channel.description = channelDescription
            val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
            manager.createNotificationChannel(channel)
        }

        //通知信息
        val notificationId = 100
        //构建要跳转的页面
        val intent = Intent(this, TestTaskStackBuilderActivity2::class.java)
        val taskBuilder = TaskStackBuilder.create(this).apply {
            this.addParentStack(TestTaskStackBuilderActivity2::class.java)
            this.addNextIntent(intent)
        }
        val pendingIntent = taskBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
        //构建通知
        val builder = NotificationCompat.Builder(this, channelId).apply {
            setSmallIcon(R.drawable.frank)
            setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.frank))
            setContentTitle("测试返回栈")
            setContentText("测试任务返回栈的消息")
            setContentIntent(pendingIntent)
            priority = NotificationCompat.PRIORITY_DEFAULT
            setAutoCancel(true)
        }
        Handler().postDelayed({
            //发送通知
            NotificationManagerCompat.from(this).notify(notificationId, builder.build())
        }, 5000)
    }

通过上面的代码构建的PendingIntent,在APP处于后台的时候也能够准确的跳转到对应的页面,返回时也能够按照正常的返回逻辑进行跳转。但是有一个问题在于当APP处于前台的时候,这个时候点击通知栏消息,此时跳转到指定的页面,退出的时候这个页面之前的页面也会被重建。比如此时我们已经打开并处于SummaryActivity这个页面了,但是通过点击通知栏消息跳转到TestTaskStackBuilderActivity2页面之后,点击返回键返回到SummaryActivity的时候,这个页面又被重建了。

普通情况下这样做不会有什么问题,但是如果类似于SummaryActivity这样的页面操作逻辑很多,那么就会有问题了。比如在MainActivity我们可能会请求后台是否有新版本更新,有的话就弹出一个Dialog,本来应该是每次打开APP获取一次,但是使用这样的通知栏消息的时候,返回到MainActivity的时候就会导致MainActivity页面被重建,又一次弹出这个Dialog,就会造成不好的体验。因此还是要根据不同的情况选择不同的方式。

特殊情况下的PendingIntent

除了上面的需要返回栈的情况外,有时候我们也会遇到某个页面只有在点击通知栏之后才会跳转过去,正常使用APP是不会跳转到这样的页面的这样的情况。对于这样的Activity,我们可以直接使用PeindingIntent.getActivity来构建,同时配合在Manifest文件中设置的属性,就可以达到相应的效果,如下代码演示了如何创建这样的PendingIntent:

  1. 首先创建一个Activity,这个Activity只有在点击通知栏的情况下才会显示出来。为了达到这样的效果,我们需要在Manifest文件中设置这个Activity的launchModesingleTask,同时设置taskAffinity为空,用来保证普通情况下不会进入到这个页面。同时设置excludeFromRecents属性为true,用来保证这个页面不会进入到最近任务列表中。(这里并不能保证这个页面在任何情况下都不会进入到最近任务列表,如果当前正处于这个页面,那么按下最近任务按钮,仍然会看这个页面)
        <activity android:name=".notification.summary.SpecialActivity"
                android:launchMode="singleTask"
                android:taskAffinity=""
                android:excludeFromRecents="true"
                ></activity>
  1. 构建并发出通知:
    //跳转到特殊的Activity
    private fun toSpecialActivity() {
        //渠道信息
        val channelId = "testSpecialActivity"
        val channelName = "测试跳转到特殊的Activity"
        val channelDescription = "用于测试跳转到特殊Activity的通知栏消息渠道"
        //创建渠道
        if (SdkVersionUtils.checkAndroidO()) {
            val channel =
                NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_DEFAULT)
            channel.description = channelDescription
            val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
            manager.createNotificationChannel(channel)
        }

        //通知信息
        val notificationId = 100
        //构建要跳转的页面
        val intent = Intent(this, SpecialActivity::class.java)
        intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
        val pendingIntent =
            PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
        //构建通知
        val builder = NotificationCompat.Builder(this, channelId).apply {
            setSmallIcon(R.drawable.frank)
            setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.frank))
            setContentTitle("测试跳转特殊Activity")
            setContentText("只有通过点击通知栏消息才能跳转到的Activity")
            setContentIntent(pendingIntent)
            priority = NotificationCompat.PRIORITY_DEFAULT
            setAutoCancel(true)
        }
        //发送通知
        NotificationManagerCompat.from(this).notify(notificationId, builder.build())

    }

创建展开式通知

在上面我们已经学习了关于通知的一些知识,通过上面这些知识其实我们已经基本能够满足日常的需求,但是在上面的基础上,Android也为我们提供了更多的通知栏样式,从而让我们的通知能够更加个性化。展开式通知就是为了达到这样的效果,我们可以在设置通知的时候设置不同的Style,来达到自己的需求。

添加大图片

如果需要在通知中显示图片,则可以使用BigPictureStyle

    //显示大图片的通知
    private fun createBigPictureNotification() {
        val channelUtils = ChannelUtils.getInstance(this);
        //创建通道
        channelUtils.createDefaultChannel()
        //创建通知
        val builder = NotificationCompat.Builder(this, channelUtils.defaultChannelId)
        builder.setSmallIcon(R.drawable.frank)
        builder.setContentTitle("大图通知")
        builder.setContentText("大图通知")
        builder.setStyle(
            NotificationCompat.BigPictureStyle()
                .bigLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.frank))
                .bigPicture(BitmapFactory.decodeResource(resources, R.drawable.frank))
                .setSummaryText("二级标题")
        )
        builder.priority = NotificationCompat.PRIORITY_DEFAULT
        builder.setAutoCancel(true)
        //显示通知
        NotificationManagerCompat.from(this).notify(100, builder.build())
    }

上面的代码中,通过指定创建通知时的Style属性就可以创建一个大图通知,在通知栏打开的时候,通知信息就是我们指定的这个图片。

添加大段文本

如果想要在通知中显示大段的文本信息,则可以通过指定Style属性为BigTextStyle属性来设置:

    private fun createBigTextNotification() {
        //创建一个渠道
        createTestStyleChannel()
        //通知相关的信息
        val title = "大段文本通知"
        val content = "测试大段文本的通知"
        //创建大段文本通知的style
        val style = NotificationCompat.BigTextStyle().run {
            this.bigText("这里是大段通知文本这里是大段通知文本这里是大段通知文本这里是大段通知文本这里是大段通知文本")
            this.setSummaryText("用于测试大段文本通知的style")
        }

        val notificationUtils = NotificationUtils.getInstance(this)
        //创建通知实体
        val builder = notificationUtils
            .createNotification(
                channelId,
                R.drawable.frank,
                title,
                content,
                style,
                null,
                testStyleNotificationGroupKey
            )
        //发送通知
        notificationUtils.sendNotification(builder.build())
        //创建并发送通知分组
        createTestStyleNotificationGroup()
    }

通过上面的代码就可以成功地创建出一个包含大段文本的通知,默认情况下会显示content设置的内容,当通知展开后就会显示设置的bigText的内容。上面主要的代码是创建出style,使用到的createTestStyleChannel()createTestStyleNotificationGroup()则是两个工具类,会在本篇笔记学习完Style之后贴出来。

创建收件箱样式的通知

如果在一条通知中想要显示多个通知内容,比如收到多条邮件,每条信息展示一条通知简介,我们则可以使用InboxStyle这样的样式来设置,这里需要注意的是InboxStyle和通知分组的区别:通知分组下的每一条通知都有自己单独的行为属性,比如每一条单独的通知都可以设置样式,设置跳转页面,但是InboxStyle只是某一条通知的样式不同,本质上只是一条通知。通知分组理论上也可以实现InboxStyle的效果,但是对于刚才的例子,我们只想展示邮件简介而没有其它特殊内容,此时使用通知分组则有些杀鸡用牛刀的感觉。

使用下面的代码可以创建InboxStyle类型的通知:

    //创建收件箱样式的通知
    private fun createInboxStyleNotification() {
        //创建渠道
        createTestStyleChannel()
        //通知内容
        val title = "邮件"
        val content = "收到新邮件"
        val style = NotificationCompat.InboxStyle().run {
            this.addLine("第一条简介")
            this.addLine("第二条简介")
            this.addLine("第三条简介")
        }
        //创建消息
        val utils = NotificationUtils.getInstance(this)
        val builder = utils.createNotification(
            testStyleChannelId,
            R.drawable.frank,
            title,
            content,
            style,
            null,
            testStyleNotificationGroupKey
        )
        //发送消息
        utils.sendNotification(builder.build())
        //创建并发送通知分组
        createTestStyleNotificationGroup()
    }

使用上面的代码则可以在通知栏中显示收件箱样式的通知消息,默认情况下显示的是contentText配置的内容,点击展开之后显示的就是我们添加的这三条简介内容。

在通知中显示对话

使用MessagingStyle可以显示任意人数之间依序发送的消息,对于即时通讯应用来说这种方式是非常友好的,如下代码可以在通知栏创建对话通知:

    //在通知中显示对话
    private fun createMessagingStyleNotification() {
        //创建渠道
        createTestStyleChannel()
        //通知消息实体
        val title = "联系人"
        val content = "联系人发来新消息"
        //创建发送消息用户的信息
        val user = Person.Builder().run {
            this.setIcon(IconCompat.createWithResource(this@SummaryActivity, R.drawable.ic_icon1))
            this.setName("Bob")
        }
        //创建style
        val style = NotificationCompat.MessagingStyle(user.build()).run {
            //添加消息
            this.addMessage("Hello World",System.currentTimeMillis() - 1000 * 60 * 60 * 24,user.build())
            this.addMessage("Hello World",System.currentTimeMillis() - 1000 * 60 * 60 * 24,user.build())
            this.addMessage(NotificationCompat.MessagingStyle.Message("Thank you",System.currentTimeMillis(),user.build()))
        }
        //创建消息
        val utils = NotificationUtils.getInstance(this)
        val builder = utils.createNotification(testStyleChannelId,R.drawable.frank,title,content,style,null,testStyleNotificationGroupKey)
        //发送消息
        utils.sendNotification(builder.build())
        //创建并发送通知分组消息
        createTestStyleNotificationGroup()
    }

使用上面的代码会在通知栏显示对话消息,需要注意的是,显示对话消息的时候,我们设置的contentTitlecontentText内容将不会显示,同时在通知栏默认显示两条消息,如果消息多余两条则会折叠起来,点击展开之后则会显示全部对话消息。

使用媒体控件创建通知

当我们的应用在播放视频或者音乐的时候,我们可能会需要在通知栏显示当前播放的曲目信息,此时就需要使用媒体控件来创建通知消息。

在使用媒体控件创建通知栏消息的时候,可以通过调用addAction来添加按钮,最多能够添加5个,用于执行不同的操作。同时可以调用setLargeIcon()来设置专辑封面。同时,我们还可以通过setShowActionsInCompactView()来设置通知收起之后仍然要显示的按钮。

同时,如果通知表示媒体会话,则还可以调用setMediaSession()在通知上附加MediaSession.Token,这样,系统就会将其识别为表示媒体会话的通知并相应的做出响应(例如在锁屏中显示专辑封面)

通过如下代码创建媒体控件通知:

    //创建媒体控件通知
    private fun createMediaStyleNotification() {
        //创建渠道
        createTestStyleChannel()
        //创建消息实体
        //五个按钮对应的操作
        val operateAction = TestMediaStyleBroadcastReceiver::class.java.name
        //上一首
        val preIntent = Intent(operateAction)
        //preIntent.action = operateAction
        preIntent.putExtra(operateKey, "上一首")
        val prePendingIntent =
            PendingIntent.getBroadcast(this, 0, preIntent, PendingIntent.FLAG_UPDATE_CURRENT)
        //下一首
        val nextIntent = Intent()
        nextIntent.action = operateAction
        nextIntent.putExtra(operateKey, "下一首")
        val nextPendingIntent =
            PendingIntent.getBroadcast(this, 1, nextIntent, PendingIntent.FLAG_UPDATE_CURRENT)
        //播放或者暂停
        val stateIntent = Intent()
        stateIntent.action = operateAction
        stateIntent.putExtra(operateKey, "播放/暂停")
        val statePendingIntent =
            PendingIntent.getBroadcast(this, 2, stateIntent, PendingIntent.FLAG_UPDATE_CURRENT)
        //收藏
        val collectionIntent = Intent()
        collectionIntent.action = operateAction
        collectionIntent.putExtra(operateKey, "收藏")
        val collectionPendingIntent =
            PendingIntent.getBroadcast(this, 3, collectionIntent, PendingIntent.FLAG_UPDATE_CURRENT)
        //喜欢
        val likeIntent = Intent()
        likeIntent.action = operateAction
        likeIntent.putExtra(operateKey, "喜欢")
        val likePendingIntent =
            PendingIntent.getBroadcast(this, 4, likeIntent, PendingIntent.FLAG_UPDATE_CURRENT)
        //创建消息实体
        val utils = NotificationUtils.getInstance(this)
        val style = androidx.media.app.NotificationCompat.MediaStyle().run {
            this.setShowActionsInCompactView(0, 1, 3)
        }
        val builder = utils.createNotification(
            testStyleChannelId,
            R.drawable.frank,
            R.drawable.frank,
            "My Music",
            "我的音乐",
            null,
            style,
            true,
            NotificationCompat.PRIORITY_DEFAULT,
            null
        )
        builder.addAction(R.mipmap.ic_back_black, "pre", prePendingIntent)
        builder.addAction(R.mipmap.ic_back_white, "state", statePendingIntent)
        builder.addAction(R.mipmap.ic_launcher, "next", nextPendingIntent)
        builder.addAction(R.mipmap.ic_search_white, "collection", collectionPendingIntent)
        builder.addAction(R.mipmap.ic_bg_layout_title, "like", likePendingIntent)
        builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
        builder.color = Color.parseColor("#336699")
        //发送消息
        utils.sendNotification(1000, builder.build())
    }

使用上面的代码,我们就可以创建出一个媒体通知样式的通知栏,包含5个操作按钮。

需要注意的是,如果项目使用的是androidx,那么在使用MediaStyle的时候可能出现找不到这个类的问题,此时需要导入androidxmedia库,包括如下部分:

 "androidx.media2:media2-session:1.0.3",
 "androidx.media2:media2-widget:1.0.3",
 "androidx.media2:media2-player:1.0.3"

版本号可以在官方文档中查询最新版。

其它

上面创建通知渠道和通知的工具类代码如下:

    public void createChannel(
            String channelId,
            String channelName,
            int importance,
            String description,
            String groupId
    ) {
        if (moreThan8()) {
            NotificationChannel channel = new NotificationChannel(channelId, channelName, importance);
            channel.setDescription(description);
            if (!TextUtils.isEmpty(groupId))
                channel.setGroup(groupId);
            NotificationManager manager =
                    (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
            manager.createNotificationChannel(channel);
        }
    }
    public NotificationCompat.Builder createNotification(
            String channelId,
            int smallImageRes,
            Integer largeImageRes,
            String title,
            String content,
            PendingIntent pendingIntent,
            NotificationCompat.Style style,
            boolean cancel,
            int priority,
            String groupKey
    ) {
        NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId);
        //小图标
        builder.setSmallIcon(smallImageRes);
        //大图标,可以不设置
        if (largeImageRes != null)
            builder.setLargeIcon(BitmapFactory.decodeResource(context.getResources(), largeImageRes));
        //标题
        builder.setContentTitle(title);
        //内容
        builder.setContentText(content);
        //点击之后执行的操作,跳转页面或者发送通知等
        if (pendingIntent != null)
            builder.setContentIntent(pendingIntent);

        //设置样式
        if (style != null)
            builder.setStyle(style);

        //是否点击后可以取消
        builder.setAutoCancel(cancel);
        //重要性,这里需要和渠道中的重要性保持一致
        builder.setPriority(priority);
        //不允许添加默认操作按钮
        builder.setAllowSystemGeneratedContextualActions(false);
        if (!TextUtils.isEmpty(groupKey))
            builder.setGroup(groupKey);
        return builder;
    }

创建基本通知

在上面我们已经将通知这里学习的差不多了,对于不同的需求上面的学习过程基本都涉及到了,但是还有一些比较特殊的没有涉及到,如下是一些比较特殊的设置。

添加操作按钮

其实在上面的MediaStyle的时候我们已经添加了5个操作按钮,但是对于普通的通知,最多只能添加3个操作按钮,添加的方式和上面的操作是一样的,都是通过给Intent设置action来启动一个BroadcastReceiver或者Activity:

    //添加操作按钮
    private fun createHaveActionsNotification() {
        //创建渠道
        createTestStyleChannel()
        //创建通知实体
        val title = "操作按钮"
        val content = "包含操作按钮的通知"
        //两个操作按钮
        val intent1 = Intent(TestMediaStyleBroadcastReceiver::class.java.name)
        val action1 =
            PendingIntent.getBroadcast(this, 10, intent1, PendingIntent.FLAG_UPDATE_CURRENT)
        val intent2 = Intent(this, MessagingStyleActivity::class.java)
        val action2 =
            PendingIntent.getActivity(this, 11, intent2, PendingIntent.FLAG_UPDATE_CURRENT)
        //创建通知工具类
        val utils = NotificationUtils.getInstance(this)
        val builder = utils
            .createNotification(testStyleChannelId, R.drawable.frank, title, content)
        //将操作按钮添加到通知上
        builder.addAction(R.drawable.ic_icon1, "已读", action1)
        builder.addAction(R.drawable.ic_icon4, "查看详情", action2)
        //显示通知
        utils.sendNotification(builder.build())
    }

在上面的代码中,创建了两个操作按钮,一个点击之后会发出一个广播,一个点击之后会跳转到指定的Activity页面。

添加直接回复操作

Android7.0及以上的版本中引入了直接回复操作,用户可以在通知栏中直接添加要回复的内容而不必打开Activity,创建直接回复的流程如下:

    //添加直接回复操作的通知
    private val REPLY_TEXT_KEY = "replyTextKey"
    //回复的对象
    private val REPLY_USER_ID = "replyUserId"
    //通知id
    private val REPLAY_NOTIFICATION_ID = 5000
    private fun createHaveReplyNotification() {
        //创建通知渠道
        createTestStyleChannel()
        //通知实体
        val title = "直接回复操作的通知"
        val content = "点击下面的回复按钮可以直接回复信息"
        val utils = NotificationUtils.getInstance(this)
        val builder = utils.createNotification(testStyleChannelId, R.drawable.frank, title, content)
        //用于直接回复的`RemoteInput.Builder`
        val remoteInput = RemoteInput.Builder(REPLY_TEXT_KEY).run {
            setLabel("请输入要回复的内容")
            build()
        }
        //为回复创建PendingIntent
        val replyRequestCode = 200
        val intent = Intent(TestMediaStyleBroadcastReceiver::class.java.name)
        intent.putExtra(REPLY_USER_ID, replyRequestCode)
        val replyPendingIntent =
            PendingIntent.getBroadcast(
                this,
                replyRequestCode,
                intent,
                PendingIntent.FLAG_UPDATE_CURRENT
            )
        //创建用于回复的action 并绑定remoteInput
        val replyAction =
            NotificationCompat.Action.Builder(R.drawable.ic_icon6, "立即回复", replyPendingIntent)
                .addRemoteInput(remoteInput)
                .build()
        //将action绑定到通知上
        builder.addAction(replyAction)
        //发送通知
        utils.sendNotification(REPLAY_NOTIFICATION_ID,builder.build())
    }

通过上面的代码就可以在状态栏创建一个能够直接回复消息的通知,在通知中会有一个立即回复的按钮,点击这个按钮就可以调出输入法,然后可以输入要回复的内容了。

很多时候我们不仅需要让用户能够直接回复,我们还需要获取到用户回复的内容,通过以下方式可以获取到用户回复的内容:

在上面的代码中,我们对回复按钮绑定的是一个BroadcastReceiver,那么在这个receiver中就可以获取到用户输入的内容:

    //获取用户回复的内容
    val replayIntent = RemoteInput.getResultsFromIntent(intent)
    replayIntent?.let {
        Log.e("TAG","用户回复的内容:${it.getCharSequence(REPLY_TEXT_KEY)}")
    }

在成功获取到用户输入的内容并执行完操作之后,我们可能倾向于继续对这条通知做一些操作,或者隐藏这条通知,通过如下方式修改通知:

    //收到用户回复的内容后更新通知
    createTestStyleChannel()
    val utils = NotificationUtils.getInstance(context)
    val builder = utils.createNotification(
        testStyleChannelId,
        R.drawable.frank,
        "直接回复操作的通知",
        "回复成功"
    )
    utils.sendNotification(REPLY_NOTIFICATION_ID, builder.build())
    //等待3秒钟删除掉这条通知
    Handler().postDelayed({
        utils.deleteNotification(REPLY_NOTIFICATION_ID)
    },3000)

在上面的代码中我们在收到回复内容并处理之后更新了这条通知,然后在等待3秒之后关闭了这条通知。

添加进度条

有时候我们需要提醒用户一些进度信息,比如当我们在执行下载等任务的时候,此时就可以在通知栏中添加进度条。在通知栏中的进度条分为两种,一种是能够确定当前进度的,另外一种是不能确定当前进度,也就是不确定结束时间的进度条,多于这两种进度条差别并不大,如下分别创建一个确定结束时间的进度条和一个不确定结束时间的进度条:

确定结束时间的进度条
    //创建一个确定结束时间的进度条
    private val mNotificationUtils by lazy {
        NotificationUtils.getInstance(this)
    }
    private val mWithFinishProgressBuilder by lazy {
        //创建渠道
        createTestStyleChannel()
        mNotificationUtils.createNotification(
            testStyleChannelId,
            R.drawable.frank,
            "进度条通知",
            "包含进度条的通知"
        ).run {
            setOnlyAlertOnce(true)
        }
    }

    private var mProgress = 0;
    private val FINISH_PROGRESS_NOTIFICATION_ID = 4000
    private val mFinishNotificationHandler by lazy {
        Handler()
    }

    private fun createWithFinishNotification() {
        mWithFinishProgressBuilder.setProgress(100, mProgress, false)
        mNotificationUtils.sendNotification(
            FINISH_PROGRESS_NOTIFICATION_ID,
            mWithFinishProgressBuilder.build()
        )
        if (mProgress >= 100) {
            mFinishNotificationHandler.removeCallbacksAndMessages(null)
            mNotificationUtils.deleteNotification(FINISH_PROGRESS_NOTIFICATION_ID)
        } else {
            mProgress += 10
            mFinishNotificationHandler.postDelayed({
                createWithFinishNotification()
            }, 1000)
        }
    }

通过上面的代码我们就可以创建出一个确定结束时间的进度条,上面使用Handler模拟进度条每次增加10。另外,由于这条通知渠道的重要新为IMPORTANCE_DEFAULT,所以设置setOnlyAlertOnce(true)可以保证只会响铃一次,也可以设置IMPORTANCE_LOW关闭响铃。

不确定结束时间的进度条

不确定结束时间的进度条和上面创建的确定结束时间的进度条差别不大,只是在NotificationCompat.Builder.setProgress(0,0,true)传递这样的参数就可以了,如下所示:

    //创建不确定结束时间的进度条通知
    private val INDETERMINATE_NOTIFICATION_ID = 40001
    private val mIndeterminateBuilder by lazy {
        createTestStyleChannel()
        mNotificationUtils.createNotification(
            testStyleChannelId,
            R.drawable.frank,
            "进度条通知",
            "不确定结束时间的进度条"
        ).run {
            setOnlyAlertOnce(true)
        }
    }

    private var mTime = 0;
    private fun createIndeterminateNotification() {
        mIndeterminateBuilder.setProgress(0, 0, true)
        mNotificationUtils.sendNotification(
            INDETERMINATE_NOTIFICATION_ID,
            mIndeterminateBuilder.build()
        )
        if (mTime >= 10 * 1000) {
            mFinishNotificationHandler.removeCallbacksAndMessages(null)
            mNotificationUtils.deleteNotification(INDETERMINATE_NOTIFICATION_ID)
        } else {
            mFinishNotificationHandler.postDelayed(
                {
                    mTime += 1000
                    createIndeterminateNotification()
                }, 1000
            )
        }

    }

设置锁定屏幕公开范围

通过设置setVisibility()属性来控制通知在锁定屏幕上的显示级别,这个属性取值范围如下:

  • VISIBILITY_PUBLIC:显示完整的通知内容
  • VISIBILITY_SECRET: 不在锁定屏幕上显示任何内容
  • VISIBILITY_PRIVATE: 只显示基本信息,例如通知图标和内容标题,但是隐藏通知的完整内容。

当设置可见性为VISIBILITY_PRIVATE的时候,我们可以提供通知的备用版本,以隐藏特定的详情信息,通过设置setPublicVersion(Notification)来将备用通知附加到普通通知之中:

    //设置锁定屏幕公开范围为private并设置通知的备用版本
    private fun createPrivateVisibility() {
        //创建渠道
        createTestStyleChannel()
        //创建备用通知
        val backupBuilder = mNotificationUtils.createNotification(
            testStyleChannelId,
            R.drawable.frank,
            "新消息",
            "收到三条新消息"
        )
        //创建通知
        val person = Person.Builder().run {
            setName("Bob")
            build()
        }
        val style = NotificationCompat.MessagingStyle(person).run {
            addMessage("Hello", System.currentTimeMillis(), person)
            addMessage("World", System.currentTimeMillis(), person)
            addMessage("Thanks", System.currentTimeMillis(), person)
        }
        val builder = mNotificationUtils.createNotification(
            testStyleChannelId,
            R.drawable.frank,
            "新消息",
            "收到三条新消息",
            style
        ).run {
            setVisibility(NotificationCompat.VISIBILITY_PRIVATE)
        }
        //设置备用通知
        builder.setPublicVersion(backupBuilder.build())
        //发送通知
        mNotificationUtils.sendNotification(builder.build())
    }

通过上面的代码设置通知之后,如果是锁屏状态下就会显示备用通知,也就是只显示"新消息"和"收到三条新消息"的通知,打开锁屏页面后就会显示通知的真正内容,也就是设置的style中的内容。

显示紧急消息

有时候我们可能需要显示一些紧急消息,比如来电,比如微信视频通话等,这种情况下,我们可以将全屏Intent与通知关联,调用通知的时候,根据设备的锁定状态,用户将会看到以下情况之一:

  • 如果用户设备被锁定,会显示全屏Activity
  • 如果用户设备处于解锁状态,通知以展开形式展示,其中包含用于处理或关闭通知的选项。

以下代码演示了将通知和全屏Intent关联:

    //显示紧急消息
    private fun createUrgentNotification() {
        //创建渠道
        createTestStyleChannel()
        //创建全屏Intent
        val intent = Intent(this, ImportantActivity::class.java)
        val fullScreenPendingIntent =
            PendingIntent.getActivity(this, 10000, intent, PendingIntent.FLAG_UPDATE_CURRENT)
        //创建通知
        val utils = NotificationUtils.getInstance(this)
        val builder =
            utils.createNotification(testStyleChannelId, R.drawable.frank, "紧急通知", "Hello World")
        //关联全屏Intent
        builder.setFullScreenIntent(fullScreenPendingIntent, true)
        //发送通知
        utils.sendNotification(builder.build())

    }

在实际测试中,除了需要设置上面的属性,还需要在要打开的Activity中设置下面的属性才能在锁屏页面直接打开要显示的Activity

    override fun onCreate(savedInstanceState: Bundle?) {
        requestWindowFeature(Window.FEATURE_NO_TITLE)
        //设置以下属性在锁屏显示通知的时候直接打开本页面
        window.addFlags(
            WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
                    or WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
                    or WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
                    or WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
        )

        super.onCreate(savedInstanceState)
    }

需要注意的是,以上代码的效果仅在我的一加5手机上测试通过,在其它手机上并没有测试过。