Android Notifications 百发百中之第一发

Notification 是 Android 系统中一个特别特别重要的机制,它可以让你在不打开 app 的情况下就可以便捷的查看消息、新闻、通知等等。

我个人认为 Android 比 iOS 好用的一个很重要的地方就是通知,嗯,一定是这样的。

比如下图:

当一个app收到推送通知后,会在状态栏显示该 app 的 logo,这样当你过段时间查看手机的时候不会遗漏重要的消息。

  • 微信:别人给你发消息,你不用打开微信就可以看到,还可以直接回复(当然了,微信没有去实现这个功能);
  • 网易新闻:通知栏可以直接看到新闻标题和一部分内容,如果你不关心详情的话,看看通知栏就可以了;
  • 豆瓣:会给你推送觉得你感兴趣的东西,如果你想看直接左滑或右滑删除,想看的话点击打开就可以;
  • 网易云音乐:常驻通知栏,你可以看到当前正在播放的歌曲,可以点击暂停、下一首、上一首等等。

Notification 主要样式

样式 描述
Normal 标准通知,显示标题和单行内容
BigText 多行文字,显示标题和多行内容
BigPicture 显示文字和图片,显示标题、内容和图片
Inbox 邮件,显示标题和多行邮件内容
Messaging 消息,显示和朋友的对话内容
Media 音乐播放器,显示常驻通知栏,可以执行各种操作
Custom 自定义,自定义 layout 的通知栏

Normal Notification

图中的1是 small icon,通过下列方法来设置:

    builder.setSmallIcon(R.drawable.small_icon);

图中的2是 app name,通过下列方法来设置:

    builder.setTicker(context.getString(R.string.app_name));

图中的3是 show time,通过下列方法来设置:

    builder.setWhen(System.currentTimeMillis());

图中的4是 content title,通过下列方法来设置:

    builder.setContentTitle("标题");

图中的5是 content text,通过下列方法来设置:

    builder.setContentText("内容");

图中的6是 large icon,通过下列方法来设置:

    builder.setLargeIcon(BitmapFactory.decodeResource(context.getResources(), R.drawable.large_icon));

下面是创建一个标准的通知栏的代码:

        Intent intent = new Intent(Intent.ACTION_MAIN);
        intent.addCategory(Intent.CATEGORY_LAUNCHER);
        intent.setClass(context, MainActivity.class);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK 
                        | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);

        PendingIntent pendingIntent = PendingIntent.getActivity(context
                        , (int) SystemClock.uptimeMillis()
                        , intent
                        , PendingIntent.FLAG_UPDATE_CURRENT);

        NotificationCompat.Builder builder = new NotificationCompat.Builder(context);

        //设置通知栏大图标,上图中右边的大图
        builder.setLargeIcon(BitmapFactory.decodeResource(
                        context.getResources(), R.drawable.large_icon))
                // 设置状态栏和通知栏小图标
                .setSmallIcon(R.drawable.small_icon)
                // 设置通知栏应用名称
                .setTicker(context.getString(R.string.app_name))
                // 设置通知栏显示时间
                .setWhen(System.currentTimeMillis())
                // 设置通知栏标题
                .setContentTitle("标题")
                // 设置通知栏内容
                .setContentText("内容")
                // 设置通知栏点击后是否清除,设置为true,当点击此通知栏后,它会自动消失
                .setAutoCancel(true)
                // 将Ongoing设为true 那么左滑右滑将不能删除通知栏  
                //.setOngoing(true);  
                // 设置通知栏点击意图
                .setContentIntent(pendingIntent);
                // 铃声、闪光、震动均系统默认
                .setDefaults(Notification.DEFAULT_ALL);
                // 设置为public后,通知栏将在锁屏界面显示
                .setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
                // 从Android4.1开始,可以通过以下方法,设置通知栏的优先级,优先级越高的通知排的越靠前,
                // 优先级低的,不会在手机最顶部的状态栏显示图标
                // 设置优先级为PRIORITY_MAX,将会在手机顶部显示通知栏
                .setPriority(NotificationCompat.PRIORITY_MAX);

        NotificationManager noti = (NotificationManager)
                     context.getSystemService(Context.NOTIFICATION_SERVICE);
        noti.notify((int) System.currentTimeMillis(), builder.build());

