Android 中各種通知(Notification)的使用

通知通道(Channel)

從 Android 8.0 開始,顯示通知必須先創建通道:

NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    NotificationChannel notificationChannel = NotificationFactory.createNotificationChannel();
    notificationManager.createNotificationChannel(notificationChannel);
}
@RequiresApi(api = Build.VERSION_CODES.O)
public static NotificationChannel createNotificationChannel() {
    String name = "通道名稱";
    // 設置通道的重要性級別(Android 7.1 或更低需要使用 setPriority 方法)
    int importance = NotificationManager.IMPORTANCE_HIGH;
    return new NotificationChannel(CHANNEL_ID, name, importance);
}

在構建 Notification Builder 時也需要傳入 Channel ID:

new NotificationCompat.Builder(context, CHANNEL_ID)

通知重要性級別

不同場景下的通知,其重要性也可能不同。下表中列出了不同的重要性級別,以及在不同系統版本上設置的方法:

級別 importance(Android 8.0 及以上) priority(Android 7.1 及以下)
緊急,發出聲音並作爲警告通知出現 IMPORTANCE_HIGH PRIORITY_HIGH 或者 PRIORITY_MAX
高,發出聲音 IMPORTANCE_DEFAULT PRIORITY_DEFAULT
中,沒有聲音 IMPORTANCE_LOW PRIORITY_LOW
低,沒有聲音並且不會出現在狀態欄中,並且通知會被摺疊 IMPORTANCE_MIN PRIORITY_MIN

創建基本通知

基本通知包含小圖標、標題和少量內容:

// 從 Android8.0 開始,必須設置 Channel ID
new NotificationCompat.Builder(context, CHANNEL_ID)
        .setSmallIcon(icon)
        .setContentTitle(title)
        .setContentText(content)
        // 設置優先級,優先級決定了 Android7.1 及更低版本通知的侵入程度
        .setPriority(NotificationCompat.PRIORITY_DEFAULT)
        .build();

大文本樣式通知

new NotificationCompat.Builder(context, channelId)
        .setSmallIcon(icon)
        .setContentTitle(title)
        .setContentText(content)
        .setPriority(priority)
        .setStyle(new NotificationCompat.BigTextStyle())
        .build();

帶圖標樣式通知

new NotificationCompat.Builder(context, channelId)
        .setSmallIcon(icon)
        .setContentTitle(title)
        .setContentText(content)
        .setPriority(priority)
        .setLargeIcon(BitmapFactory.decodeResource(context.getResources(), R.mipmap.ic_launcher))
        .build();

大圖標樣式通知

Bitmap iconBitmap = BitmapFactory.decodeResource(context.getResources(), R.mipmap.ic_launcher);
Bitmap avatarBitmap = BitmapFactory.decodeResource(context.getResources(), R.mipmap.ic_avatar);
return new NotificationCompat.Builder(context, channelId)
        .setSmallIcon(icon)
        .setContentTitle(title)
        .setContentText(content)
        .setPriority(priority)
        .setLargeIcon(iconBitmap)
        .setStyle(new NotificationCompat.BigPictureStyle()
                .bigPicture(avatarBitmap))
        .build();

響應用戶點擊

可以通過設置 setContentIntent()方法來定義當用戶點擊通知後所執行操作:

