Android 特殊用户通知用法汇总 - Notification 源码分析

  一直用的android手机,用过这么多的app,平时也会遇到有趣的通知提醒,在这里先总结两种吧,notification和图标数字,有的之后看到再研究。还有,推广一下哈,刚刚创建一个Q群544645972,有兴趣的加一下,一块儿成长。html

Notification

  Notification应该算是最多见的app通知方式了,网上资料也不少,各类使用方法官方文档也已经写的很是详细了:developer.android.com/intl/zh-cn/…。这里就介绍一下几种特殊的用法:java

动态改变

  将一个notification的setOngoing属性设置为true以后,notification就可以一直停留在系统的通知栏直到cancel或者应用退出。因此有的时候须要实时去根据情景动态改变notification,这里以一个定时器的功能为例,须要每隔1s去更新一下notification,具体效果:

  这里写图片描述

  很是简单的功能,代码也很简单:android

private Timer timer;
private TimerTask task;
...
if (timer != null)
    return;
timer = new Timer("time");
task = new TimerTask() {
    @Override
    public void run() {
        showDynamicNotification();
    }
};
timer.scheduleAtFixedRate(task, 0, 1000);

private void showDynamicNotification() {
    L.i("show dynamic notification");
    mBuilder = new NotificationCompat.Builder(NotificationActivity.this);
    RemoteViews view = new RemoteViews(getPackageName(), R.layout.layout_notification);
    view.setTextViewText(R.id.tv_number, parseDate());
    view.setImageViewResource(R.id.iv_icon, R.mipmap.ic_launcher);

    Intent intent = new Intent(NOTIFY_ACTION);
    PendingIntent pendingIntent = PendingIntent.getBroadcast(NotificationActivity.this,
            1000, intent, PendingIntent.FLAG_UPDATE_CURRENT);

    mBuilder.setSmallIcon(R.mipmap.ic_launcher)
            .setContentIntent(pendingIntent)
            .setTicker("you got a new message")
            .setOngoing(true)
            .setContent(view);
    notification = mBuilder.build();
    notificationManager.notify(NOTIFY_ID2, notification);
}

private String parseDate() {
    SimpleDateFormat format = new SimpleDateFormat("yyyy hh:mm:ss", Locale.getDefault());
    return format.format(System.currentTimeMillis());
}复制代码

  须要注意的是Notification.Builder 是 Android 3.0 (API 11) 引入的,为了兼容低版本,咱们通常使用 Support V4 包提供的 NotificationCompat.Builder 来构建 Notification。要想动态更新notification,须要利用 NotificationManager.notify() 的 id 参数,该 id 在应用内须要惟一(若是不惟一,在有些4.x的手机上会出现pendingIntent没法响应的问题,在红米手机上出现过相似状况),要想更新特定 id 的通知,只须要建立新的 notification,并触发与以前所用 id 相同的 notification,若是以前的通知仍然可见,则系统会根据新notification 对象的内容更新该通知,相反,若是以前的通知已被清除,系统则会建立一个新通知。

  在这个例子中使用的是彻底自定义的remoteViews,remoteViews和普通view的更新机制不同,网上资料不少,感兴趣的能够去仔细了解。还有一个就是PendingIntent,这就不详细介绍了,这里简单列一下PendingIntent的4个flag的做用git


  • FLAG_CANCEL_CURRENT:若是构建的PendingIntent已经存在,则取消前一个,从新构建一个。

  • FLAG_NO_CREATE:若是前一个PendingIntent已经不存在了,将再也不构建它。

  • FLAG_ONE_SHOT:代表这里构建的PendingIntent只能使用一次。

  • FLAG_UPDATE_CURRENT:若是构建的PendingIntent已经存在,则替换它,经常使用。

big view

  Notification有两种视觉风格,一种是标准视图(Normal view)、一种是大视图(Big view)。标准视图在Android中各版本是通用的,可是对于大视图而言,仅支持Android4.1+的版本,好比邮件,音乐等软件就会使用到这种大视图样式的扩展通知栏,系统提供了setStyle()函数用来设置大视图模式,通常状况下有三种模式提供选择:github


  • NotificationCompat.BigPictureStyle, 在细节部分显示一个256dp高度的位图

  • NotificationCompat.BigTextStyle,在细节部分显示一个大的文本块。

  • NotificationCompat.InboxStyle,在细节部分显示一段行文本。

      这里写图片描述

    在21版本以后增长了一个Notification.MediaStyle,这个能够达到相似

      这里写图片描述

    的效果,基本和一些主流媒体播放器的界面相似了,在这就不具体介绍了,提供一篇资料:controllingMedia

      若是不使用上面的几种style,彻底自定义布局也是能够的,例如实现:

      这里写图片描述

    相关源码:Android custom notification for music player Example

      在这我就以一个简单的实现为例,效果以下:

      这里写图片描述

    代码:

RemoteViews smallView = new RemoteViews(getPackageName(), R.layout.layout_notification)
smallView.setTextViewText(R.id.tv_number, parseDate())
smallView.setImageViewResource(R.id.iv_icon, R.mipmap.ic_launcher)

mBuilder = new NotificationCompat.Builder(NotificationActivity.this)
mBuilder.setSmallIcon(R.mipmap.ic_launcher)
        .setNumber((int) (Math.random() * 1000))
        //No longer displayed in the status bar as of API 21.
        .setTicker()
        .setDefaults(Notification.DEFAULT_SOUND
                | Notification.DEFAULT_VIBRATE | Notification.DEFAULT_LIGHTS)
//        .setDeleteIntent()
        .setAutoCancel(true)
        .setWhen(0)
        .setPriority(NotificationCompat.PRIORITY_LOW)

intent = new Intent(NOTIFY_ACTION)
pendingIntent = PendingIntent.getBroadcast(NotificationActivity.this,
        1000, intent, PendingIntent.FLAG_UPDATE_CURRENT)
mBuilder.setContentIntent(pendingIntent)

//在5.0版本以后,能够支持在锁屏界面显示notification
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){
    mBuilder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
}
notification = mBuilder.build()
notification.contentView = smallView

//若是系统版本 >= Android 4.1,设置大视图 RemoteViews
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
    RemoteViews view = new RemoteViews(getPackageName(), R.layout.layout_big_notification)
    view.setTextViewText(R.id.tv_name, )
    view.setOnClickPendingIntent(R.id.btn_click_close,
            PendingIntent.getBroadcast(NotificationActivity.this, 1001,
                    new Intent(CLICK_ACTION), PendingIntent.FLAG_UPDATE_CURRENT))
    //textview marquee property is useless for bigContentView
    notification.bigContentView = view
}

notificationManager.notify(NOTIFY_ID3, notification)复制代码

xml布局:api

<linearlayout android:background="#ef222222" android:layout_height="match_parent" android:layout_width="match_parent" android:orientation="horizontal" android:padding="10dp" xmlns:android="http://schemas.android.com/apk/res/android">

    <framelayout android:layout_height="150dp" android:layout_width="match_parent">

        <imageview android:background="@mipmap/ic_launcher" android:id="@+id/iv_icon" android:layout_gravity="center_vertical" android:layout_height="wrap_content" android:layout_width="wrap_content">

        <linearlayout android:layout_height="match_parent" android:layout_weight="1" android:layout_width="match_parent" android:orientation="vertical">


            <textview android:ellipsize="marquee" android:fadingedge="horizontal" android:focusable="true" android:focusableintouchmode="true" android:gravity="center_horizontal|center_vertical" android:id="@+id/tv_name" android:layout_gravity="center" android:layout_height="wrap_content" android:layout_width="fill_parent" android:marqueerepeatlimit="marquee_forever" android:scrollhorizontally="false" android:singleline="true" android:textcolor="#fff" android:textsize="15sp" android:textstyle="bold">
            <requestfocus>
            </requestfocus></textview><button android:background="#ef222222" android:id="@+id/btn_click_close" android:layout_gravity="center_horizontal" android:layout_height="40dp" android:layout_margintop="10dp" android:layout_width="40dp" android:text="X" android:textsize="30sp" type="submit"></button></linearlayout>
    </framelayout>

</imageview></linearlayout>复制代码

  这里有几点须要着重说明一下微信


  • setTicker函数在21版本以后已经Deprecated了,没有效果。

  • 监听notification删除函数setDeleteIntent是在11版本后新增的,并且setAutoCancel为true后,该函数会失效。

  • setPriority函数用来给notification设置优先级,上面给的google文档中有很详细的介绍。

  • 21版本以后,能够支持在锁屏界面显示notification,这个在google文档中也有介绍,这个体验对于我我的来讲感触很深,对于短信等私密性通知能够隐藏,可是对于通常毫无隐私的应用通知,就能够设置其为public,省去用户解锁,下拉通知栏的操做。

  • 自定义大图模式也是将自定义的RemoteViews赋值给notification.bigContentView变量,并且这个功能也只是在api16(4.1)以后生效。

  • 大图模式高度的设置有些奇怪,在上面的xml文件中,LinearLayout设置高度是无效的,必需要套一层FrameLayout,设置FrameLayout的高度才行,貌似定义最外层的LinearLayout的layoutParams是无效的。

  • 在bigContentView中是没法实现textview的marquee效果,并且事实也很奇怪,单独使用contentView,传入的remoteViews中的textview的marquee属性是好用的,可是一旦设置了bigContentView,contentView中的textview属性也失效了,这点使用的时候要注意。

浮动通知

  这种效果你们应该在微信中看的不少,其实实现也很简单:

  这里写图片描述

代码:app

RemoteViews headsUpView = new RemoteViews(getPackageName(), R.layout.layout_heads_up_notification)

intent = new Intent(NOTIFY_ACTION)
pendingIntent = PendingIntent.getBroadcast(NotificationActivity.this,
        1000, intent, PendingIntent.FLAG_UPDATE_CURRENT)
mBuilder = new NotificationCompat.Builder(NotificationActivity.this)
mBuilder.setSmallIcon(R.mipmap.ic_launcher)
        .setContentTitle()
        .setContentText()
        .setNumber((int) (Math.random() * 1000))
        .setTicker()
        //must set pendingintent for this notification, or will be crash
        .setContentIntent(pendingIntent)
        .setDefaults(Notification.DEFAULT_SOUND
                | Notification.DEFAULT_VIBRATE | Notification.DEFAULT_LIGHTS)
        .setAutoCancel(true)
        .setWhen(0)
notification = mBuilder.build()
if (Build.VERSION.SDK_INT >= 21) {
    notification.priority = Notification.PRIORITY_MAX
    notification.headsUpContentView = headsUpView
}
notificationManager.notify(NOTIFY_ID1, notification)复制代码

  这个效果很是的方便,用户都不须要直接下拉出通知栏,直接就可以看见,省去了多余操做,google官方文档介绍:developer.android.com/intl/zh-cn/…。headsUpContentView属性也只是在21版本时出现,使用的时候须要注意。less