点击消失

如果你想点击通知后,该通知自动消失,那么你就需要调用 setAutoCancel(boolean b) 这个方法,并将其设置为 true;

设置铃声、震动、闪光效果

setDefaults(Notification.DEFAULT_ALL) 表示系统系统默认的铃声、震动和闪光效果。举例:
自定义通知铃声:

  • 注释 setDefaults(Notification.DEFAULT_ALL) 方法;
  • 调用 builder.setSound(Uri sound);
  • 你还可以通过 setVibrate 和 setLights 设置震动和闪光效果

锁屏显示

如上图所示,如果你想在锁屏情况下显示通知,则需要调用 setVisibility(int visibility),并将其设置为NotificationCompat.VISIBILITY_PUBLIC

头部显示

如上图所示,如果你想通知显示在手机头部,比如来电时的通知,那么你就需要调用 setPriority(int pri),并将其设置为NotificationCompat.PRIORITY_MAX

禁止删除

如果你想让你的通知栏常驻,用户无法滑动删除,也不能通过手机的清除键删除,类似于网易云音乐等 app 的通知栏,那么你可以设置 setOngoing 方法,也设为 true,这样,通知栏只能通过代码调用 cancel 方法才能消失;

通知点击事件

使用 build.setContentIntent(PendingIntent intent) 设置点击事件。
先看官方的代码吧:

        /**
         * Supply a {@link PendingIntent} to send when the notification is clicked.
         * If you do not supply an intent, you can now add PendingIntents to individual
         * views to be launched when clicked by calling {@link RemoteViews#setOnClickPendingIntent
         * RemoteViews.setOnClickPendingIntent(int,PendingIntent)}.  Be sure to
         * read {@link Notification#contentIntent Notification.contentIntent} for
         * how to correctly use this.
         */
        public Builder setContentIntent(PendingIntent intent) {
            mContentIntent = intent;
            return this;
        }

从代码注释可以看出,当通知被点击的时候,系统会发送一个 PendingIntent。
从字面上看,PendingIntent:等待的,未决定的Intent。

要获取 PendingIntent 对象,有以下静态方法:

  • getActivity(Context context, int requestCode, Intent intent, int flags)
  • getActivities(Context context, int requestCode, Intent[] intents, int flags)
  • getBroadcast(Context context, int requestCode, Intent intent, int flags)
  • getService(Context context, int requestCode, Intent intent, int flags)

分别对应着 Intent 的3个行为,跳转到一个或几个 activity 组件、打开一个广播组件和打开一个服务组件。
PendingIntent 是一种特殊的 Intent。主要的区别在于 Intent 的执行立刻的,而 PendingIntent 的执行不是立刻的。
PendingIntent 执行的操作实质上是参数传进来的 Intent 的操作,但是使用 PendingIntent 的目的在于它所包含的 Intent 的操作的执行是需要满足某些条件的。

一般情况下,点击通知都是打开特定的 Activity,你可以直接使用 getActivity 或 getActivities,也可以使用 getBroadcast 或 getService,然后在广播或服务中做一些启动 Activity 的操作。

所有的 app 点击通知栏启动 Activity 的方式不外乎以下三种:

  • 如果要启动的 app 已经被杀掉,则启动主界面,否则启动栈顶的 Activity,使用方式如下:
        Intent intent = new Intent(Intent.ACTION_MAIN);

        intent.addCategory(Intent.CATEGORY_LAUNCHER);
        intent.setClass(context, MainActivity.class);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK 
                        | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);

        PendingIntent pendingIntent = PendingIntent.getActivity(context
                        , (int) SystemClock.uptimeMillis()
                        , intent
                        , PendingIntent.FLAG_UPDATE_CURRENT);
  • 启动特定的 Activity,不管当前 app 有没有被杀掉,点击后退键后返回到当前 app 的主界面,例如微信、QQ等等,使用方式有两种:

使用 TaskStackBuilder

        Intent intent = new Intent();

        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK 
                        | Intent.FLAG_ACTIVITY_SINGLE_TOP 
                        |  Intent.FLAG_ACTIVITY_CLEAR_TOP);
        intent.setClass(context, ThirdActivity.class);

        TaskStackBuilder stackBuilder = TaskStackBuilder.create(context);
        stackBuilder.addParentStack(ThirdActivity.class);
        stackBuilder.addNextIntent(intent);

        PendingIntent pendingIntent = stackBuilder.getPendingIntent(
                        (int) SystemClock.uptimeMillis()
                        , PendingIntent.FLAG_UPDATE_CURRENT);

使用 getActivities

        Intent intent = new Intent(context, MainActivity.class);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK 
                        | Intent.FLAG_ACTIVITY_SINGLE_TOP 
                        | Intent.FLAG_ACTIVITY_CLEAR_TOP);
        Intent secondIntent = new Intent(context, SecondActivity.class);
        Intent[] intents = {intent, secondIntent};
        PendingIntent pendingIntent = PendingIntent.getActivities(context
                        , (int) SystemClock.uptimeMillis()
                        , intents
                        , PendingIntent.FLAG_UPDATE_CURRENT);
  • 启动特定的 Activity,启动前判断,如果当前程序没有被杀掉且除了当前被启动的 Activity 外至少有一个 Activity 没有被 finish,点击后退键则返回到之前栈顶的 Activity;如果当前程序被杀掉或之前任务栈没有 Activity,点击后退键则返回到当前 app 的主界面,例如网易云音乐。
        if (Util.isAppAlive(context, BuildConfig.APPLICATION_ID) 
            && !DemoApplication.getInstance().isAllActivityFinished()) {
            Intent intent = new Intent();
            intent.setClass(context, ThirdActivity.class);
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK 
                            | Intent.FLAG_ACTIVITY_CLEAR_TOP);
            startActivity(intent);
        } else {
            Intent intent = new Intent(context, MainActivity.class);
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK 
                            | Intent.FLAG_ACTIVITY_SINGLE_TOP 
                            | Intent.FLAG_ACTIVITY_CLEAR_TOP);
            Intent secondIntent = new Intent(context, SecondActivity.class);
            Intent[] intents = {intent, secondIntent};
            startActivities(intents);
        }
获取 PendingIntent 对象的第四个参数为 flags,一般 flags 有5种选择:
  • FLAG_ONE_SHOT:表明这里构建的 PendingIntent 只能使用一次,使用后将被自动删除,再次使用将会报错;
  • FLAG_NO_CREATE:如果前一个 PendingIntent 已经不存在了,将不再构建它,直接返回 null;
  • FLAG_CANCEL_CURRENT:如果构建的 PendingIntent 已经存在,则取消前一个,重新构建一个;
  • FLAG_UPDATE_CURRENT:如果构建的PendingIntent已经存在,那么系统将不会重复创建,只是将他的 intent 中的参数替换;
  • FLAG_IMMUTABLE 表示这是一个不可变的 PendingIntent。

通常情况下,我们会使用 FLAG_UPDATE_CURRENT 这个 flags 值来构造 PendingIntent,但是使用 FLAG_UPDATE_CURRENT 经常会发生点击通知栏后没有任何响应,时灵时不灵。

原来使用 FLAG_UPDATE_CURRENT 这个参数后,系统不会重新创建新的 PendingIntent,这样一来,如果你传递的 Intent 的 extra 参数没有变化的话,那么系统就会认为你没有发送新的 PendingIntent,这样就不会重新响应你的点击事件。一般情况下,为了能够区分每次的 PendingIntent 不一样,我们常常会在构造 Intent 的时候,设置不同的 Action 或 Extra 值,这样一来,即便是使用 FLAG_UPDATE_CURRENT 这个参数,系统也会因为传值参数的变化而去响应每次的点击事件。不过这种解决方法还是很坑爹的,大部分时候我们根本不需要传递额外的 Action 或 Extra 值,这个时候我们只需要把

PendingIntent.getActivity(context, requestCode, intent, flags)

这个方法中的第二个参数(requestCode)设置成唯一的标识就可以了,可以直接使用

int requestCode = (int) SystemClock.uptimeMillis();

当然你可以直接使用 FLAG_CANCEL_CURRENT 这个 flag,每次都会创建一个新的,那么上面的问题就不存在了。

注:判断 APP 是否被杀死和所有的 Activity 是否都被杀死,详见这篇 BlogDemo

通知栏添加 Action

通过添加 Action,用户可以不用打开 app 就可以直接对收到的信息就行操作,比如回复短信、设置短信已读和删除短信等等。
是不是很方便?是不是比 iOS 厉害多了?

Android 提供了一个叫做 RemoteInput 的类,给通知栏的回复操作提供强有力的支持,用户点击回复后,直接在通知栏显示输入框,输入内容后点击发送即可。那怎么使用呢?直接看代码吧。

      public final static String REPLY = "reply";
      public final static String KEY_TEXT_REPLY = "key_text_reply";
      RemoteInput input = new RemoteInput.Builder(KEY_TEXT_REPLY).setLabel("请输入内容").build();

      Intent reply = new Intent();
      reply.setAction(REPLY);
      reply.setClass(context, NotificationReceiver.class);
      PendingIntent pendingReply = PendingIntent.getBroadcast(context
                      , (int) SystemClock.uptimeMillis()
                      , reply
                      , PendingIntent.FLAG_UPDATE_CURRENT);

      NotificationCompat.Action action = 
                      new NotificationCompat.Action.Builder(0, "回复", pendingReply)
                      .addRemoteInput(input)
                      .setAllowGeneratedReplies(true)
                      .build();

      builder.addAction(action);

这里使用了发送广播组件,你可以在接收到的广播中直接获取输入框输入的内容。

     case REPLY:{
          //获取输入框输入的内容
          Bundle input = RemoteInput.getResultsFromIntent(intent);
          if (input == null)
              return;
          //Do something
          String content = input.getCharSequence(KEY_TEXT_REPLY)
          Toast.makeText(context, content, Toast.LENGTH_SHORT).show();
          break;
    }

添加设置已读和删除操作

     Intent maskAsRead = new Intent();
     maskAsRead.setAction(MAKE_AS_READ);
     maskAsRead.setClass(context, NotificationReceiver.class);
     PendingIntent pendingMaskAsRead = PendingIntent.getBroadcast(context, 
                     (int) SystemClock.uptimeMillis()
                     , maskAsRead
                     , PendingIntent.FLAG_UPDATE_CURRENT);

     Intent delete = new Intent();
     delete.setAction(DELETE);
     delete.setClass(context, NotificationReceiver.class);
     PendingIntent pendingDelete = PendingIntent.getBroadcast(context, 
                     (int) SystemClock.uptimeMillis()
                     , delete
                     , PendingIntent.FLAG_UPDATE_CURRENT);
     builder.addAction(0, "mask as read", pendingMaskAsRead);
     builder.addAction(0, "delete", pendingDelete);

同样的你可以在 Receiver 中获取你做了什么操作,下一步该做什么等等。

启动 Notification

启动 Notification 的方法很简单,如下所示:

    NotificationManager notificationManager 
            = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
    notificationManager.notify(ID, builder.build());

对于同一个 ID,后启动的 Notification 会替换先启动的 Notification ,所以:

对于类似于微信这样的聊天通知,你需要为每一个给你发消息的用户设置一个独一无二的 ID,比如可以根据用户 ID来生成;
对于类似于网易新闻这样的新闻通知,你需要为每个通知设置不同的独一无二的 ID,比如使用 (int) SystemClock.uptimeMillis();
如果你的通知栏只需要显示一个的话,只需要用同一个 ID 就可以;
对于需要代码清除的 Notification,你需要为 ID 设置一个固定的独一无二的值,比如短信通知,你点了删除操作,删除短信后你就需要清除对应的 Notification

清除 Notification

两种方式:

根据 ID 清除特定的 Notification:

notificationManager.cancel(id);

清除该 app 所有的 Notification:

notificationManager.cancelAll();

详细的代码请参见GitHub

敬请期待 Android Notifications 百发百中之第二发

Smile Wei wechat
请扫码关注我的微信公众号