Intent intent = new Intent(context, NotificationDetailActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
new NotificationCompat.Builder(context, channelId)
        .setSmallIcon(icon)
        .setContentTitle(title)
        .setContentText(content)
        .setPriority(priority)
        // 點擊後自動移除通知
        .setAutoCancel(true)
        // 設置點擊後響應的 PendingIntent
        .setContentIntent(pendingIntent)
        .build();

也可以通過 setDeleteIntent()方法設置當用戶移除通知後所執行的操作。

另外還可以通過 setFullScreenIntent()方法設置「橫幅通知」:

在這裏插入圖片描述

當通知具有較高的優先級並使用鈴聲或震動時,也可能會觸發「橫幅通知」。

該通知用於極高優先級的通知,會直接以橫幅的形式顯示給用戶。如果是普通通知,不建議使用該方法,會打擾到用戶。另外實際測試中,發現在某些廠商的定製 Rom,如華爲 EMUI,需要在通知設置中打開橫幅通知設置纔行。另外如果應用定位於 AndroidQ 或以上,需要在 Manifest 中配置 Manifest.permission.USE_FULL_SCREEN_INTENT權限才能使用。

添加操作按鈕

通知最多可添加三個按鈕,用於快速響應用戶的操作。這些按鈕點擊後的響應操作不應該與用戶點擊通知所響應的操作一樣。

Intent intent = new Intent(context, NotificationDetailActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);

Intent intentReceiver = new Intent(context, NotificationReceiver.class);
intentReceiver.setAction(NotificationReceiver.ACTION);
PendingIntent pendingIntentReceiver = PendingIntent.getBroadcast(context, 0, intentReceiver, 0);

new NotificationCompat.Builder(context, channelId)
        .setSmallIcon(icon)
        .setContentTitle(title)
        .setContentText(content)
        .setPriority(priority)
        .setContentIntent(pendingIntent)
        // 添加操作按鈕(最多可添加三個)
        .addAction(android.R.drawable.sym_action_chat, "消息", pendingIntentReceiver)
        .build();

顯示效果示例:
在這裏插入圖片描述

添加直接回復操作

從 Android7.0 (API 24)開始,用戶可以直接在通知欄回覆消息。這對於一些 IM 應用來說很有幫助:

// 創建廣播,用於接收並處理用戶回覆的信息
Intent intent = new Intent(context, HandlerReplyMessageReceiver.class);
intent.setAction(HandlerReplyMessageReceiver.ACTION_HANDLER_REPLY_MESSAGE);

// 創建 PaddingIntent
int requestCode = 1; // 這裏應該是當前會話 ID
PendingIntent pendingIntent = PendingIntent.getBroadcast(
        context, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT);

// 創建 RemoteInput
RemoteInput remoteInput = new RemoteInput.Builder(HandlerReplyMessageReceiver.KEY_REPLY_TEXT)
        .setLabel("回覆")
        .build();

// 創建 action
NotificationCompat.Action action = new NotificationCompat.Action.Builder(android.R.drawable.sym_action_email, "回覆", pendingIntent)
        .addRemoteInput(remoteInput)
        .build();

new NotificationCompat.Builder(context, channelId)
        .setSmallIcon(icon)
        .setContentTitle(title)
        .setContentText(content)
        .setPriority(priority)
        .addAction(action)
        .build();

在廣播中接收用戶消息並處理:

public class HandlerReplyMessageReceiver extends BroadcastReceiver {

    public static final String ACTION_HANDLER_REPLY_MESSAGE = "action_handler_reply_message";

    public static final String KEY_REPLY_TEXT = "key_reply_text";

    @Override
    public void onReceive(Context context, Intent intent) {
        if (!ACTION_HANDLER_REPLY_MESSAGE.equals(intent.getAction())) {
            return;
        }

        String replyMessage = getReplyMessage(intent);
        Log.d(MainActivity.TAG, "回覆消息: " + replyMessage);
        Toast.makeText(context, "回覆消息: " + replyMessage, Toast.LENGTH_SHORT).show();

        // 處理完用戶回覆的消息後,應更新通知,否則發送按鈕會一直轉圈
        MyNotificationManager.getInstance().cancel(7); // 這裏的處理是直接不顯示該條通知
    }

    // 獲取用戶在通知欄輸入的回覆消息
    private String getReplyMessage(Intent intent) {
        Bundle resultsFromIntent = RemoteInput.getResultsFromIntent(intent);
        if (resultsFromIntent != null) {
            return resultsFromIntent.getString(KEY_REPLY_TEXT);
        }
        return null;
    }

}

進度條通知

進度條通知可以分爲兩種,一種是知道確切進度的,一種是不確定進度的。

當知道確切的進度時:

int icon = android.R.drawable.stat_notify_sync;
String title = "有進度的進度條通知";
String content = "當知道確切進度時使用";

// 先創建基本的通知
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID)
        .setSmallIcon(icon)
        .setContentTitle(title)
        .setContentText(content)
        .setPriority(NotificationCompat.PRIORITY_LOW);