notification常见问题总结

  1.经过notification打开activity的时候,就要涉及到保存用户导航的问题,这个时候就要使用到activity task的相关内容了,我之前写过一篇博客中有介绍到activity task的内容:android深刻解析Activity的launchMode启动模式,Intent Flag,taskAffinity,感兴趣的能够去看看。那么要实现点击notification打开指定activity,就须要设置相关的pendingIntent,有两种特殊的状况须要说明一下:  dom

  • 第一种是须要打开该activity的整个task栈,也就是说父activity也须要同时所有打开,并且按照次序排列在task栈中。
    可是这里会有一个问题,它在打开整个activity栈以前会先清空原先的activity task栈,因此最后在task栈中只剩下相关的几个activity,举个例子我要打开A->B->C的activity栈,可是我原先的activity栈中有D和C这两个activity,系统会直接按顺序关闭D和C这两个activity,接着按顺序打开A->B->C,这种状况在使用的时候须要注意。

  • 第二种是直接打开一个activity在一个单独的task栈中
    这种状况会生成两个task栈,
    这两种状况在google官方文档中已经详细介绍了:developer.android.com/intl/zh-cn/…

  2.我在之前的博客中曾经介绍过一个notification图标变成白块的问题:android5.0状态栏图标变成白色,这个问题在国产的不少rom中本身解决了,例如小米,锤子等,可是例如HTC和三星等rom仍然是有这样的问题,要重视起来啊

  3.列表内容在2.3的时候直接使用

RemoteViews rvMain = new RemoteViews(context.getPackageName(), R.layout.notification_layout);
//TODO rvMain...
NotificationCompat.Builder builder = new NotificationCompat.Builder(context)
                .setContent(rvMain);
// TOOD ...复制代码

  是无效的,须要换一种方式:

RemoteViews rvMain = new RemoteViews(context.getPackageName(), R.layout.notification_layout);
//TODO rmMain...
NotificationCompat.Builder builder = new NotificationCompat.Builder(context)
                .setContent(rvMain);
// TOOD ...
Notification notification = builder.build();
if(Build.VERSION.SDK_INT <= 10){
    notification.contentView = rvMain;
}复制代码

  4.通知栏上的操做事件:

  setContentIntent():用户点击通知时触发

  setFullScreenIntent()://TODO 这个在通知显示的时候会被调用

  setDeleteIntent():用户清除通知时触发,能够是点击清除按钮,也能够是左右滑动删除(固然了,前提是高版本)

  2.3及如下是没法处理自定义布局中的操做事件的,这样咱们就不要去考虑增长自定义按钮了。

notification源码解析

  分析一下源码,以NotificationManager.notify为入口进行分析:

INotificationManager service = getService()
......
service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id,
                    stripped, idOut, UserHandle.myUserId())复制代码

getService()函数:

static public INotificationManager getService()
{
    if (sService != null) {
        return sService;
    }
    IBinder b = ServiceManager.getService();
    sService = INotificationManager.Stub.asInterface(b);
    return sService;
}复制代码

该函数经过ServiceManager.getService(“notification”)获取了INotificationManager的Binder对象,用来进行跨进程通讯,Binder不太明白的能够看看我之前写的一篇博客:android IPC通讯(下)-AIDL。这里获取的Binder对象就是NotificationManagerService,这里涉及的两个类要介绍一下,StatusBarManagerService和NotificationManagerService,这两个service都会在frameworks/base/services/java/com/android/server/SystemServer.java文件里面进行启动的:

class ServerThread extends Thread {    
public void run() {  
......  
     StatusBarManagerService statusBar = null;  
     NotificationManagerService notification = null;  
......  
    statusBar = new StatusBarManagerService(context, wm);  
    ServiceManager.addService(Context.STATUS_BAR_SERVICE, statusBar);  
......  
    notification = new NotificationManagerService(context, statusBar, lights);                     
    ServiceManager.addService(Context.NOTIFICATION_SERVICE, notification);  
......  

   }  

} 复制代码

我在早期的博客中介绍过SystemServer,system_server子进程是zygote经过forkSystemServer函数建立的,感兴趣的能够去看看android启动过程详细讲解。上面的代码就调用到了NotificationManagerService的enqueueNotificationWithTag方法,enqueueNotificationWithTag方法会调用到enqueueNotificationInternal方法,这个方法就是核心了,咱们抽取其中比较重要的代码分析一下:

