Notification相关

  • 概述

    通知是指 Android 在您应用的界面之外显示的消息,旨在向用户提供提醒、来自他人的通信信息或您应用中的其他实时信息。用户可以点按通知来打开应用,或直接从通知中执行操作。

    需要注意的是,因为Android系统的开源属性,不同厂商的系统会有区别,如果系统通知设置中有关于通知的相关设置开关,则会以系统设置的为准(用户设置最终控制),所以Notification相关的API设置并不一定呈现API期望的效果,比如通知重要程度(优先级)的设置

    image

    比如这张图展示的某个通知渠道的系统设置,开启锁屏通知、圆点图标、横幅通知(悬挂式通知)、在状态栏上显示、铃声、振动、勿扰模式时允许通知提醒等设置都可以通过系统提供的开关设置,所以代码中设置的同样作用的API就会无效。

  • 基本样式

    image-20210112164741233
  • 创建通知

    • 使用兼容API

      为了兼容低版本,代码中最好使用Compat后缀的相关类,比如NotificationCompat替代Notification、NotificationManegerCompat代替NotificationManeger等,这样的话新的功能可能在旧系统上无效但不会崩溃。

    • 构建

      Notification通过NotificationCompat.Builder来构建,Builder(context)方法被废弃了,Android 8.0及以上使用NotificationCompat.Builder(context,channelId)来构建,也就是必须设置NotificationChannel,否则不会显示出通知。channelId使用NotificationChannel.DEFAULT_CHANNEL_ID并不会生效,因为根据这个字段的注释得知这个值是为了8.0以下的版本没有设置channelId的情况下使用的:

      /**
       * The id of the default channel for an app. This id is reserved by the system. All
       * notifications posted from apps targeting {@link android.os.Build.VERSION_CODES#N_MR1} or
       * earlier without a notification channel specified are posted to this channel.
       */
      public static final String DEFAULT_CHANNEL_ID = "miscellaneous";
      

      构建示例:

      var builder = NotificationCompat.Builder(this, CHANNEL_ID)
                  .setSmallIcon(R.drawable.notification_icon)
                  .setContentTitle(textTitle)
                  .setContentText(textContent)
                  .setPriority(NotificationCompat.PRIORITY_DEFAULT)
      

      setPriority设置优先级,确定通知在 Android 7.1 和更低版本上的干扰程度。在8.0及以上则通过channel的importance设置。

    • 创建通知渠道

      private fun createChannel(){
          if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
              NotificationManagerCompat.from(this).apply {
                  val newChannel = NotificationChannel(
                      MyNotificationChannelId.MUSIC_NOTIFICATION_CHANNEL_ID,
                      "通知渠道的名称,在系统通知设置中的对应应用通知设置下可以看到",
                      NotificationManager.IMPORTANCE_LOW
                  )
                  createNotificationChannel(newChannel)
              }
          }
      }
      

      注意:您应该通过对 SDK_INT 版本设置条件来防护此代码,以使其仅在 Android 8.0(API 级别 26)及更高版本上运行,因为支持库中不提供通知渠道 API。

      由于您必须先创建通知渠道,然后才能在 Android 8.0 及更高版本上发布任何通知,因此应在应用启动时立即执行这段代码。反复调用这段代码是安全的,因为创建现有通知渠道不会执行任何操作。

      NotificationChannel的第一个参数是channelId,我们在构建Notification的时候传入Builder的channelId就是这个,从而和这里创建的channel绑定在一起;

      第二个参数是渠道的名称,这个名称是为了在通知设置中展示,比如:

      image

      系统默认通道是系统分配的,其他三个是通过形如上面的代码创建的,“消息推送”、“音乐通知”、“天气预报”就是这里的第二个参数指定的;

      第三个参数是设置通知的重要性,决定通知的干扰程度,和旧版本的setPriority作用一样,它的可设的值有:

      • NotificationManager.IMPORTANCE_UNSPECIFIED:表示没有设置重要性,不要用于某个具体的Notification构建;

      • NotificationManager.IMPORTANCE_NONE:可以理解为不通知任何东西;

      • NotificationManager.IMPORTANCE_MIN:可以理解为只显示App图标上的圆点标志(因系统而异);

      • NotificationManager.IMPORTANCE_LOW:可以理解为显示圆点标志并在状态栏显示,注意如果上下文是Foreground Service(前台服务)则系统会自动提高该通知的重要性;

      • NotificationManager.IMPORTANCE_DEFAULT:可以理解为显示圆点标志、在状态栏显示并发出声音;

      • NotificationManager.IMPORTANCE_HIGH:可以理解为显示圆点标志、在状态栏显示、发出声音并横幅弹出。

    • 多文本预览

      默认情况下,通知的文本内容会被截断以放在一行。如果想要更长的通知,可以使用setStyle()添加样式模板来启用可展开的通知:

          var builder = NotificationCompat.Builder(this, CHANNEL_ID)
                  ... ...
                  .setStyle(Notification.BigTextStyle()
                      .bigText("Much longer text that cannot fit one line..."))
      
    • 点击通知跳转

      通常我们需要点击通知跳转到原生页面或者网页中。

          // Create an explicit intent for an Activity in your app
          val intent = Intent(this, AlertDetails::class.java).apply {
              flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
          }
          val pendingIntent: PendingIntent = PendingIntent.getActivity(this, 0, intent, 0)
      
          val builder = NotificationCompat.Builder(this, CHANNEL_ID)
                  .setSmallIcon(R.drawable.notification_icon)
                  .setContentTitle("My notification")
                  .setContentText("Hello World!")
                  .setPriority(NotificationCompat.PRIORITY_DEFAULT)
                  // Set the intent that will fire when the user taps the notification
                  .setContentIntent(pendingIntent)
                              //设置点击通知后自动从状态栏和下拉通知中消失
                  .setAutoCancel(true)
                              //设置横幅通知样式,Android 10(API 级别 29)或更高版本,必须在应用清单文件中请求 USE_FULL_SCREEN_INTENT 权限
                              .setFullScreenIntent(pendingIntent, true)
      
      

      PendingIntent.getActivity最多可以传递四个参数,依次是上下文、requestCode、intent、intent的启动属性、传递的参数Bundle。除此,还有getBroadcast、getForegroundService、getService方法,但是它们不能传递Bundle。还有一个getActivities方法,这个方法会按数组的逆顺序创建Activity,只有上一个Activity销毁后才会创建。

    • 显示、更新通知

      显示和更新都是使用notify:

      NotificationManagerCompat.from(this).apply {
          //id和Notification对象
          notify(2, builder.build())
      }
      

      id和之前的一致就是更新(通知还在的情况下)。

    • 取消

      • 用户关闭通知,手指左右滑动删除。
      • 用户点击通知,且在创建通知时调用了 setAutoCancel()
      • 针对特定的通知 ID 调用了 cancel()。
      • 调用了 cancelAll() 方法,该方法将移除之前发出的所有通知。
      • 如果在创建通知时使用 setTimeoutAfter() 设置了超时,系统会在指定持续时间过后取消通知。如果需要,可以在指定的超时持续时间过去之前取消通知。
    • 设置类别

      builder可以通过setCategory设置通知所属的类别,这样可以在勿扰模式下决定通知会不会被允许干扰,通知设置中如果对分组打开了勿扰模式下允许通知的选项,则设不设置类别都会提示,在未设置的情况下,通常只有设置的允许项、闹钟、倒计时等允许干扰,所以比如设置NotificationCompat.CATEGORY_ALARM会在勿扰模式被允许提示。

    • 锁屏时的可见度

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

      当设置 VISIBILITY_PRIVATE 时,您还可以提供通知内容的备用版本,以隐藏特定详细信息。例如,短信应用可能会显示一条通知,提示“您有 3 条新短信”,但是隐藏了短信内容和发件人。如需提供此备用通知,首先请像平时一样使用 NotificationCompat.Builder 创建备用通知。然后使用 setPublicVersion() 将备用通知附加到普通通知中:

       if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
      //            builder.setVisibility(NotificationCompat.VISIBILITY_SECRET)
      //            builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
                  builder.setVisibility(NotificationCompat.VISIBILITY_PRIVATE)
                  //私有模式时可以设置锁屏状态下用户可以看见的内容
                  val publicNote = NotificationCompat.Builder(
                      this,
                      MyNotificationChannelId.MESSAGE_NOTIFICATION_CHANNEL_ID
                  )
                      .setSmallIcon(R.mipmap.ic_launcher_round)
                      .setContentTitle("收到新消息3条~~")
                  builder.setPublicVersion(publicNote.build())
      
          }
      

      但是,对于通知在锁定屏幕上是否可见,用户始终拥有最终控制权,甚至可以根据应用的通知渠道来控制公开范围,这也就是我们一开始说的系统设置(也就是用户本身)拥有最终的控制权。所以这里设置的可见性在未提供用户设置的情况下才有效,setPublicVersion方法生效只有在最终的可见性是隐藏时才会有效果。

    • 添加进度条

      调用builder的setProgress(max,progress,false)来设置进度条,最后一个参数为true表示是一个不确定性的进度条,会一直显示加载中的动画,直到调用setProcess(0,0,false)来使进度条消失。

      如果是fasle表示是确定性的进度条,第一个参数是最大值,第二个参数是当前进度,通常会在后台线程中更新当前进度,然后在主线程调用setProcess方法更细进度,从而带动进度条的前进,下面使用协程写个例子:

        val builder = NotificationCompat.Builder(
                      this,
                      MyNotificationChannelId.MUSIC_NOTIFICATION_CHANNEL_ID
                  )
                              val MAX_PROCESS = 100
                  var currentProcess = 0
                  builder.setSmallIcon(R.mipmap.ic_launcher_round)
                      .setContentText("下载中...")
                      .setProgress(MAX_PROCESS, currentProcess, false)
                      //不确定型进度条
                      //.setProgress(0, 0, true)
      
                  NotificationManagerCompat.from(this).apply {
                      notify(4, builder.build())
                  }
      
                  GlobalScope.launch {
                      while (currentProcess < MAX_PROCESS) {
                          currentProcess += 5
                          updateProcess(builder, currentProcess)
                          delay(500)
                      }
      
                      updateProcess(builder, currentProcess, "下载结束~~~")
              }
      

      updateProcess如下:

      private fun updateProcess(
          builder: NotificationCompat.Builder,
          currentProcess: Int,
          label: String? = null
      ) {
          //下载结束
          if (label != null) {
              builder.setContentText(label)
          }
          builder.setProgress(MAX_PROCESS, currentProcess, false)
          NotificationManagerCompat.from(this).apply {
              notify(4, builder.build())
          }
      }
      

      但是实际上下载文件,应该使用DownloadManager,它会提供自己的通知来跟踪下载进度。

    • 添加操作按钮

      一个通知最多可以提供三个操作按钮,让用户能够快速响应,例如暂停提醒,甚至回复短信。但这些操作按钮不应该重复用户在点按通知时执行的操作。

          val snoozeIntent = Intent(this, MyBroadcastReceiver::class.java).apply {
              action = ACTION_SNOOZE
              putExtra(EXTRA_NOTIFICATION_ID, 0)
          }
          val snoozePendingIntent: PendingIntent =
              PendingIntent.getBroadcast(this, 0, snoozeIntent, 0)
          val builder = NotificationCompat.Builder(this, CHANNEL_ID)
                  .setSmallIcon(R.drawable.notification_icon)
                  .setContentTitle("My notification")
                  .setContentText("Hello World!")
                  .setPriority(NotificationCompat.PRIORITY_DEFAULT)
                  .setContentIntent(pendingIntent)
                  .addAction(R.drawable.ic_snooze, getString(R.string.snooze),
                          snoozePendingIntent)
          
      
    • 添加直接回复操作

      Android 7.0(API 级别 24)中引入的直接回复操作允许用户直接在通知中输入文本,然后会直接提交给应用,而不必打开 Activity。例如,可以使用直接回复操作让用户从通知内回复短信或更新任务列表。

      image-20210119141022645

      直接回复操作在通知中显示为一个额外按钮,可打开文本输入。当用户完成输入后,系统会将文本回复附加到为通知操作指定的 Intent,然后将 Intent 发送到您的应用。

      下面是一个从回复到接收处理的例子:

      /**添加直接回复操作*/
      txt_reply_notification.setOnClickListener {
          //hintLabel是输入框中的输入提示
          var hintLabel = "在这里秒回"
          //之后要根据ConversationData.KEY_TEXT_REPLY为key来获取输入的文本
          var remoteInput = RemoteInput.Builder(ConversationData.KEY_TEXT_REPLY).run {
              setLabel(hintLabel)
              build()
          }
      
          val replyPendingIntent = PendingIntent.getBroadcast(this,
              ConversationData.getConversationId(),
              Intent(this,MessageBroadcast::class.java).apply {
                  putExtra("requestCode",ConversationData.getConversationId())
              },
              PendingIntent.FLAG_UPDATE_CURRENT
              )
        
              //“回复它”是通知栏上的按钮的文字显示
          val action = NotificationCompat.Action.Builder(R.mipmap.ic_launcher_round,"回复它",replyPendingIntent)
              .addRemoteInput(remoteInput).setAllowGeneratedReplies(false).build()
      
          // Build the notification and add the action.
          //注意这里的NotificationChannel已提前创建
          val newMessageNotification = NotificationCompat.Builder(this, MyNotificationChannelId.MUSIC_NOTIFICATION_CHANNEL_ID)
              .setSmallIcon(R.mipmap.ic_launcher_round)
              .setContentTitle("收到John的新消息")
              .setContentText("明天一起打球吗?")
              .addAction(action)
              .build()
      
          // Issue the notification.
          with(NotificationManagerCompat.from(this)) {
              notify(6, newMessageNotification)
          }
      }
      

      接收的MessageBroadcast为:

      //通过以下方式获取通知中回复输入的信息
                  val replyMsg = RemoteInput.getResultsFromIntent(intent)?.getCharSequence(ConversationData.KEY_TEXT_REPLY)
                  if (replyMsg?.isNotEmpty() == true) {
                      ConversationData.messageList.add(replyMsg.toString())
                      Log.d("ReplyMsg", "say: $replyMsg")
      
                      //更新通知栏
                      val newMessageNotification = NotificationCompat.Builder(context!!, MyNotificationChannelId.MUSIC_NOTIFICATION_CHANNEL_ID)
                          .setSmallIcon(R.mipmap.ic_launcher_round)
                          .setContentTitle("回复反馈")
                          .setContentText("John已收到回复")
                              //这里设置无效,只能在remoteInput情况下才有效
                          //.setRemoteInputHistory(arrayOf(replyMsg))
                          .build()
      
                      // Issue the notification.
                      with(NotificationManagerCompat.from(context)) {
                          notify(6, newMessageNotification)
                      }
                  }
      

      此外可以设置setRemoteInputHistory(array)来把消息添加到通知底部,形如:

      image-20210119142315549
    • 有关即时通讯应用的最佳做法

      上面的setRemoteInputHistory方法不是即时通讯通知的最优方式。

      从 Android 7.0(API 级别 24)起,Android 提供了专用于消息内容的通知样式模板。使用 NotificationCompat.MessagingStyle 类,可以更改在通知中显示的多个标签,包括会话标题、其他消息和通知的内容视图。

          var notification = NotificationCompat.Builder(this, CHANNEL_ID)
                  .setStyle(NotificationCompat.MessagingStyle("Me")
                          .setConversationTitle("Team lunch")
                          .addMessage("Hi", timestamp1, null) // Pass in null for user.
                          .addMessage("What's up?", timestamp2, "Coworker")
                          .addMessage("Not much", timestamp3, null)
                          .addMessage("How about lunch?", timestamp4, "Coworker"))
                  .build()
          
      

      从 Android 8.0(API 级别 26)起,使用 NotificationCompat.MessagingStyle 类的通知会在采用折叠形式时显示更多内容。

  • 创建展开式通知

    • 添加大图片

          var notification = NotificationCompat.Builder(context, CHANNEL_ID)
                  .setSmallIcon(R.drawable.new_post)
                  .setContentTitle(imageTitle)
                  .setContentText(imageDescription)
                              //如需使该图片仅在通知收起时显示为缩略图(如图 1 所示),请调用 setLargeIcon() 并向其传递图片
                  .setLargeIcon(myBitmap)
                  .setStyle(NotificationCompat.BigPictureStyle()
                          //展开时的图片
                          .bigPicture(myBitmap)
                                              //同时调用 BigPictureStyle.bigLargeIcon() 并向其传递 null,这样大图标就会在通知展开时消失
                          .bigLargeIcon(null))
                  .build()
          
      
      image-20210119152402875

      setLargeIcon参数有两种类型,Icon和Bitmap,适用于不同的版本:

      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
          builder.setLargeIcon(
              Icon.createWithResource(
                  this,
                  R.drawable.ic_launcher_background
              )
          )
      } else {
          val drawable = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
              resources.getDrawable(R.drawable.ic_launcher_background, null)
          } else {
              resources.getDrawable(R.drawable.ic_launcher_background)
          }
          val bitmap = Bitmap.createBitmap(
              drawable.intrinsicWidth,
              drawable.intrinsicHeight,
              Bitmap.Config.ARGB_8888
          )
          val canvas = Canvas(bitmap)
          drawable.setBounds(0, 0, canvas.width, canvas.height)
          drawable.draw(canvas)
      
          val bb = BitmapFactory.decodeResource(resources, R.drawable.ic_launcher_background)
          builder.setLargeIcon(bitmap)
      }
      
    • 添加大段文本

          var notification = NotificationCompat.Builder(context, CHANNEL_ID)
                  .setSmallIcon(R.drawable.new_mail)
                  .setContentTitle(emailObject.getSenderName())
                  .setContentText(emailObject.getSubject())
                  .setLargeIcon(emailObject.getSenderAvatar())
                  .setStyle(NotificationCompat.BigTextStyle()
                          .bigText(emailObject.getSubjectAndSnippet()))
                  .build()
          
      
    • 创建收件箱样式的通知

      如果想添加多个简短摘要行(例如收到的电子邮件中的摘要),请对通知应用 NotificationCompat.InboxStyle。这样就可以添加多条内容文本,并且每条文本均截断为一行,而不是显示为 NotificationCompat.BigTextStyle 提供的一个连续文本行。

      如需添加新行,最多可调用 addLine() 6 次。如果添加的行超过 6 行,仅显示前 6 行。

      var builder = NotificationCompat.Builder(this,MyNotificationChannelId.MESSAGE_NOTIFICATION_CHANNEL_ID)
                  .setSmallIcon(R.mipmap.ic_launcher_round)
                  .setContentTitle("收件箱样式")
                  .setContentText("收取新邮件~")
                  .setStyle(NotificationCompat.InboxStyle()
                      .setSummaryText("邮件来了:")
                      .addLine("星期一")
                      .addLine("星期二")
                      .addLine("星期三")
                      .addLine("星期四")
                      .addLine("星期五")
                      .addLine("星期六")
                      .addLine("星期日")
                  )
       NotificationManagerCompat.from(this).apply {
            notify(7,builder.build())
       }    
      

      折叠时:

      image-20210119181957112

      展开时:

      image-20210119182045198

      可以看到,这里显示了七条,和官方说明的不一样,不同厂商的定制系统会有差异。

  • 启动两种模式的Activity

    创建通知一文中介绍了为通知设置点按行为的基本方法,而此页面介绍了如何为通知的操作设置 PendingIntent,以便它可以创建新的任务和返回堆栈。但是,具体操作取决于需要启动的 Activity 类型:

    • 常规 Activity

      这类 Activity 是应用的正常用户体验流程的一部分。因此,当用户从通知转到这类 Activity 时,新任务应包括完整的返回堆栈,以便用户可以按“返回”按钮并沿应用层次结构向上导航。

      规范做法如下:

      1. 通过向应用清单文件中的每个 <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>
            <!-- MainActivity is the parent for ResultActivity -->
            <activity
                android:name=".ResultActivity"
                android:parentActivityName=".MainActivity" />
                ...
            </activity>
            
        

        parentActivityName用于指定此Activity返回后下一个要显示的页面,但只对系统导航栏的返回有效,对于页面中的返回按钮的逻辑还是要自己定义。

      2. 如需启动包含 Activity 的返回堆栈的 Activity,您需要创建 TaskStackBuilder 的实例并调用 addNextIntentWithParentStack(),向其传递您要启动的 Activity 的 Intent

        只要您为每个 Activity 定义了父 Activity(如上文所述),就可以调用 getPendingIntent() 来接收包含整个返回堆栈的 PendingIntent

        TaskStackBuilder.create(this).run {
             addNextIntentWithParentStack(Intent(this@MainActivity, Activity3::class.java))
             //只有系统导航栏返回的时候有效,自定义的finish等无效
             editIntentAt(0)?.apply {
                  putExtra("Param", "我来自Activity2")
             }
             editIntentAt(1)?.apply {
                  putExtra("Param", "我来自Activity3")
             }
             editIntentAt(2)?.apply {
                  putExtra("Param", "我来自通知")
             }
             getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
         }
        

        AndroidManifest.xml中:

        <activity android:name="com.mph.demo2.Activity3" android:parentActivityName="com.mph.demo2.Activity2"/>
        <activity android:name="com.mph.demo2.Activity2" android:parentActivityName="com.mph.demo2.Activity1"/>
        <activity android:name="com.mph.demo2.Activity1" />
        

        editIntentAt(index)会按照index取到跳转的intent从而来设置参数,比如这里,按照manifest里面的设置,跳转的顺序是Activity3->Activity2->Activity1,editIntentAt(0)就是Activity2跳Activity1的intent,editIntentAt(1)就是Activity3跳Activity2的intent,editIntentAt(2)就是通知跳Activity3的intent。

        注意页面中的自定义的含有返回逻辑的按钮需要自己设置返回参数或者intent,manifest中配置的和这个无关。

    • 特殊 Activity

      只有当 Activity 从通知启动时,用户才可以看到此类 Activity。从某种意义上来说,这类 Activity 通过提供通知本身难以显示的信息来扩展通知界面。因此,这类 Activity 不需要返回堆栈。

          <activity
              android:name=".ResultActivity"
              android:launchMode="singleTask"
              android:taskAffinity=""
              android:excludeFromRecents="true">
          </activity>
          
      
          val notifyIntent = Intent(this, ResultActivity::class.java).apply {
              flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
          }
          val notifyPendingIntent = PendingIntent.getActivity(
                  this, 0, notifyIntent, PendingIntent.FLAG_UPDATE_CURRENT
          )
          
      

      taskAffinity与将在代码中使用的 FLAG_ACTIVITY_NEW_TASK 标记结合使用,将此属性设置为空,可确保这类 Activity 不会进入应用的默认任务。具有应用默认亲和性的任何现有任务都不会受到影响。

      excludeFromRecents用于从“最近”中排除新任务,以免用户意外返回它。

  • 创建一组通知

    从 Android 7.0(API 级别 24)开始,可以在一个通知组中显示相关通知(以前称为“捆绑式”通知)。例如,如果应用针对收到的电子邮件显示通知,应将所有通知放入同一个通知组,以便它们可以收起。

    为了支持较低版本,还可以添加摘要通知,该摘要通知会单独显示以总结所有单独的通知 - 通常最好使用收件箱样式的通知实现此目的。

    如果应用发出 4 条或更多条通知且未指定通知组,那么在 Android 7.0 及更高版本上,系统会自动将这些通知分为一组。注意,手动设置摘要是为了兼容低版本,而7.0之后设置了某些属性也可能不会起作用(被系统取代)。

    如需创建通知组,请为该通知组定义一个唯一标识符字符串。然后,对于您想要添加到通知组中的每条通知,只需调用 setGroup() 并传入通知组名称即可。

        val GROUP_KEY_WORK_EMAIL = "com.android.example.WORK_EMAIL"
    
        val newMessageNotification = NotificationCompat.Builder(this@MainActivity, CHANNEL_ID)
                .setSmallIcon(R.drawable.new_mail)
                .setContentTitle(emailObject.getSenderName())
                .setContentText(emailObject.getSubject())
                .setLargeIcon(emailObject.getSenderAvatar())
                .setGroup(GROUP_KEY_WORK_EMAIL)
                .build()
        
    

    默认情况下,系统会根据通知的发布时间对其进行排序,但您可以通过调用 setSortKey() 更改通知顺序。

    如果通知组的提醒应由其他通知处理,请调用 setGroupAlertBehavior()。例如,如果您只希望通知组摘要发出提醒,那么通知组中的所有子级都应具有通知组提醒行为 GROUP_ALERT_SUMMARY。其他选项包括 GROUP_ALERT_ALL 和 GROUP_ALERT_CHILDREN。

    在 Android 7.0(API 级别 24)及更高版本上,系统会使用每条通知中的文本摘要,自动为您的通知组创建摘要。用户可以展开此通知以查看每条单独的通知,如图 1 所示。要支持无法显示嵌套通知组的较低版本,您必须另外创建一条通知来充当摘要。这是显示的唯一通知,系统会隐藏所有其他通知。因此,此摘要应包含所有其他通知的片段,供用户点按以打开您的应用(通过setGroup方法联系在一起)。

    注意:通知组摘要的行为在某些设备类型(例如穿戴式设备)上可能会有所不同。为确保在所有设备和版本上实现最佳体验,请务必在创建通知组时添加通知组摘要。

    如需添加通知组摘要,请按以下步骤操作:

    1. 使用通知组说明创建新通知 - 通常最好使用收件箱样式的通知实现此目的。
    2. 通过调用 setGroup() 将摘要通知添加到通知组中。
    3. 通过调用 setGroupSummary(true) 指定将其用作通知组摘要。
        //use constant ID for notification used as group summary
        val SUMMARY_ID = 0
        val GROUP_KEY_WORK_EMAIL = "com.android.example.WORK_EMAIL"
    
        val newMessageNotification1 = NotificationCompat.Builder(this@MainActivity, CHANNEL_ID)
                .setSmallIcon(R.drawable.ic_notify_email_status)
                .setContentTitle(emailObject1.getSummary())
                .setContentText("You will not believe...")
                .setGroup(GROUP_KEY_WORK_EMAIL)
                .build()
    
        val newMessageNotification2 = NotificationCompat.Builder(this@MainActivity, CHANNEL_ID)
                .setSmallIcon(R.drawable.ic_notify_email_status)
                .setContentTitle(emailObject2.getSummary())
                .setContentText("Please join us to celebrate the...")
                .setGroup(GROUP_KEY_WORK_EMAIL)
                .build()
    
        val summaryNotification = NotificationCompat.Builder(this@MainActivity, CHANNEL_ID)
                .setContentTitle(emailObject.getSummary())
                //set content text to support devices running API level < 24
                .setContentText("Two new messages")
                .setSmallIcon(R.drawable.ic_notify_summary_status)
                //build summary info into InboxStyle template
                .setStyle(NotificationCompat.InboxStyle()
                        .addLine("Alex Faarborg Check this out")
                        .addLine("Jeff Chang Launch Party")
                        .setBigContentTitle("2 new messages")
                        .setSummaryText("janedoe@example.com"))
                //specify which group this notification belongs to
                .setGroup(GROUP_KEY_WORK_EMAIL)
                //set this notification as the summary for the group
                .setGroupSummary(true)
                .build()
    
        NotificationManagerCompat.from(this).apply {
            notify(emailNotificationId1, newMessageNotification1)
            notify(emailNotificationId2, newMessageNotification2)
            notify(SUMMARY_ID, summaryNotification)
        }
        
    
  • 创建渠道分组

    不仅通知可以分组,渠道也可以分组,这就允许有相同名字的渠道,但是属于不同的渠道分组。

    /**
     * 注意:您应该通过对 SDK_INT 版本设置条件来防护此代码,以使其仅在 Android 8.0(API 级别 26)及更高版本上运行,因为支持库中不提供通知渠道 API。
     */
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        NotificationManagerCompat.from(context).apply {
            //创建"主要通知"渠道组
            createNotificationChannelGroup(
                NotificationChannelGroup(
                    MyNotificationChannelId.NOTIFICATION_CHANNEL_GROUP_MAIN_ID,
                    MyNotificationChannelId.NOTIFICATION_CHANNEL_GROUP_MAIN_NAME
                )
            )
            //创建"其他通知"渠道组
            createNotificationChannelGroup(
                NotificationChannelGroup(
                    MyNotificationChannelId.NOTIFICATION_CHANNEL_GROUP_OTHER_ID,
                    MyNotificationChannelId.NOTIFICATION_CHANNEL_GROUP_OTHER_NAME
                )
            )
        }
    }
    
    const val NOTIFICATION_CHANNEL_GROUP_MAIN_ID = "Main_Notification_Channel_Group"
    const val NOTIFICATION_CHANNEL_GROUP_OTHER_ID = "Other_Notification_Channel_Group"
    const val NOTIFICATION_CHANNEL_GROUP_MAIN_NAME = "主要通知"
    const val NOTIFICATION_CHANNEL_GROUP_OTHER_NAME = "其他通知"
    

    这样在系统通知管理中的当前应用下会看到:

    image

    “其他”是系统默认的渠道分组。

  • 创建自定义通知布局

    NotificationManagerCompat.from(this).apply {
        // Get the layouts to use in the custom notification
        //不支持ConstraintLayout
        val notificationLayout = RemoteViews(packageName, R.layout.notification_small)
        val notificationLayoutExpanded = RemoteViews(packageName, R.layout.notification_large)
        val notification = NotificationCompat.Builder(
            this@MainActivity,
            MyNotificationChannelId.MESSAGE_NOTIFICATION_CHANNEL_ID
        )
            .setSmallIcon(R.mipmap.ic_launcher_round)
            //适配低于4.1的系统(RemoteViews和setCustomContentView设置的是同一个)
            .setContent(notificationLayout)
            //如果要完全自定义则不要调用setStyle(不建议)
            .setStyle(NotificationCompat.DecoratedCustomViewStyle())
            .setCustomContentView(notificationLayout)
            .setCustomBigContentView(notificationLayoutExpanded)
            //设置横幅布局
            .setCustomHeadsUpContentView(notificationLayout)
            .build()
        notify(10, notification)
    }
    
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:text="@string/notification_title"
            android:id="@+id/notification_title"
            style="@style/TextAppearance.Compat.Notification.Title" />
        
    
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容