// 應該在非主線程中更新進度,爲了演示方便,這裏直接新起一個子線程
new Thread(() -> {
    int max = 100;
    int progress = 0;
    while (progress < max) {
        progress ++;
        builder.setProgress(max, progress, false);
        mNotificationManager.notify(8, builder.build());
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    // 執行完畢,刪除進度條
    builder.setContentText("任務執行完畢");
    builder.setProgress(0, 0, false);
    mNotificationManager.notify(8, builder.build());
}).start();

不知道確定進度時:

int icon = android.R.drawable.stat_notify_sync;
String title = "不確定的進度條通知";
String content = "當不知道確切進度時使用";

// 先創建基本的通知
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID)
        .setSmallIcon(icon)
        .setContentTitle(title)
        .setContentText(content)
        .setPriority(NotificationCompat.PRIORITY_DEFAULT);

// 設置爲不確定進度
builder.setProgress(0, 0, true);
mNotificationManager.notify(9, builder.build());

// 模擬一段時間後任務執行完畢
new Handler().postDelayed(() -> {
    // 刪除進度條
    builder.setContentText("任務執行完畢");
    builder.setProgress(0, 0, false);
    mNotificationManager.notify(9, builder.build());
}, 5000);

設置分類

Android 爲通知預置了一些分類。當你的通知設置了重要性較高的分類,那麼即使用戶設置了「勿擾模式」,用戶也能收到通知,避免錯過重要消息(實際測試中,沒發現設置前後有啥區別,求解惑):

new NotificationCompat.Builder(context, channelId)
        .setSmallIcon(icon)
        .setContentTitle(title)
        .setContentText(content)
        .setPriority(priority)
        .setCategory(category)
        .build();

有以下分類:

  • CATEGORY_ALARM:鬧鐘或定時器。
  • CATEGORY_CALL:來電(語音或視頻)或類似的同步通信請求。
  • CATEGORY_EMAIL:異步批量消息(電子郵件)。
  • CATEGORY_ERROR:後臺操作或身份驗證狀態中的錯誤。
  • CATEGORY_EVENT:日曆事件。
  • CATEGORY_MESSAGE:傳入的直接消息(短信,即時消息等)。
  • CATEGORY_NAVIGATION:地圖轉彎導航。
  • CATEGORY_PROGRESS:長時間運行的後臺操作的進度。
  • CATEGORY_PROMO:促銷或廣告。
  • CATEGORY_RECOMMENDATION:針對單個事物的特定,及時的建議。例如,新聞應用可能想要推薦它認爲用戶下次要閱讀的新聞報道。
  • CATEGORY_REMINDER:用戶預定提醒。
  • CATEGORY_SERVICE:運行後臺服務的指示。
  • CATEGORY_SOCIAL:社交網絡或共享更新。
  • CATEGORY_STATUS:有關設備或上下文狀態的持續信息。
  • CATEGORY_SYSTEM:系統或設備狀態更新。保留供系統使用。
  • CATEGORY_TRANSPORT:用於播放的媒體傳輸控制。

設置鎖屏下通知可見性

通過 setVisibility()方法可以設置在鎖屏下通知的可見性。有三種:

  • VISIBILITY_PUBLIC:顯示完整的通知。
  • VISIBILITY_SECRET:在鎖定屏幕上不顯示通知的任何信息。
  • VISIBILITY_PRIVATE:只顯示基本信息,例如通知的圖標和內容標題,但隱藏通知內容。
new NotificationCompat.Builder(context, channelId)
        .setSmallIcon(icon)
        .setContentTitle(title)
        .setContentText(content)
        .setPriority(priority)
        .setVisibility(visibility)
        .build();

我在實際測試中,發現無論是使用原生系統(模擬器)還是第三方 ROM 系統。都沒有任何效果。是我哪裏姿勢不對嗎?

通知導航