void enqueueNotificationInternal(final String pkg, final String opPkg, final int callingUid,
            final int callingPid, final String tag, final int id, final Notification notification,
            int[] idOut, int incomingUserId) {
    ...
    //------这里会作一个限制,除了系统级别的应用以外,其余应用的notification数量会作限制,
    //------用来放置DOS攻击致使的泄露
    // Limit the number of notifications that any given package except the android
    // package or a registered listener can enqueue. Prevents DOS attacks and deals with leaks.
    ...
    //------post到工做handler中进行工做
    mHandler.post(new Runnable() {
        @Override
        public void run() {

            synchronized (mNotificationList) {

                // === Scoring ===

                //------审查参数priority
                // 0. Sanitize inputs
                notification.priority = clamp(notification.priority, Notification.PRIORITY_MIN,
                        Notification.PRIORITY_MAX);
                .....

                //------初始化score
                // 1. initial score: buckets of 10, around the app [-20..20]
                final int score = notification.priority * NOTIFICATION_PRIORITY_MULTIPLIER;

                //------将前面传递进来的Notification封装成一个StatusBarNotification对象,而后
                //------和score封装成一个NotificationRecord对象,接着会调用handleGroupedNotificationLocked
                //------方法,看可否跳过下一步操做,额外的会对downloadManager进行单独处理
                // 2. extract ranking signals from the notification data
                .....

                //------主要是统计notification的各类行为,另外将该上面封装好的NotificationRecord对象
                //------加入到mNotificationList中,而后排序,排序外后,若是notification设置了smallIcon,
                //------调用全部NotificationListeners的notifyPostedLocked方法,通知有新的notification,
                //------传入的参数为上面封装成的StatusBarNotification对象。
                // 3. Apply local rules

                .....
                mRankingHelper.sort(mNotificationList);

                if (notification.getSmallIcon() != null) {
                    StatusBarNotification oldSbn = (old != null) ? old.sbn : null;
                    mListeners.notifyPostedLocked(n, oldSbn);
                } else {
                    ......
                }
                //通知status bar显示该notification
                buzzBeepBlinkLocked(r);
            }
        }
    });
}复制代码

notifyPostedLocked方法中会继续post到工做handler中,在该工做handler中调用notifyPosted方法,notifyPosted方法很简单,也是经过Binder调用到了NotificationListenerService中,这个NotificationListenerService中类很实用,它能够继承,用来监听系统notification的各类动做:Android 4.4 KitKat NotificationManagerService使用详解与原理分析(一)__使用详解。通知完成,最后异步操做就是调用buzzBeepBlinkLocked()方法去显示该notification了,这个函数也很长,可是职责很明确,确认是否须要声音,震动和闪光,若是须要,那么就发出声音,震动和闪光:

