opoojkk

为什么设置 clipChildren = false 可以突破父 View 边界?

lxx
目次

设置clipChildren = false #

默认情况下,clipChildrentrue 的。也就是说,在 ViewGroup 绘制时,子 View 超出父 ViewGroup 范围的部分会被裁剪掉。当然,这个行为是可以通过设置关闭的。

如果将 clipChildren 设置为 false,允许子 View 绘制到父容器之外,Framework 内部到底做了什么?这篇就从源码角度来捋一捋这个过程。

setClipChildren 做了哪些事? #

入口很清晰,就是 ViewGroup#setClipChildren

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/**
 * By default, children are clipped to their bounds before drawing. This
 * allows view groups to override this behavior for animations, etc.
 *
 * @param clipChildren true to clip children to their bounds,
 *        false otherwise
 * @attr ref android.R.styleable#ViewGroup_clipChildren
 */
public void setClipChildren(boolean clipChildren) {
    boolean previousValue = (mGroupFlags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN;
    if (clipChildren != previousValue) {
        setBooleanFlag(FLAG_CLIP_CHILDREN, clipChildren);
        for (int i = 0; i < mChildrenCount; ++i) {
            View child = getChildAt(i);
            if (child.mRenderNode != null) {
                child.mRenderNode.setClipToBounds(clipChildren);
            }
        }
        invalidate(true);
    }
}

整体逻辑并不复杂,设置后一共做了四件事:

  1. 判断之前的状态,避免重复设置;
  2. clipChildren 状态保存到 ViewGroup 的 flag 中;
  3. 遍历所有子 View,同步更新其 RenderNode
  4. 触发一次强制重绘。

其中,真正值得多看一眼的只有第 3 步。

RenderNode#setClipToBounds() #

View 中的 mRenderNode 是渲染系统的核心对象:

1
2
3
4
5
6
7
8
9
/**
 * RenderNode holding View properties, potentially holding a DisplayList of View content.
 * <p>
 * When non-null and valid, this is expected to contain an up-to-date copy
 * of the View content. Its DisplayList content is cleared on temporary detach and reset on
 * cleanup.
 */
@UnsupportedAppUsage
final RenderNode mRenderNode;

当调用:

1
child.mRenderNode.setClipToBounds(clipChildren)

本质上是在告诉渲染系统: 这个 View 对应的 RenderNode 是否需要将自身绘制内容裁剪到其 bounds 之内。

继续往下看:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/**
 * Set whether the Render node should clip itself to its bounds. This defaults to true,
 * and is useful to the renderer in enable quick-rejection of chunks of the tree as well as
 * better partial invalidation support.
 *
 * @param clipToBounds true if the display list should clip to its bounds, false otherwise.
 * @return True if the value changed, false if the new value was the same as the previous value.
 */
public boolean setClipToBounds(boolean clipToBounds) {
    return nSetClipToBounds(mNativeRenderNode, clipToBounds);
}

这里已经很明确了: clipChildren 最终并不是通过 Java 层 Canvas 裁剪实现的,而是直接下沉到了 RenderNode / HWUI 层。

nSetClipToBounds 在 Native 层 #

继续向下追,逻辑就进入了 AOSP 的 C++ 渲染管线中,这一点非常合理——毕竟最终执行绘制的是 HWUI。

简单来说,nSetClipToBounds 并不会立刻执行裁剪操作,而是:

也就是说:

clipToBounds 是一个渲染属性声明, 而不是一次即时裁剪操作。 成员变量只是状态载体,真正的效果体现在后续渲染流程中。

如下图所示(保持原图):

clipToBounds 如何真正影响绘制? #

在 HWUI 的 RenderNode 树遍历过程中,clipToBounds 会参与多项关键逻辑:

clipChildren = false 时:

换句话说:

收集到的子 View 绘制区域,会在不受裁剪限制的情况下, 合并进已有的 dirty region,参与后续重绘。

小结 #

从 AOSP 16 的实现来看,clipChildren 的本质并不是一个简单的「是否裁剪 Canvas」开关,而是:

这也解释了为什么它对动画、悬浮效果以及性能都有直接影响。

标签:
Categories: