SystemUI 开发之 NSSL.addContainerView() 之后发生了什么(八)

66 阅读6分钟

在前文的代码追踪中 NotificationViewHierarchyManagerExpandableNotificationRow 通过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() ,就在这个方法中触发measurelayoutdraw 的操作。

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;
        }
    }
}

这个过程可以简化如下:

addViewrequestLayoutscheduleTraversals →监听VSYNCdoTraversalperformTraversalsmeasure + 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 操作后除了触发 ViewRootImplVSYNC信号订阅外,还回触发 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 中的时机选择非常精妙:

  1. 即刻配置(onViewAdded 中):在新通知视图进入容器的第一时间,就完成所有状态同步和关系绑定。此时,视图尚未布局。
  2. 动态响应(后续 layout 过程中):由于提前设置了 OnHeightChangedListener,当通知在后续交互中展开/折叠(改变高度)时,NSSL 能在监听器回调中再次触发 requestLayout()
  3. 闭环形成:这就形成了一个 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、设置容器大小

setMaxLayoutHeightupdateContentHeight 设置通知列表高度。

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 非常重要,它是通知列表的排序算法的逻辑计算模块