提醒通知

qiang.zhang大约 13 分钟

提醒通知

通知剖析

通知的设计由系统模板决定,我们的应用只需要定义模板中各个部分的内容即可。通知的部分详情仅在展开后的视图中显示。

包含基本详情的通知
包含基本详情的通知

图中展示了通知最常见的部分,具体如下所示:

小图标:必须提供,通过 setSmallIcon() 进行设置。
应用名称:由系统提供。
时间戳:由系统提供,但我们可以使用 setWhen() 替换它或者使用 setShowWhen(false) 隐藏它。
大图标:可选内容(通常仅用于联系人照片,勿将其用于应用图标),通过 setLargeIcon() 进行设置。
标题:可选内容,通过 setContentTitle() 进行设置。
文本:可选内容,通过 setContentText() 进行设置。

创建自定义通知布局

使用自定义通知布局时,特别注意确保我们的自定义布局适用于不同的设备屏幕方向和分辨率。虽然对于所有界面布局,此建议都适用,但它对通知布局而言尤为重要,因为抽屉式通知栏中的空间非常有限。自定义通知布局的可用高度取决于通知视图。通常情况下,收起后的视图布局的高度上限为 64 dp,展开后的视图布局的高度上限为 256 dp。

为内容区域创建自定义布局

如果我们需要自定义布局,可以将 NotificationCompat.DecoratedCustomViewStyle 应用于我们的通知。借助此 API,我们可以为通常由标题和文本内容占据的内容区域提供自定义布局,同时仍对通知图标、时间戳、子文本和操作按钮使用系统装饰。

该 API 的工作方式与展开式通知模板类似,都是基于基本通知布局,如下所示:

使用 NotificationCompat.Builder 构建基本通知。
调用 setStyle(),向其传递一个 NotificationCompat.DecoratedCustomViewStyle 实例。
将自定义布局扩充为 RemoteViews 的实例。
调用 setCustomContentView() 以设置收起后通知的布局。

我们还可以选择调用 setCustomBigContentView() 为展开后通知设置不同的布局。

  • tip:如果要为媒体播放控件创建自定义通知,采纳同样的建议,但改用 NotificationCompat.DecoratedMediaCustomViewStyle 类。 例如:

    // Get the layouts to use in the custom notification
    RemoteViews notificationLayout = new RemoteViews(getPackageName(), R.layout.notification_small);
    RemoteViews notificationLayoutExpanded = new RemoteViews(getPackageName(), R.layout.notification_large);

    // Apply the layouts to the notification
    Notification customNotification = new NotificationCompat.Builder(context, CHANNEL_ID)
            .setSmallIcon(R.drawable.notification_icon)
            .setStyle(new NotificationCompat.DecoratedCustomViewStyle())
            .setCustomContentView(notificationLayout)
            .setCustomBigContentView(notificationLayoutExpanded)
            .build();

通知的背景颜色可能会因设备和版本而异。因此,我们应始终在自定义布局中应用支持库样式,例如对文本使用 TextAppearance_Compat_Notification,对标题使用 TextAppearance_Compat_Notification_Title。这些样式会适应颜色的变化,因此不会出现黑色文本采用黑色背景或白色文本采用白色背景的情况。例如:


    <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" />

另外,避免在 RemoteViews 对象上设置背景图片,因为可能会导致文本颜色无法读取。

创建完全自定义的通知布局

如果我们不希望使用标准通知图标和标题装饰通知,按照上述步骤使用 setCustomBigContentView(),但不要调用 setStyle()。

  • tip:建议不要使用未经装饰的通知,因为这会使通知与它的其余部分不匹配,进而导致通知在向通知区域应用了不同样式的不同设备上显示时,出现严重的布局兼容性问题。 如需支持低于 Android 4.1(API 级别 16)的 Android 版本,我们还应调用 setContent(),向其传递同一 RemoteViews 对象。

通知的使用和构造

添加支持库

dependencies {
        implementation "com.android.support:support-compat:28.0.0"
    }

设置通知内容

首先,需要使用 NotificationCompat.Builder 对象设置通知内容和渠道。以下示例显示了如何创建包含下列内容的通知:

小图标,通过 setSmallIcon() 设置。这是所必需的唯一用户可见内容。
标题,通过 setContentTitle() 设置。
正文文本,通过 setContentText() 设置。
通知优先级,通过 setPriority() 设置。优先级确定通知在 Android 7.1 和更低版本上的干扰程度。(对于 Android 8.0 和更高版本,必须设置渠道重要性,如下一节中所示。)
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID)
    .setSmallIcon(R.drawable.notification_icon)
    .setContentTitle(textTitle)
    .setContentText(textContent)
    .setPriority(NotificationCompat.PRIORITY_DEFAULT);

NotificationCompat.Builder 构造函数要求提供渠道 ID。这是兼容 Android 8.0(API 级别 26)及更高版本所必需的,但会被较旧版本忽略。

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


    NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID)
            .setSmallIcon(R.drawable.notification_icon)
            .setContentTitle("My notification")
            .setContentText("Much longer text that cannot fit one line...")
            .setStyle(new NotificationCompat.BigTextStyle()
                    .bigText("Much longer text that cannot fit one line..."))
            .setPriority(NotificationCompat.PRIORITY_DEFAULT);
  • tip:扩展知识点-包含可展开详情的通知

创建渠道并设置重要性

必须先通过向 createNotificationChannel() 传递 NotificationChannel 的实例在系统中注册应用的通知渠道,然后才能在 Android 8.0 及更高版本上提供通知。

    private void createNotificationChannel() {
        // Create the NotificationChannel, but only on API 26+ because
        // the NotificationChannel class is new and not in the support library
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            CharSequence name = getString(R.string.channel_name);
            String description = getString(R.string.channel_description);
            int importance = NotificationManager.IMPORTANCE_DEFAULT;
            NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance);
            channel.setDescription(description);
            // Register the channel with the system; you can't change the importance
            // or other notification behaviors after this
            NotificationManager notificationManager = getSystemService(NotificationManager.class);
            notificationManager.createNotificationChannel(channel);
        }
    }

在应用启动时立即执行这段代码。反复调用这段代码是安全的,因为创建现有通知渠道不会执行任何操作。

  • 通知重要性级别:
用户可见的重要性级别重要性(Android 8.0 及更高版本)优先级(Android 7.1 及更低版本)
紧急:发出提示音,并以浮动通知的形式显示IMPORTANCE_HIGHPRIORITY_HIGH 或 PRIORITY_MAX
高:发出提示音IMPORTANCE_DEFAULTPRIORITY_DEFAULT
中:无提示音IMPORTANCE_LOWPRIORITY_LOW
低:无提示音,且不会在状态栏中显示。IMPORTANCE_MINPRIORITY_MIN

设置通知的点按操作

示例,以下代码段展示了如何创建基本 Intent,以在用户点按通知时打开 Activity:

    // Create an explicit intent for an Activity in your app
    Intent intent = new Intent(this, AlertDetails.class);
    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
    PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0);

    NotificationCompat.Builder builder = new 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);// 用户点击后自动移除通知

显示通知

如需显示通知,调用NotificationManagerCompat.notify()

并将通知的唯一ID和NotificationCompat.Builder.build() 的结果传递给它。

例如:

    NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);

    // notificationId is a unique int for each notification that you must define
    notificationManager.notify(notificationId, builder.build());
    

保存传递到 NotificationManagerCompat.notify() 的通知 ID,因为如果之后想要更新或移除通知,将需要使用这个 ID。

更新通知

如需在发出此通知后对其进行更新,再次调用 NotificationManagerCompat.notify(),并将之前使用的具有同一 ID 的通知传递给该方法。如果之前的通知已被关闭,则系统会创建一个新通知。

可以选择性调用 setOnlyAlertOnce(),这样通知只会在通知首次出现时打断用户(通过声音、振动或视觉提示),而之后更新则不会再打断用户。

  • Android 会在更新通知时应用速率限制。如果过于频繁地发布对某条通知的更新(不到一秒内发布多条通知),系统可能会丢弃部分更新。

移除通知

除非发生以下情况之一,否则通知仍然可见:

  1. 用户关闭通知。
  2. 用户点击通知,且在创建通知时调用了 setAutoCancel()。
  3. 针对特定的通知 ID 调用了 cancel()。此方法还会删除当前通知。
  4. 调用了 cancelAll() 方法,该方法将移除之前发出的所有通知。
  5. 如果在创建通知时使用 setTimeoutAfter() 设置了超时,系统会在指定持续时间过后取消通知。也可以在指定的超时持续时间过去之前取消通知。

添加操作按钮

添加操作按钮,将 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 builder = new 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。例如,可以使用直接回复操作让用户从通知内回复短信或更新任务列表。

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

添加回复按钮

  1. 创建一个可添加到通知操作的 RemoteInput.Builder 实例。此类的构造函数接受系统用作文本输入键的字符串。

    // Key for the string that's delivered in the action's intent.
    private static final String KEY_TEXT_REPLY = "key_text_reply";

    String replyLabel = getResources().getString(R.string.reply_label);
    RemoteInput remoteInput = new RemoteInput.Builder(KEY_TEXT_REPLY)
            .setLabel(replyLabel)
            .build();
  1. 为回复操作创建 PendingIntent。

    // Build a PendingIntent for the reply action to trigger.
    PendingIntent replyPendingIntent =
            PendingIntent.getBroadcast(getApplicationContext(),
                    conversation.getConversationId(),
                    getMessageReplyIntent(conversation.getConversationId()),
                    PendingIntent.FLAG_UPDATE_CURRENT);

如果重复使用 PendingIntent,则用户回复的会话可能不是他们所认为的会话。我们必须为每个会话提供一个不同的求代码,或者提供我们对任何其他会话的回复 Intent 调用 equals() 时不会返回 true 的 Intent。会话 ID 会作为 Intent 的 extra 包的一部分被频繁传递,但调用 equals() 时会被忽略。

  1. 使用 addRemoteInput() 将 RemoteInput 对象附加到操作上。
    // Create the reply action and add the remote input.
    NotificationCompat.Action action =
            new NotificationCompat.Action.Builder(R.drawable.ic_reply_icon,
                    getString(R.string.label), replyPendingIntent)
                    .addRemoteInput(remoteInput)
                    .build();
  1. 对通知应用操作并发出通知。
    // Build the notification and add the action.
    Notification newMessageNotification = new Notification.Builder(context, CHANNEL_ID)
            .setSmallIcon(R.drawable.ic_message)
            .setContentTitle(getString(R.string.title))
            .setContentText(getString(R.string.content))
            .addAction(action)
            .build();

    // Issue the notification.
    NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
    notificationManager.notify(notificationId, newMessageNotification);

从回复中检索用户输入

如需从通知回复界面接收用户输入,调用 RemoteInput.getResultsFromIntent() 并传入 BroadcastReceiver 收到的 Intent:


    private CharSequence getMessageText(Intent intent) {
        Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
        if (remoteInput != null) {
            return remoteInput.getCharSequence(KEY_TEXT_REPLY);
        }
        return null;
     }

处理完文本后,必须使用相同的 ID 和标记(如果使用)调用 NotificationManagerCompat.notify() 来更新通知。若要隐藏直接回复界面并向用户确认他们的回复已收到并得到正确处理,则必须完成该操作。

代码示例:

    //在处理这个新通知时,要使用传递给接收者的 onReceive() 方法的上下文context。
    // Build a new notification, which informs the user that the system
    // handled their interaction with the previous notification.
    Notification repliedNotification = new Notification.Builder(context, CHANNEL_ID)
            .setSmallIcon(R.drawable.ic_message)
            .setContentText(getString(R.string.replied))
            .build();

    // Issue the new notification.
    NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
    notificationManager.notify(notificationId, repliedNotification);

添加进度条

通知可以包含动画形式的进度指示器,向用户显示正在进行的操作的状态。

如果可以估算操作在任何时间点的完成进度,应通过调用 setProgress(max, progress, false) 使用指示器的“确定性”形式(如图所示)。第一个参数是“完成”值(如 100);第二个参数是当前完成的进度,最后一个参数表明这是一个确定性进度条。

随着操作的继续,持续使用 progress 的更新值调用 setProgress(max, progress, false) 并重新发出通知。

代码示例:


    ...
    NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
    NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID);
    builder.setContentTitle("Picture Download")
            .setContentText("Download in progress")
            .setSmallIcon(R.drawable.ic_notification)
            .setPriority(NotificationCompat.PRIORITY_LOW);

    // Issue the initial notification with zero progress
    int PROGRESS_MAX = 100;
    int PROGRESS_CURRENT = 0;
    builder.setProgress(PROGRESS_MAX, PROGRESS_CURRENT, false);
    notificationManager.notify(notificationId, builder.build());

    // Do the job here that tracks the progress.
    // Usually, this should be in a
    // worker thread
    // To show progress, update PROGRESS_CURRENT and update the notification with:
    // builder.setProgress(PROGRESS_MAX, PROGRESS_CURRENT, false);
    // notificationManager.notify(notificationId, builder.build());

    // When done, update the notification one more time to remove the progress bar
    builder.setContentText("Download complete")
            .setProgress(0,0,false);
    notificationManager.notify(notificationId, builder.build());
  • tip : 移除进度条,调用 setProgress(0, 0, false)

如需显示不确定性进度条(不指示完成百分比的进度条),调用 setProgress(0, 0, true)。结果会产生一个与上述进度条样式相同的指示器,区别是这个进度条是一个不指示完成情况的持续动画。在调用 setProgress(0, 0, false) 之前,进度动画会一直运行,调用后系统会更新通知以移除这个通知。

设置系统范围的类别

Android 使用一些预定义的系统范围类别来确定在用户启用勿扰模式后是否发出指定通知来干扰客户。

如果通知属于 NotificationCompat 中定义的预定义通知类别之一(例如 CATEGORY_ALARM、CATEGORY_REMINDER、CATEGORY_EVENT 或 CATEGORY_CALL),应通过将相应类别传递到 setCategory() 来进行声明。

    NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID)
            .setSmallIcon(R.drawable.notification_icon)
            .setContentTitle("My notification")
            .setContentText("Hello World!")
            .setPriority(NotificationCompat.PRIORITY_DEFAULT)
            .setCategory(NotificationCompat.CATEGORY_MESSAGE);
  • 这一项不是必需操作

设置锁定屏幕公开范围

如需控制锁定屏幕中通知的可见详情级别,调用 setVisibility() 并指定以下值之一:

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

当设置 VISIBILITY_PRIVATE 时,还可以提供通知内容的备用版本,以隐藏特定详细信息。例如,短信应用可能会显示一条通知,提示“您有 3 条新短信”,但是隐藏了短信内容和发件人。

如需提供此备用通知,首先像平时一样使用 NotificationCompat.Builder 创建备用通知。然后使用 setPublicVersion() 将备用通知附加到普通通知中。

但是,对于通知在锁定屏幕上是否可见,用户始终拥有最终控制权,甚至可以根据应用的通知渠道来控制公开范围。

  • 说白了就是无论我们做应用的怎么设置,用户最后都可以去控制我们应用的这个行为。

显示紧急消息

调用通知时,根据设备的锁定状态,用户会看到以下情况之一:

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

  • 包含全屏 Intent 的通知有很强的干扰性,因此这类通知只能用于最紧急的时效性消息。
  • 如果应用的目标平台是 Android 10(API 级别 29)或更高版本,必须在应用清单文件中请求 USE_FULL_SCREEN_INTENT 权限,以便系统启动与时效性通知关联的全屏 Activity。

示例代码:

    Intent fullScreenIntent = new Intent(this, ImportantActivity.class);
    PendingIntent fullScreenPendingIntent = PendingIntent.getActivity(this, 0,
            fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT);

    NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID)
            .setSmallIcon(R.drawable.notification_icon)
            .setContentTitle("My notification")
            .setContentText("Hello World!")
            .setPriority(NotificationCompat.PRIORITY_DEFAULT)
            .setFullScreenIntent(fullScreenPendingIntent, true);

实践

即时通讯应用的消息通知(专篇实现,还未写)

音乐软件应用的消息通知(专篇实现,还未写)

Loading...