當用戶通過通知進入了應用內,此時用戶點擊返回鍵,根據場景的不同結果也會不同。可能是直接退出應用,也可能是回到應用主頁。

針對這兩種情況有不同的實現方式,直接退出應用:

Intent intent = new Intent(context, NotificationDetailActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);

回到主頁:

Intent intent = new Intent(context, NotificationDetailActivity.class);

TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(context);
// 添加到後退棧中
taskStackBuilder.addParentStack(NotificationDetailActivity.class);
// 添加到堆棧頂部
taskStackBuilder.addNextIntent(intent);

PendingIntent pendingIntent = taskStackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);

new NotificationCompat.Builder(context, channelId)
        .setSmallIcon(icon)
        .setContentTitle(title)
        .setContentText(content)
        .setPriority(priority)
        .setStyle(new NotificationCompat.BigTextStyle())
        .setContentIntent(pendingIntent)
        .build();

manifest中需要配置 parentActivityName

<activity
        android:name=".activity.NotificationDetailActivity"
        android:parentActivityName=".activity.MainActivity"/>

實際測試中發現,回到首頁該方法在個別手機上會無效。因此最好不要完全依賴該方法。

自定義通知

通知的內容高度,普通通知限制爲 64dp,擴展通知限制爲 256dp。

自定義通知內容佈局

RemoteViews smallRemoteViews = new RemoteViews(context.getPackageName(), R.layout.notification_small);
smallRemoteViews.setTextViewText(R.id.notification_title, title);

Intent intent = new Intent(context, NotificationDetailActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
smallRemoteViews.setOnClickPendingIntent(R.id.notification_btn, pendingIntent);

new NotificationCompat.Builder(context, channelId)
        .setSmallIcon(icon)
        .setPriority(priority)
        .setStyle(new NotificationCompat.DecoratedCustomViewStyle())
        .setCustomContentView(smallRemoteViews)
        .build();

佈局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center">

    <TextView
            android:id="@+id/notification_title"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            tools:text="標題"
            style="@style/TextAppearance.Compat.Notification.Title"/>

    <Button
            android:id="@+id/notification_btn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Button"/>

</LinearLayout>

注意:由於在不同的設備上,通知的背景都可能會不同。爲了避免文字顏色與背景顏色相似導致看不清文字,最好如上面示例中一樣,使用系統中的 style 樣式。系統還有其他的 notification style,大家可以試一試。

運行效果:
在這裏插入圖片描述

完全自定義通知

RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.notification_big);
remoteViews.setTextViewText(R.id.notification, content);

new NotificationCompat.Builder(context, channelId)
        .setSmallIcon(icon)
        .setPriority(priority)
        .setCustomBigContentView(remoteViews)
        .build();
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center_vertical"
        android:padding="16dp">

    <TextView
            android:id="@+id/notification"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            tools:text="內容"
            style="@style/TextAppearance.Compat.Notification.Info"/>

</LinearLayout>

在這裏插入圖片描述

通知 Badge

官方文檔上說 8.0 開始,顯示通知時可以同時顯示 Badge。但我實際測試結果發現,只在原生系統上有效。國內廠商應該是都沒有實現,比如在華爲手機上就沒有 Badge,想顯示 Badge 只能用華爲自己提供的 API 實現。所以說該功能比較雞肋,暫時不談,看以後都兼容了再說吧。

顯示、更新通知

顯示和更新通知都使用的是 notify()方法:

notify(int id, @NonNull Notification notification)

更新時只需要保證 id 相同即可。

通過 setOnlyAlertOnce()方法可以設置只在第一次時提醒,更新時不再發出聲音和震動。

需要注意的是,不要太頻繁的更新,比如說一秒內多次,系統可能會刪除一些更新。

取消通知

應用主動取消通知有以下方法:

  • cancel():取消指定 ID 的通知。
  • cancelAll():取消當前應用所有顯示的通知。
  • setTimeoutAfter():在指定的時間後取消通知。

注意事項

  • 從 Android 8.1 開始,如果在 1 秒內發送了多個通知,那麼通知提醒音只會響一次。

參考文檔

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章