private void buzzBeepBlinkLocked(NotificationRecord record) {
    .....

    // Should this notification make noise, vibe, or use the LED?
    ......

    // If we're not supposed to beep, vibrate, etc. then don't.
    .....
    if (disableEffects == null
            && (!(record.isUpdate
            && (notification.flags & Notification.FLAG_ONLY_ALERT_ONCE) != 0 ))
            && (record.getUserId() == UserHandle.USER_ALL ||
            record.getUserId() == currentUser ||
            mUserProfiles.isCurrentProfile(record.getUserId()))
            && canInterrupt
            && mSystemReady
            && mAudioManager != null) {
        if (DBG) Slog.v(TAG, "Interrupting!");

        sendAccessibilityEvent(notification, record.sbn.getPackageName());

        // sound

        // should we use the default notification sound? (indicated either by
        // DEFAULT_SOUND or because notification.sound is pointing at
        // Settings.System.NOTIFICATION_SOUND)
        .....

        // vibrate
        // Does the notification want to specify its own vibration?
        ....

    // light
    ....
    if (buzz || beep || blink) {
        EventLogTags.writeNotificationAlert(record.getKey(),
                buzz ? 1 : 0, beep ? 1 : 0, blink ? 1 : 0);
        mHandler.post(mBuzzBeepBlinked);
    }
}复制代码

最后将mBuzzBeepBlinked post到工做handler,最后会调用到mStatusBar.buzzBeepBlinked(),mStatusBar是StatusBarManagerInternal对象,这个对象是在StatusBarManagerService中初始化,因此最后调用到了StatusBarManagerService中StatusBarManagerInternal的buzzBeepBlinked()方法:

public void buzzBeepBlinked() {
    if (mBar != null) {
        try {
            mBar.buzzBeepBlinked();
        } catch (RemoteException ex) {
        }
    }
}复制代码

mBar是一个IStatusBar对象,这个mBar在哪里赋值的呢?看这里:www.programering.com/a/MTOzITNwA…,英文看不懂不要紧,有中文版:home.bdqn.cn/thread-4215…。因此最终调用到了CommandQueue类中,接着sendEmptyMessage给了内部的H类(貌似很喜欢用H这个单词做为Handler的命名,好比acitivity的启动:android 不能在子线程中更新ui的讨论和分析),接着调用了mCallbacks.buzzBeepBlinked()方法,这个mCallbacks就是BaseStatusBar,最终会将notification绘制出来,到这里一个notification就算是完成了。

  注:我分析代码的时候看的代码是最新版本的api 23代码,buzzBeepBlinked()这个函数在BaseStatusBar类中是不存在的,绘制代码是在UpdateNotification()函数中,可是BaseStatusBar分明是继承了CommandQueue.Callbacks接口,却没有实现它,因此这个buzzBeepBlinked()函数到最后就莫名其妙失踪了,求大神指点,很是疑惑。

相关资料


www.tutorialsface.com/2015/08/and…

developer.android.com/intl/zh-cn/…

glgjing.github.io/blog/2015/1…

www.codeceo.com/article/and…

www.itnose.net/detail/6169…

www.cnblogs.com/over140/p/4…

blog.csdn.net/loongggdroi…

www.2cto.com/kf/201408/3…

blog.csdn.net/xxbs2003/ar…

www.jianshu.com/p/4d76b2bc8…

home.bdqn.cn/thread-4215…


图标数字

  这里写图片描述

  虽说这是iOS上的风格,可是在某些手机上仍是支持的,好比三星和HTC(m8t,6.0)的有些手机均可以,小米手机是个特例,它是根据notification的数量来自动生成的。

  通常状况下,HTC和三星可使用下面的函数生成

public static void setBadge(Context context, int count) {
    String launcherClassName = getLauncherClassName(context);
    if (launcherClassName == null) {
        return;
    }
    Intent intent = new Intent();
    intent.putExtra(, count);
    intent.putExtra(, context.getPackageName());
    intent.putExtra(, launcherClassName);
    context.sendBroadcast(intent);
}

public static String getLauncherClassName(Context context) {

    PackageManager pm = context.getPackageManager();

    Intent intent = new Intent(Intent.ACTION_MAIN);
    intent.addCategory(Intent.CATEGORY_LAUNCHER);

    List resolveInfos = pm.queryIntentActivities(intent, 0);
    for (ResolveInfo resolveInfo : resolveInfos) {
        String pkgName = resolveInfo.activityInfo.applicationInfo.packageName;
        if (pkgName.equalsIgnoreCase(context.getPackageName())) {
            String className = resolveInfo.activityInfo.name;
            return className;
        }
    }
    return null;
}复制代码

  因为android碎片化太严重,因此在不一样手机上适配起来是很是麻烦,不过还好在github上国人写了一个库能够覆盖挺多机型:ShortcutBadger,也能够参考一下:stackoverflow.com/questions/1…

源码

github.com/zhaozepeng/…

相关文章
相关标签/搜索