在前文的代码追踪中 NotificationViewHierarchyManager 将 ExpandableNotificationRow 通过addContainerView 方法传给了NotificationStackScrollLayout 并使用 addView 添加到子View中。接下来看看这一个过程。
0x00、NSSL.addContainerView()
public void addContainerView(View v) {
Assert.isMainThread();
addView(v);
}
0x01、ViewGroup.addView()
public void addView(View child) {
addView(child, -1);
}
...
public void addView(View child, int index, LayoutParams params) {
if (DBG) {
System.out.println(this + " addView");
}
if (child == null) {
throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
}
// addViewInner() will call child.requestLayout() when setting the new LayoutParams
// therefore, we call requestLayout() on ourselves before, so that the child's request
// will be blocked at our level
// 1、执行了requstLayout
requestLayout();
invalidate(true);
// 2、执行 addView 操作
addViewInner(child, index, params, false);
}
ViewGroup.addView() 会执行到 requestLayout() 中来
1、View.requestLayout()
requestLayout() 会不停地向上查找直到 ViewRootImpl
public void requestLayout() {
if (mMeasureCache != null) mMeasureCache.clear();
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
// Only trigger request-during-layout logic if this is the view requesting it,
// not the views in its parent hierarchy
ViewRootImpl viewRoot = getViewRootImpl();
if (viewRoot != null && viewRoot.isInLayout()) {
if (!viewRoot.requestLayoutDuringLayout(this)) {
return;
}
}
mAttachInfo.mViewRequestingLayout = this;
}
mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;
if (mParent != null && !mParent.isLayoutRequested()) {
// 开始向上回溯
mParent.requestLayout();
}
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
mAttachInfo.mViewRequestingLayout = null;
}
}
2、ViewRootImpl.requestLayout()
在 ViewRootImpl 中执行了 scheduleTraversals()
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
// 又到了这个非常著名的方法了
scheduleTraversals();
}
}
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
// 发送同步屏障
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
// 注册 VSYNC 信号,监听回调触发UI渲染
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
在 mTraversalRunnable 回调中最后执行 doTraversal() ,在此方法中执行了 performTraversals() ,就在这个方法中触发measure、layout 和 draw 的操作。
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
// 移除同步屏障
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
// 执行 measure、layout、draw 三大操作
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
这个过程可以简化如下:
addView→ requestLayout → scheduleTraversals →监听VSYNC→ doTraversal → performTraversals → measure + layout + draw
3、ViewGroup.addViewInner()
接下来在 addViewInner 方法中触发了 dispatchViewAdded
private void addViewInner(View child, int index, LayoutParams params,
boolean preventRequestLayout) {
...
addInArray(child, index);
// tell our children
if (preventRequestLayout) {
child.assignParent(this);
} else {
child.mParent = this;
}
if (child.hasUnhandledKeyListener()) {
incrementChildUnhandledKeyListeners();
}
final boolean childHasFocus = child.hasFocus();
if (childHasFocus) {
requestChildFocus(child, child.findFocus());
}
AttachInfo ai = mAttachInfo;
if (ai != null && (mGroupFlags & FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW) == 0) {
boolean lastKeepOn = ai.mKeepScreenOn;
ai.mKeepScreenOn = false;
child.dispatchAttachedToWindow(mAttachInfo, (mViewFlags&VISIBILITY_MASK));
if (ai.mKeepScreenOn) {
needGlobalAttributesUpdate(true);
}
ai.mKeepScreenOn = lastKeepOn;
}
if (child.isLayoutDirectionInherited()) {
child.resetRtlProperties();
}
// 分发添加
dispatchViewAdded(child);
...
}
进入了 dispatchViewAdded()
void dispatchViewAdded(View child) {
onViewAdded(child);
if (mOnHierarchyChangeListener != null) {
mOnHierarchyChangeListener.onChildViewAdded(this, child);
}
}
/**
* Called when a new child is added to this ViewGroup. Overrides should always
* call super.onViewAdded.
*
* @param child the added child view
*/
public void onViewAdded(View child) {
// 这个方法给子类实现用的
}
在这里又回调了子类实现的 onViewAdded()
即当执行 addView 操作后除了触发 ViewRootImpl 的VSYNC信号订阅外,还回触发 onViewAdded 方法。
0x02、NSSL.onViewAdded()
public void onViewAdded(View child) {
super.onViewAdded(child);
onViewAddedInternal((ExpandableView) child);
}
private void onViewAddedInternal(ExpandableView child) {
// 锁屏时自动隐藏敏感通知内容
updateHideSensitiveForChild(child);
// 监听通知项的展开/折叠,实现流畅的连锁动画和布局调整
child.setOnHeightChangedListener(this);
//新通知滑入、淡入的动画效果,提供操作响应感
generateAddAnimation(child, false /* fromMoreCard */);
//确保新通知的动画与整个列表的展开/折叠状态同步
updateAnimationState(child);
//更新通知中的“时间戳”(如“刚刚”),确保信息准确
updateChronometerForChild(child);
if (child instanceof ExpandableNotificationRow) {
((ExpandableNotificationRow) child).setDismissRtl(mDismissRtl);
}
if (ANCHOR_SCROLLING) {
// TODO: once we're recycling this will need to check the adapter position of the child
if (child == getFirstChildNotGone() && (isScrolledToTop() || !mIsExpanded)) {
// New child was added at the top while we're scrolled to the top;
// make it the new anchor view so that we stay at the top.
mScrollAnchorView = child;
}
}
}
与 requestLayout 流程的关系
将这些逻辑置于 onViewAdded 中的时机选择非常精妙:
- 即刻配置(
onViewAdded中):在新通知视图进入容器的第一时间,就完成所有状态同步和关系绑定。此时,视图尚未布局。 - 动态响应(后续
layout过程中):由于提前设置了OnHeightChangedListener,当通知在后续交互中展开/折叠(改变高度)时,NSSL 能在监听器回调中再次触发requestLayout()。 - 闭环形成:这就形成了一个
addView→ 配置 →layout→ 用户交互 → 高度变化 → 回调 → 再次requestLayout→ 重新layout的完美闭环,支撑起整个动态列表。
简单来说,onViewAdded 中的代码为每个新通知装上了 “遥控器” 和 “定位器” ,让 NSSL 这个“总控台”能够实时感知并精确控制每一个成员,从而实现高度动态、流畅的通知列表效果。
接下来看看 layout 过程。
0x03、NSSL.onLayout()
1、首先所有子 View 居中置顶
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// we layout all our children centered on the top
// 计算中心点位置
float centerX = getWidth() / 2.0f;
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
// We need to layout all children even the GONE ones, such that the heights are
// calculated correctly as they are used to calculate how many we can fit on the screen
float width = child.getMeasuredWidth();
float height = child.getMeasuredHeight();
// left = centerX - width / 2.0f
// 目的让 child 居中,top 固定为 0
child.layout((int) (centerX - width / 2.0f),
0,
(int) (centerX + width / 2.0f),
(int) height);
}
// 锁定 NSSL 的最大可用高度,并将此高度传递给 mShelf(通知托盘)。
// 这决定了通知在滚动到哪个位置时应该开始“收缩”进入底部托盘。
setMaxLayoutHeight(getHeight());
// 计算当前所有通知的总高度(Intrinsic Height),这个加上通知之间的间距(Padding)
// 以及 Section 之间的间隔
updateContentHeight();
// 防止由于布局改变(例如某条通知消失)导致的“非法滚动”。
// 如果当前滚动量超过了新的内容总高度,它会将滚动条强制回弹到合法范围内。
clampScrollPosition();
// 这个方法向 ViewTreeObserver 注册一个 onPreDraw 监听。
// 在下一帧绘制前,**StackScrollAlgorithm** 会介入,
// 根据当前状态计算出每个 View 真正应该存在的 TranslationY
requestChildrenUpdate();
// 更新背景,通知列表是一个整体,需要为有圆角和边界的Item确定效果,
updateFirstAndLastBackgroundViews();
// 更新算法所需的最小高度
updateAlgorithmLayoutMinHeight();
// 根据通知在堆栈中的位置(是否有悬浮、是否是第一条)动态调整 TranslationZ。
// 这是渲染阴影的关键,确保上层的通知阴影能正确投射在下层通知上
updateOwnTranslationZ();
}
为什么top是0? NSSL 是一个高度动态的列表,通知的 Y 轴坐标受到滚动、展开进度、阻尼回弹、分组折叠等几十个变量影响。如果在 onLayout 中硬编码 Y 坐标,会导致频繁的 requestLayout,性能极差。因此,NSSL 选择在 onLayout 中完成基础定位,而将复杂的 Y 轴偏移交给 setTranslationY()
**这样做的好处是:**当用户滚动列表时,NSSL 只需要改变子 View 的 TranslationY,这由 GPU 处理,不需要触发代价昂贵的全局 layout 过程,从而保证了通知中心滑动的极致流畅。
2、设置容器大小
setMaxLayoutHeight 和 updateContentHeight 设置通知列表高度。
3、触发渲染
requestChildrenUpdate它会向 ViewTreeObserver 注册一个 onPreDraw 监听。在下一帧绘制前,StackScrollAlgorithm 会介入,根据当前状态计算出每个 View 真正应该存在的 TranslationY。
总结
- 结构层面:
addView触发requestLayout,一路到ViewRootImpl,由 VSYNC 驱动一次完整的measure/layout/draw。 - 控制层面:
onViewAdded()中完成新通知的状态绑定和监听器安装,使其之后的任何高度和状态变化,都能回流到 NSSL 的布局和动画系统。 - 布局与动画层面:
onLayout()做统一基准布局,requestChildrenUpdate()+StackScrollAlgorithm在下一帧前计算并下发真正的 TranslationY 和 Z,从而构建出一个依赖 GPU 合成、性能友好的动态通知列表。
NSSL.addContainerView() 不只是“把 View 插进列表”,而是通过 requestLayout + VSYNC + onViewAdded + 算法驱动 TranslationY,把这条通知完全纳入到 NSSL 的滚动、动画和可见性控制闭环中,既保证结构正确,又保证交互流畅。
StackScrollAlgorithm对于NSSL非常重要,它是通知列表的排序算法的逻辑计算模块