一直用的android手機,用過這麼多的app,平時也會遇到有趣的通知提醒,在這裏先總結兩種吧,notification和圖標數字,有的之後看到再研究。還有,推廣一下哈,剛剛創建一個Q羣544645972,有興趣的加一下,一塊兒成長。html
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
Notification有兩種視覺風格,一種是標準視圖(Normal view)、一種是大視圖(Big view)。標準視圖在Android中各版本是通用的,可是對於大視圖而言,僅支持Android4.1+的版本,好比郵件,音樂等軟件就會使用到這種大視圖樣式的擴展通知欄,系統提供了setStyle()函數用來設置大視圖模式,通常狀況下有三種模式提供選擇:github
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>複製代碼
這裏有幾點須要着重說明一下微信
這種效果你們應該在微信中看的不少,其實實現也很簡單:
代碼: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
1.經過notification打開activity的時候,就要涉及到保存用戶導航的問題,這個時候就要使用到activity task的相關內容了,我之前寫過一篇博客中有介紹到activity task的內容:android深刻解析Activity的launchMode啓動模式,Intent Flag,taskAffinity,感興趣的能夠去看看。那麼要實現點擊notification打開指定activity,就須要設置相關的pendingIntent,有兩種特殊的狀況須要說明一下: dom
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及如下是沒法處理自定義佈局中的操做事件的,這樣咱們就不要去考慮增長自定義按鈕了。
分析一下源碼,以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…。