概述
前文讲解了Notification的构造,现在来讲讲notification的发送,以及公布前文留下的疑问(自定义view不论高度是多高,最后只能显示为64dp,why?)
NotificationManager
在Notification构造完成后,会调用NotificationManager的notify
方法来发送通知,我们就来看看该方法
frameworks/base/core/java/android/app/NotificationManager.java
public void notify(String tag, int id, Notification notification)
{
...
INotificationManager service = getService();
...
service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id,
stripped, idOut, UserHandle.myUserId());
...
}
可以看出NotificationManager只是一个空壳,没有做什么实际上的事情,只是把notify的动作交给了service来做。
为了主干的清晰,直接给出enqueueNotificationWithTag的实现在NotificationManagerService中
NotificationManagerService
frameworks/base/services/java/com/android/server/NotificationManagerService.java
public void enqueueNotificationWithTag(String pkg, String opPkg, String tag, int id,
Notification notification, int[] idOut, int userId) throws RemoteException {
enqueueNotificationInternal(pkg, opPkg, Binder.getCallingUid(),
Binder.getCallingPid(), tag, id, notification, idOut, userId);
}
所以重要的是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) {
...
if (!isSystemNotification && !isNotificationFromListener) {
...
//MAX_PACKAGE_NOTIFICATIONS = 50;
if (count >= MAX_PACKAGE_NOTIFICATIONS) {
return;
}
}
...
mHandler.post(new Runnable() {
@Override
public void run() {
synchronized (mNotificationList) {
...
// blocked apps
//如果用户设置了该引用不显示通知,并且不是系统通知的话,直接将该通知打分为-1000
if (ENABLE_BLOCKED_NOTIFICATIONS && !noteNotificationOp(pkg, callingUid)) {
if (!isSystemNotification) {
//JUNK_SCORE = -1000;
r.score = JUNK_SCORE;
}
}
//SCORE_DISPLAY_THRESHOLD = -20;
//打分小于阈值的通知不显示
if (r.score < SCORE_DISPLAY_THRESHOLD) {
// Notification will be blocked because the score is too low.
return;
}
//垃圾通知,也不会显示
if (isNotificationSpam(notification, pkg)) {
mArchive.record(r.sbn);
return;
}
...
//只显示有图标的通知
if (notification.icon != 0) {
StatusBarNotification oldSbn = (old != null) ? old.sbn : null;
mListeners.notifyPostedLocked(n, oldSbn);
}
...
//声音,震动,闪光灯的控制
buzzBeepBlinkLocked(r);
}
}
});
}
可以看到要想发出通知必须得满足以下几个条件
- 非系统应用,最多只能发送50个通知消息
- 用户设置了允许应用发送通知
-
被系统判定为非垃圾通知(该功能是cm自己添加的,系统中会有一个数据库,然后根据通知栏的Extra信息来匹配,如果成功则判定为垃圾通知,但是该功能现在并没有实现) - 通知必须得有icon
检查通过后再使用notifyPostedLocked
方法做真正的发送动作。buzzBeepBlinkLocked很简单,不浪费篇幅叙述了。
INotificationListener
notifyPostedLocked方法最后调用notifyPosted方法,我们直接来看看该方法
private void notifyPosted(final ManagedServiceInfo info,
final StatusBarNotification sbn, NotificationRankingUpdate rankingUpdate) {
final INotificationListener listener = (INotificationListener)info.service;
...
listener.onNotificationPosted(sbnHolder, rankingUpdate);
...
}
这里有一个INotificationListener对象,一看到以I
开头的就可以知道,这里肯定又是一个IPC通信。
查看源码可以知道,onNotificationPosted
的实现是在SystemUI进程中,也就是我们的状态栏进程。
BaseStatusBar
frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
@Override
public void onNotificationPosted(final StatusBarNotification sbn,
final RankingMap rankingMap) {
mHandler.post(new Runnable() {
@Override
public void run() {
...
boolean isUpdate = mNotificationData.get(sbn.getKey()) != null
|| isHeadsUp(sbn.getKey());
...
if (isUpdate) {
updateNotification(sbn, rankingMap);
} else {
addNotification(sbn, rankingMap);
}
}
});
}
状态栏会根据通知的唯一key值来判断该通知是否是更新还是新增的。
我们以新增的为例来讲.addNotification
是一个抽象方法,实现是在BaseStatusBar的子类PhoneStatusBar
中
PhoneStatusBar
frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
public void addNotification(StatusBarNotification notification, RankingMap ranking) {
...
Entry shadeEntry = createNotificationViews(notification);
if (shadeEntry == null) {
return;
}
...
addNotificationViews(shadeEntry, ranking);
...
}
该方法做了2个重要的事情,一个就是创建Entry实例,另外一个就是将Entry添加到状态栏上,然后就显示完成了。
因为createNotificationViews
的实现是在父类中,并且该方法十分重要,所以我们先跳过该方法。
先把Entry理解成一条通知,来讲addNotificationViews的实现。
protected void addNotificationViews(Entry entry, RankingMap ranking) {
if (entry == null) {
return;
}
// Add the expanded view and icon.
mNotificationData.add(entry, ranking);
updateNotifications();
}
先直接将得到的Entry添加到mNotificationData里面
最终updateNotifications会调用PhoneStatusBar中的updateNotificationShade
方法
private void updateNotificationShade() {
...
ArrayList activeNotifications = mNotificationData.getActiveNotifications();
ArrayList toShow = new ArrayList<>(activeNotifications.size());
...
for (int i=0; i
- 从mNotificationData对象中获取一个list
对象 - 将mNotificationData中的每一个Entry对象的row属性添加到List
中 - 将ExpandableNotificationRow添加到mStackScroller里面
这个mStackScroller是NotificationStackScrollLayout的对象,而这个NotificationStackScrollLayout是一个继承自ViewGroup的,也就是我们下拉状态栏看到的整片view的根view.
那么ExpandableNotificationRow也就是对应着每一个通知了. ExpandableNotificationRow是继承自FrameLayout的
我们前面说到把Entry先理解为一条通知,看到这里,其实添加的是Entry对象里面的row属性到界面上,也就是ExpandableNotificationRow
createNotificationViews
这个是解答开头疑问的关键。 该方法是BaseStatusBar
类的方法。
protected NotificationData.Entry createNotificationViews(StatusBarNotification sbn) {
...
// Construct the expanded view.
NotificationData.Entry entry = new NotificationData.Entry(sbn, iconView);
if (!inflateViews(entry, mStackScroller)) {
handleNotificationError(sbn, "Couldn't expand RemoteViews for: " + sbn);
return null;
}
return entry;
}
这里首先实例化了NotificationData的内部类Entry。
NotificationData是一个十分重要的类,里面有几个比较重要的数据结构
ArrayMapmEntries = new ArrayMap<>(); //所有Entry的集合
ArrayListmSortedAndFiltered = new ArrayList<>(); //排序后的Entry集合
那这个Entry到底是个什么东西呢?先来看看这个类的定义
public static final class Entry {
...
public ExpandableNotificationRow row; // the outer expanded view
public View expanded; // the inflated RemoteViews
public View expandedPublic; // for insecure lockscreens
public View expandedBig;
...
}
从定义里面可以看出,一个Entry对应了一条通知栏的所有Data信息,其中比较重要的是row属性,前面已经碰到过了。最后添加界面上的也就是这个row。
在inflateViews
方法里面,这个row会被赋值,我们来看看row是怎么被赋值的
private boolean inflateViews(NotificationData.Entry entry, ViewGroup parent, boolean isHeadsUp) {
...
//contentView和bigContentView是我们构造Notification时传过来的view
RemoteViews contentView = sbn.getNotification().contentView;
RemoteViews bigContentView = sbn.getNotification().bigContentView;
...
ExpandableNotificationRow row;
...
//使用指定view填充
row = (ExpandableNotificationRow) inflater.inflate(R.layout.status_bar_notification_row,
parent, false);
...
//这个expanded view就是我们在下拉状态栏中看到的每一条view,这里命名为expanded 应该是状态栏展开,而不是通知展开
//NotificationContentView是继承自FrameLayout的,会根据不同状态来控制显示哪个view(默认通知/展开通知)
NotificationContentView expanded =
(NotificationContentView) row.findViewById(R.id.expanded);
...
//给每一条通知设置onClick的点击事件,以来相应我们设置的动作.
PendingIntent contentIntent = sbn.getNotification().contentIntent;
final View.OnClickListener listener = makeClicker(contentIntent, sbn.getKey(),
isHeadsUp);
row.setOnClickListener(listener);
...
///////关键////////////
View contentViewLocal = null;
View bigContentViewLocal = null;
//将构造通知栏时设置的contentView & bigContentView(RemoteView)转换为view
contentViewLocal = contentView.apply(mContext, expanded,
mOnClickHandler, themePackageName);
if (bigContentView != null) {
bigContentViewLocal = bigContentView.apply(mContext, expanded,
mOnClickHandler, themePackageName);
}
...
//因为expanded 是一个FrameLayout的ViewGroup,所以往里面塞了2个view
expanded.setContractedChild(contentViewLocal);
expanded.setExpandedChild(bigContentViewLocal);
}
看完上面的代码,先来坐个小节,整理下思路。在Entry.row添加到屏幕上前,做了如下的属性赋值
- inflate布局文件status_bar_notification_row(这是每个通知栏的根view)
- 给根view设置监听器
- 将在构造通知过程中的bigContentView 和 contentView 塞到通知栏的根view里面
到这里,一个通知栏从初始化到显示的流程就讲完了,但是最开头的疑问不是还没有解答吗?来看答案
答案
contentView固定高度
在expanded.setContractedChild
方法前,传递进来的ContentView都还是自义定的view,没有做高度限制或者系统默认的view. 最后显示的时候却被限制了,说明在setContractedChild方法里做了手脚
public void setContractedChild(View child) {
...
sanitizeContractedLayoutParams(child);
addView(child);
...
}
private void sanitizeContractedLayoutParams(View contractedChild) {
LayoutParams lp = (LayoutParams) contractedChild.getLayoutParams();
lp.height = mSmallHeight;
contractedChild.setLayoutParams(lp);
}
可以看到在sanitizeContractedLayoutParams
方法里面,不论传递进来的contentView有多高最后的会被改成mSmallHeight的高度。这个mSmallHeight的值就是在SystemUI里面配置的,64dp
bigview最大高度
在expanded.setExpandedChild
的方法里面却没有做最大高度的限制,那么最大高度是在哪限制的呢?
这个时候就要看看ExpandableNotificationRow这个根view了
ExpandableNotificationRow继承自ExpandableView,来看看onMeasure方法
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//mMaxNotificationHeight是systemui中配置的值,256dp
int ownMaxHeight = mMaxNotificationHeight;
...
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
int childHeightSpec = newHeightSpec;
ViewGroup.LayoutParams layoutParams = child.getLayoutParams();
if (layoutParams.height != ViewGroup.LayoutParams.MATCH_PARENT) {
if (layoutParams.height >= 0) {
// An actual height is set
childHeightSpec = layoutParams.height > ownMaxHeight
? MeasureSpec.makeMeasureSpec(ownMaxHeight, MeasureSpec.EXACTLY)
: MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY);
}
child.measure(
getChildMeasureSpec(widthMeasureSpec, 0 /* padding */, layoutParams.width),
childHeightSpec);
int childHeight = child.getMeasuredHeight();
maxChildHeight = Math.max(maxChildHeight, childHeight);
} else {
mMatchParentViews.add(child);
}
}
int ownHeight = hasFixedHeight ? ownMaxHeight : maxChildHeight;
newHeightSpec = MeasureSpec.makeMeasureSpec(ownHeight, MeasureSpec.EXACTLY);
for (View child : mMatchParentViews) {
child.measure(getChildMeasureSpec(
widthMeasureSpec, 0 /* padding */, child.getLayoutParams().width),
newHeightSpec);
}
...
如果bigviewlayoutParams.height == ViewGroup.LayoutParams.MATCH_PARENT
则高度就是newHeightSpec。这个newHeightSpec要么是ownMaxHeight 要么是maxChildHeight,而这2个值的最大值就是256dp
如果bigviewlayoutParams.height != ViewGroup.LayoutParams.MATCH_PARENT
,最大值也是maxChildHeight 也就是256dp
注意: 这里并没有显示bigview的最小高度,所以bigview的高度范围是可以在(0,256dp ] 区间的
最后
a pic is worth a thousands words, tow pics worth double, lol
类图
流程图
相关阅读
Notification之---NotificationListenerService5.0实现原理
Notification之----Android5.0实现原理(一)
Notification之----自定义样式
Notification之----默认样式
Notification之----任务栈