在之前的一篇文章中我们了解到 SystemUI 中的通知是通过监听 NotificationListenerService(NLS)获取到的,那通知又是如何一步一步变成 ExpandableNotificationRow 并添加到NotificationStackScrollLayout(NSSL) 中形成列表的呢?本文就解析一下通知实体从数据层到视图层工作机制。
本文基于 Android 11 源码进行分析,其中涉及到核心类关系如下
classDiagram
%% 通知监听和处理器
class NotificationListener {
+onNotificationPosted()
+onNotificationRemoved()
+onNotificationRankingUpdate()
-mNotificationHandlers : List~NotificationHandler~
}
class NotificationEntryManager {
-mPendingNotifications : List~NotificationEntry~
-mActiveNotifications : List~NotificationEntry~
-mPresenter : NotificationPresenter
-mRowBinder : NotificationRowBinderImpl
+addNotification()
+updateNotification()
+getActiveNotifications()
}
class NotificationHandler {
<<interface>>
+onNotificationPosted()
+onNotificationRemoved()
+onNotificationRankingUpdate()
}
%% 视图绑定和布局类
class NotificationRowBinderImpl {
-mRowInflaterTask : RowInflaterTask
-mViewHierarchyManager : NotificationViewHierarchyManager
+inflateViews()
-bindRow()
-updateRow()
}
class RowInflaterTask {
-mAsyncLayoutInflater : AsyncLayoutInflater
+inflate()
}
class AsyncLayoutInflater {
-mInflateThread : InflateThread
-mHandler : Handler
-mInflater : LayoutInflater
+inflate()
}
%% 视图和Presenter类
class ExpandableNotificationRow {
-mEntry : NotificationEntry
-mIsSummaryWithChildren : boolean
-mIsHeadsUp : boolean
+setEntry()
+onNotificationUpdated()
}
class NotificationPresenter {
<<interface>>
+updateNotificationViews()
}
class NotificationViewHierarchyManager {
-mListContainer : NotificationListContainer
+updateNotificationViews()
}
class NotificationEntry {
-mKey : String
-mSbn : StatusBarNotification
-mRow : ExpandableNotificationRow
}
%% 新增的NSSL相关类
class NotificationListContainer {
<<interface>>
+addContainerView()
+removeContainerView()
+getContainerChildCount()
+getContainerChildAt()
+setChildTransferInProgress()
}
class NotificationStackScrollLayout {
<<ViewGroup>>
-mRows : List~ExpandableNotificationRow~
+addContainerView()
+generateChildOrderChangedEvent()
}
%% 继承和实现关系
NotificationListener ..|> NotificationHandler
%% 组合/关联关系
NotificationListener --> NotificationHandler : 持有列表
NotificationListener --> NotificationEntryManager : 委托处理
NotificationEntryManager --> NotificationPresenter : 持有
NotificationEntryManager --> NotificationRowBinderImpl : 持有
NotificationEntryManager --> NotificationEntry : 管理列表
NotificationEntry --> ExpandableNotificationRow : 关联
NotificationRowBinderImpl --> RowInflaterTask : 使用
NotificationRowBinderImpl --> AsyncLayoutInflater : 使用
NotificationRowBinderImpl --> NotificationViewHierarchyManager : 持有
RowInflaterTask --> AsyncLayoutInflater : 使用
%% 异步回调
AsyncLayoutInflater ..> ExpandableNotificationRow : 创建实例
%% 视图层次管理 - 通过接口持有
NotificationViewHierarchyManager --> NotificationListContainer : 持有mListContainer
%% NSSL实现NotificationListContainer接口
NotificationStackScrollLayout ..|> NotificationListContainer
%% NSSL管理ExpandableNotificationRow
NotificationStackScrollLayout --> ExpandableNotificationRow : 管理列表
%% Presenter与视图管理器的交互
NotificationPresenter --> NotificationViewHierarchyManager : 更新视图
%% 注释说明
note for NotificationListener "接收系统通知事件,转发给EntryManager"
note for NotificationEntryManager "核心管理者,协调Presenter、Binder和ViewManager"
note for NotificationRowBinderImpl "负责通知行的绑定和异步布局膨胀"
note for RowInflaterTask "异步布局膨胀任务,避免主线程卡顿"
note for ExpandableNotificationRow "单个通知的视图表示"
note for NotificationStackScrollLayout "通知堆栈滚动布局,实际管理通知行的容器"
note for NotificationListContainer "容器接口,抽象化通知行的容器操作"
0x00 数据源
首先在应用层通过NotificationListener 设置监听。
NotificationListener.addNotificationHandler() 方法
/** Registers a listener that's notified when notifications are added/removed/etc. */
public void addNotificationHandler(NotificationHandler handler) {
if (mNotificationHandlers.contains(handler)) {
throw new IllegalArgumentException("Listener is already added");
}
mNotificationHandlers.add(handler);
}
NotificationEntryManager 在初始化时通过 attach 方法绑定了监听
/** Once called, the NEM will start processing notification events from system server. */
public void attach(NotificationListener notificationListener) {
notificationListener.addNotificationHandler(mNotifListener);
}
NotificationHandler 这里定义添加、更新和移除的相关回调方法
private final NotificationHandler mNotifListener = new NotificationHandler() {
@Override
public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) {
final boolean isUpdateToInflatedNotif = mActiveNotifications.containsKey(sbn.getKey());
// 当前通知列表中已存在则进行更新
if (isUpdateToInflatedNotif) {
updateNotification(sbn, rankingMap);
} else {
// 否则添加新通知
addNotification(sbn, rankingMap);
}
}
@Override
public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap) {
// 通知移除
removeNotification(sbn.getKey(), rankingMap, UNDEFINED_DISMISS_REASON);
}
@Override
public void onNotificationRemoved(
StatusBarNotification sbn,
RankingMap rankingMap,
int reason) {
// 通知移除
removeNotification(sbn.getKey(), rankingMap, reason);
}
@Override
public void onNotificationRankingUpdate(RankingMap rankingMap) {
// 更新通知列表排序
updateNotificationRanking(rankingMap);
}
...
};
以一条新通知为例
public void addNotification(StatusBarNotification notification, RankingMap ranking) {
try {
addNotificationInternal(notification, ranking);
} catch (InflationException e) {
handleInflationException(notification, e);
}
}
private void addNotificationInternal(
StatusBarNotification notification,
RankingMap rankingMap) throws InflationException {
String key = notification.getKey();
if (DEBUG) {
Log.d(TAG, "addNotification key=" + key);
}
// 更新排序:rankingMap是完整的排序信息,包括已存在的和即将添加的通知
updateRankingAndSort(rankingMap, "addNotificationInternal");
Ranking ranking = new Ranking();
rankingMap.getRanking(key, ranking);
NotificationEntry entry = mPendingNotifications.get(key);
if (entry != null) {
// 情况1: Entry已存在(正在inflate中,这个操作是异步的,
// 第一次通知过来是它在异步线程中,但是很快通知又被更新了,就会进入到这个分支)
entry.setSbn(notification);
} else {
// 情况2: 新Entry,使用NotificationEntry对sbn进行封装
entry = new NotificationEntry(
notification,
ranking,
mFgsFeatureController.isForegroundServiceDismissalEnabled(),
SystemClock.uptimeMillis());
mAllNotifications.add(entry);
// 内存泄漏检测!
mLeakDetector.trackInstance(entry);
for (NotifCollectionListener listener : mNotifCollectionListeners) {
listener.onEntryInit(entry);
}
}
for (NotifCollectionListener listener : mNotifCollectionListeners) {
listener.onEntryBind(entry, notification);
}
// Construct the expanded view.
// 开始构建对应的视图组件
if (!mFeatureFlags.isNewNotifPipelineRenderingEnabled()) {
// 通过 Dagger 注入的,实际获取到的是 NotificationRowBinderImpl
mNotificationRowBinderLazy.get()
.inflateViews(
entry,
() -> performRemoveNotification(notification, REASON_CANCEL),
mInflationCallback);
}
mPendingNotifications.put(key, entry);
mLogger.logNotifAdded(entry.getKey());
for (NotificationEntryListener listener : mNotificationEntryListeners) {
listener.onPendingEntryAdded(entry);
}
for (NotifCollectionListener listener : mNotifCollectionListeners) {
listener.onEntryAdded(entry);
}
for (NotifCollectionListener listener : mNotifCollectionListeners) {
listener.onRankingApplied();
}
}
RankingMap
- NMS计算好的排序信息
StatusBarNotification(SBN)
- 从NMS来传递过来的通知数据(
Parcelable对象)
NotificationEntry
- 封装了
SBN,生成唯一 Key - Key 格式:
userId|pkg|id|tag|uid
0x01 创建视图
通过 NotificationRowBinderImpl 创建视图组件
/**
* Inflates the views for the given entry (possibly asynchronously).
*/
@Override
public void inflateViews(
NotificationEntry entry,
Runnable onDismissRunnable,
NotificationRowContentBinder.InflationCallback callback)
throws InflationException {
ViewGroup parent = mListContainer.getViewParentForNotification(entry);
// 如果通知存在,进行更新
// 设想这个场景:
// 用户收到一条消息通知,接着很快又收到更新通知,就复用这个row
if (entry.rowExists()) {
mIconManager.updateIcons(entry);
ExpandableNotificationRow row = entry.getRow();
// reset确保每次更新都从干净状态开始
row.reset();
// 更新信息
updateRow(entry, row);
// 绑定
inflateContentViews(entry, row, callback);
entry.getRowController().setOnDismissRunnable(onDismissRunnable);
} else {
// 更新状态栏、Sheft等图标
mIconManager.createIcons(entry);
// 从Dagger中获取 RowInflaterTask 并执行异步inflate
mRowInflaterTaskProvider.get().inflate(mContext, parent, entry,
row -> {
// Setup the controller for the view.
// 异步结果返回的 row 就是 ExpandableNotificationRow 的通知条目的视图实例
// 把 row entry 等实体又交给 Dagger 进行管理并创建对应的控制器 Controller
// 你会发现 SystemUI 很多视图都有对应的 Controller 用于解耦
ExpandableNotificationRowComponent component =
mExpandableNotificationRowComponentBuilder
.expandableNotificationRow(row)
.notificationEntry(entry)
.onDismissRunnable(onDismissRunnable)
.rowContentBindStage(mRowContentBindStage)
.onExpandClickListener(mPresenter)
.build();
// 使用 Controller 对 ExpandableNotificationRow 进行操作
ExpandableNotificationRowController rowController =
component.getExpandableNotificationRowController();
rowController.init();
entry.setRowController(rowController);
bindRow(entry, row);
updateRow(entry, row);
// inflate 完成后 通过 callback 回调回去
inflateContentViews(entry, row, callback);
});
}
}
/**
* Inflate the row's basic content views.
*/
private void inflateContentViews(
NotificationEntry entry,
ExpandableNotificationRow row,
@Nullable NotificationRowContentBinder.InflationCallback inflationCallback) {
// 判断是否是“重要”通知消息
final boolean useIncreasedCollapsedHeight =
mMessagingUtil.isImportantMessaging(entry.getSbn(), entry.getImportance());
// If this is our first time inflating, we don't actually know the groupings for real
// yet, so we might actually inflate a low priority content view incorrectly here and have
// to correct it later in the pipeline. On subsequent inflations (i.e. updates), this
// should inflate the correct view.
// 上面的注释说是第一次inflate时可能会出错
// 时间线
// T1: 通知A到达 (parent未知,判断为lowPriority)
// T2: 通知B到达 (发现A和B是一组)
// T3: NotificationGroupManager重新计算分组
// T4: A变成分组的summary (不再是lowPriority!)
// T5: 重新inflate A的ContentView ← 纠正错误
final boolean isLowPriority = mLowPriorityInflationHelper.shouldUseLowPriorityView(entry);
// 确定参数:优先级、高度、是否重新绑定等参数
RowContentBindParams params = mRowContentBindStage.getStageParams(entry);
params.setUseIncreasedCollapsedHeight(useIncreasedCollapsedHeight);
params.setUseLowPriority(isLowPriority);
// TODO: Replace this API with RowContentBindParams directly. Also move to a separate
// redaction controller.
// 锁屏隐私:例如微信聊天通知在锁屏下不显示内容只显示消息条数
row.setNeedsRedaction(mNotificationLockscreenUserManager.needsRedaction(entry));
// 请求重新绑定
params.rebindAllContentViews();
mRowContentBindStage.requestRebind(entry, en -> {
row.setUsesIncreasedCollapsedHeight(useIncreasedCollapsedHeight);
row.setIsLowPriority(isLowPriority);
// 通过回调接口通知视图构建完成
if (inflationCallback != null) {
inflationCallback.onAsyncInflationFinished(en);
}
});
}
接着使用RowInflaterTask 进行异步 inflate 操作,它使用了 AsyncLayoutInflater 接口帮它干活。
/**
* Inflates a new notificationView. This should not be called twice on this object
*/
public void inflate(Context context, ViewGroup parent, NotificationEntry entry,
RowInflationFinishedListener listener) {
if (TRACE_ORIGIN) {
mInflateOrigin = new Throwable("inflate requested here");
}
mListener = listener;
AsyncLayoutInflater inflater = new AsyncLayoutInflater(context);
mEntry = entry;
entry.setInflationTask(this);
inflater.inflate(R.layout.status_bar_notification_row, parent, this);
}
AsyncLayoutInflater 的实现也比较简单,内部是一个线程、任务队列和Handler的组合。inflate 的耗时操作是在线程中,然后将解析出来的 View 通过 Handler 发送到主线程并回调出去。
视图创建好之后绑定、更新视图
// row 与 entry 进行绑定
private void bindRow(NotificationEntry entry, ExpandableNotificationRow row) {
mListContainer.bindRow(row);
mNotificationRemoteInputManager.bindRow(row);
row.setOnActivatedListener(mPresenter);
entry.setRow(row);
row.setEntry(entry);
mNotifBindPipeline.manageRow(entry, row);
mBindRowCallback.onBindRow(row);
}
private void updateRow(
NotificationEntry entry,
ExpandableNotificationRow row) {
row.setLegacy(entry.targetSdk >= Build.VERSION_CODES.GINGERBREAD
&& entry.targetSdk < Build.VERSION_CODES.LOLLIPOP);
// bind the click event to the content area
// 注册了onClick事件
requireNonNull(mNotificationClicker).register(row, entry.getSbn());
}
最后在 NotificationEntryManager 中的 InflationCallback.onAsyncInflationFinished() 接受
private final InflationCallback mInflationCallback = new InflationCallback() {
...
@Override
public void onAsyncInflationFinished(NotificationEntry entry) {
mPendingNotifications.remove(entry.getKey());
// If there was an async task started after the removal, we don't want to add it back to
// the list, otherwise we might get leaks.
if (!entry.isRowRemoved()) {
boolean isNew = getActiveNotificationUnfiltered(entry.getKey()) == null;
mLogger.logNotifInflated(entry.getKey(), isNew);
if (isNew) {
// 刚发过来的新通知
for (NotificationEntryListener listener : mNotificationEntryListeners) {
listener.onEntryInflated(entry);
}
// 将解析好的 entry 加入到 NotificationEntryManager
// 的内存集合 mActiveNotifications 中
addActiveNotification(entry);
// 这里会触发更新通知
updateNotifications("onAsyncInflationFinished");
for (NotificationEntryListener listener : mNotificationEntryListeners) {
listener.onNotificationAdded(entry);
}
} else {
for (NotificationEntryListener listener : mNotificationEntryListeners) {
listener.onEntryReinflated(entry);
}
}
}
}
};
/**
* Update the notifications
* @param reason why the notifications are updating
*/
public void updateNotifications(String reason) {
reapplyFilterAndSort(reason);
// 重要!!!使用 NotificationPresenter 更新通知视图
if (mPresenter != null && !mFeatureFlags.isNewNotifPipelineRenderingEnabled()) {
mPresenter.updateNotificationViews(reason);
}
}
0x02 视图渲染
NotificationPresenter 的具体实现类是 StatusBarNotificationPresenter 在 updateNotificationViews方法分别使用 mViewHierarchyManager 和 mNotificationPanel 更新视图
public void updateNotificationViews(final String reason) {
// The function updateRowStates depends on both of these being non-null, so check them here.
// We may be called before they are set from DeviceProvisionedController's callback.
if (mScrimController == null) return;
// Do not modify the notifications during collapse.
if (isCollapsing()) {
mShadeController.addPostCollapseAction(() -> updateNotificationViews(reason));
return;
}
// 根据最新的数据列表(Entry List)
// 让 UI 容器(NSSL)里的 View 顺序、层级、显示状态与之一致
mViewHierarchyManager.updateNotificationViews();
// mNotificationPanel 是 NotificationPanelViewController
// 它是 NSSL 的视图控制器,通过它执行视图的更新
mNotificationPanel.updateNotificationViews(reason);
}
这里重点看一下 NotificationViewHierarchyManager.updateNotificationViews()
这个方法也是极其复杂,它的任务是:根据最新的数据列表(Entry List),让 UI 容器(NSSL)里的 View 顺序、层级、显示状态与之一致。
我们分四个阶段来逻辑拆解:
第一阶段:视图状态判定与过滤
代码开始时,遍历 activeNotifications,对每一条通知进行“身份审查”:
- 过滤非法状态:如果通知被删除(
Dismissed/Removed)、是气泡通知或属于前台服务特定部分,则跳过。 - 隐私脱敏(Redaction):
- 检查当前用户是否在锁屏状态(Public Mode)。
- 调用
mLockscreenUserManager判定该通知是否包含敏感内容。 - 如果需要脱敏,调用
ent.setSensitive(sensitive, ...),这会导致通知内容变成“隐藏内容,解锁后查看”。
- 分组判定(Grouping):
- 判定该通知是“独立通知”还是“组内子通知”。
public void updateNotificationViews() {
...
// 获取当前可见的通知列表
List<NotificationEntry> activeNotifications = mEntryManager.getVisibleNotifications();
// 存放所有顶层通知(Top-level Rows),包括独立通知和组的摘要(Summary)
ArrayList<ExpandableNotificationRow> toShow = new ArrayList<>(activeNotifications.size());
final int N = activeNotifications.size();
for (int i = 0; i < N; i++) {
NotificationEntry ent = activeNotifications.get(i);
// 过滤非法状态
if (ent.isRowDismissed() || ent.isRowRemoved()
|| mBubbleController.isBubbleNotificationSuppressedFromShade(ent)
|| mFgsSectionController.hasEntry(ent)) {
// we don't want to update removed notifications because they could
// temporarily become children if they were isolated before.
continue;
}
int userId = ent.getSbn().getUserId();
// Display public version of the notification if we need to redact.
// TODO: This area uses a lot of calls into NotificationLockscreenUserManager.
// We can probably move some of this code there.
// 是否在锁屏状态,需要对用户隐私数据进行处理
int currentUserId = mLockscreenUserManager.getCurrentUserId();
boolean devicePublic = mLockscreenUserManager.isLockscreenPublicMode(currentUserId);
boolean userPublic = devicePublic
|| mLockscreenUserManager.isLockscreenPublicMode(userId);
if (userPublic && mDynamicPrivacyController.isDynamicallyUnlocked()
&& (userId == currentUserId || userId == UserHandle.USER_ALL
|| !mLockscreenUserManager.needsSeparateWorkChallenge(userId))) {
userPublic = false;
}
// 下面处理是否是敏感信息
boolean needsRedaction = mLockscreenUserManager.needsRedaction(ent);
boolean sensitive = userPublic && needsRedaction;
boolean deviceSensitive = devicePublic
&& !mLockscreenUserManager.userAllowsPrivateNotificationsInPublic(
currentUserId);
ent.setSensitive(sensitive, deviceSensitive);
ent.getRow().setNeedsRedaction(needsRedaction);
mLowPriorityInflationHelper.recheckLowPriorityViewAndInflate(ent, ent.getRow());
...
// 判断 通知是“独立通知”还是“组内子通知”
boolean isChildInGroup = mGroupManager.isChildInGroupWithSummary(ent.getSbn());
boolean groupChangesAllowed =
mVisualStabilityManager.areGroupChangesAllowed() // user isn't looking at notifs
|| !ent.hasFinishedInitialization(); // notif recently added
NotificationEntry parent = mGroupManager.getGroupSummary(ent.getSbn());
if (!groupChangesAllowed) {
// We don't to change groups while the user is looking at them
boolean wasChildInGroup = ent.isChildInGroup();
if (isChildInGroup && !wasChildInGroup) {
isChildInGroup = wasChildInGroup;
mVisualStabilityManager.addGroupChangesAllowedCallback(mEntryManager,
false /* persistent */);
} else if (!isChildInGroup && wasChildInGroup) {
// We allow grouping changes if the group was collapsed
if (mGroupManager.isLogicalGroupExpanded(ent.getSbn())) {
isChildInGroup = wasChildInGroup;
parent = ent.getRow().getNotificationParent().getEntry();
mVisualStabilityManager.addGroupChangesAllowedCallback(mEntryManager,
false /* persistent */);
}
}
}
...
}
第二阶段:构建视图列表
代码使用 toShow 列表和 mTmpChildOrderMap 来构建通知列表:
toShow:存放所有顶层通知,包括独立通知和组的摘要(Summary)。mTmpChildOrderMap:建立“父通知 -> 子通知列表”的映射关系。
public void updateNotificationViews() {
...
if (isChildInGroup) {
List<NotificationEntry> orderedChildren = mTmpChildOrderMap.get(parent);
if (orderedChildren == null) {
orderedChildren = new ArrayList<>();
// 建立父子视图关系
mTmpChildOrderMap.put(parent, orderedChildren);
}
orderedChildren.add(ent);
} else {
// Top-level notif (either a summary or single notification)
// A child may have already added its summary to mTmpChildOrderMap with a
// list of children. This can happen since there's no guarantee summaries are
// sorted before its children.
if (!mTmpChildOrderMap.containsKey(ent)) {
// mTmpChildOrderMap's keyset is used to iterate through all entries, so it's
// necessary to add each top-level notif as a key
mTmpChildOrderMap.put(ent, null);
}
// 添加到待显示列表
toShow.add(ent.getRow());
}
...
}
第三阶段:添加、移除
这是最繁忙的阶段,负责执行真正的 addView 和 removeView。
- 清理不再需要的 View:
- 遍历
NSSL中现有的子视图,如果它不在toShow蓝图里,就调用removeContainerView将其移除。
- 遍历
- 添加新 View:
- 遍历
toShow,如果某个 Row 还没有 Parent,调用mListContainer.addContainerView(v)正式将其加入NSSL。
- 遍历
- 处理子通知层级:
- 执行
addNotificationChildrenAndSort(),将组内成员放入ExpandableNotificationRow内部的容器中。
- 执行
public void updateNotificationViews() {
...
// 清理不再需要的 View
ArrayList<ExpandableNotificationRow> viewsToRemove = new ArrayList<>();
// mListContainer 就是 NSSL
for (int i=0; i< mListContainer.getContainerChildCount(); i++) {
View child = mListContainer.getContainerChildAt(i);
if (!toShow.contains(child) && child instanceof ExpandableNotificationRow) {
ExpandableNotificationRow row = (ExpandableNotificationRow) child;
// Blocking helper is effectively a detached view. Don't bother removing it from the
// layout.
if (!row.isBlockingHelperShowing()) {
// 收集待删除的view
viewsToRemove.add((ExpandableNotificationRow) child);
}
}
}
for (ExpandableNotificationRow viewToRemove : viewsToRemove) {
...
if (viewToRemove.isSummaryWithChildren()) {
viewToRemove.removeAllChildren();
}
// 删除view
mListContainer.removeContainerView(viewToRemove);
mListContainer.setChildTransferInProgress(false);
}
removeNotificationChildren();
// 遍历 toShow 挂载新 View
for (int i = 0; i < toShow.size(); i++) {
View v = toShow.get(i);
if (v.getParent() == null) {
// 还没有 Parent 添加 到 mListContainer(NSSL)
mVisualStabilityManager.notifyViewAddition(v);
// 重要!!!终于把 row view 添加到 NSSL 中来了
mListContainer.addContainerView(v);
} else if (!mListContainer.containsView(v)) {
// the view is added somewhere else. Let's make sure
// the ordering works properly below, by excluding these
toShow.remove(v);
i--;
}
}
...
}
其中 mListContainer 就是 NotificationStackScrollLayout(NSSL) 通知列表的容器组件。执行 addView 操作后就会触发 requestLayout 最终由 NSSL 完成列表的渲染
public void addContainerView(View v) {
Assert.isMainThread();
addView(v);
}
第四阶段:调整收尾
即使 View 都加进来了,顺序可能还是错的。
- 双指针同步排序:
- 遍历
NSSL的子视图和toShow蓝图。 - 如果位置不匹配,调用
mListContainer.changeViewPosition。 - 稳定性检查:如果
canReorderNotification返回 false(比如用户正在触摸),排序会被推迟,并注册一个回调等待时机。
- 遍历
- 状态刷新:
onNotificationViewUpdateFinished():通知 NSSL 更新已经完成。
public void updateNotificationViews() {
...
int j = 0;
for (int i = 0; i < mListContainer.getContainerChildCount(); i++) {
View child = mListContainer.getContainerChildAt(i);
// ... 过滤非通知 View ...
ExpandableNotificationRow targetChild = toShow.get(j);
if (child != targetChild) {
// 发现位置对不上了!
if (mVisualStabilityManager.canReorderNotification(targetChild)) {
// 将 targetChild 挪到当前的物理索引 i
mListContainer.changeViewPosition(targetChild, i);
}
}
j++;
}
...
mListContainer.onNotificationViewUpdateFinished();
...
}
有人可能好奇理论上第三步的时候这个方法应该结束了,为何还需要第四步的重排操作呢?
原因:
addView默认是“末尾追加”- Android
ViewGroup的addView(View v)默认行为是:把新的子 View 放到所有子 View 的最后面(Index 最大) - 通知栏的通知是根据优先级进行排序的,优先级高的排在前面
- Android
- UI 树 index 顺序决定了“绘制顺序”和“重叠关系”
- 绘制顺序 (Drawing Order):Index 小的先画,Index 大的后画。
- 重叠/遮盖 (Z-Order):在通知折叠、挤压动画中,由于 NSSL 允许通知之间有微小的重叠,Index 较大的 View 会遮盖在 Index 较小的 View 之上。
如果不进行精细的 changeViewPosition 调整,当通知发生位移或重叠动画时,会出现“底部的通知挡住了顶部的通知”这种严重的视觉问题。
0x03 总体流程总结
可以把整个路径简化成下面这条链:
-
NLS 回调:
NotificationListener收到通知 →NotificationEntryManager通过NotificationHandler处理新增/更新/删除。 -
数据封装和排序:
用
StatusBarNotification + RankingMap构造NotificationEntry,维护各种集合,并决定通知在逻辑上的顺序。 -
视图创建和内容绑定:
NotificationRowBinderImpl+RowInflaterTask+AsyncLayoutInflater→ 构造
ExpandableNotificationRow→ 再通过
RowContentBindStage绑定具体内容(普通布局、低优先级布局、锁屏脱敏等)。 -
加入活动列表并触发 UI 更新:
NotificationEntryManager把 entry 加入mActiveNotifications,调用updateNotifications(...)。 -
Presenter 驱动 UI 更新:
StatusBarNotificationPresenter.updateNotificationViews→
NotificationViewHierarchyManager.updateNotificationViews→ 根据最新 Entry 列表和分组关系,更新 NSSL 子 View 的增减、分组关系、顺序。
-
NSSL 渲染:
通过
addView / removeView / changeViewPosition,最终由NotificationStackScrollLayout完成通知列表的测量、布局和绘制,用户就能在通知栏看到对应的通知行了。
引用