探索RippleDrawable作为背景是如何绘制到View外的

某天某时某刻,脑内突然发现一个疑问:RippleDrawable 是怎么把波纹绘制到所在 View 外面的?

稍微了解点 Android 绘制知识的就知道,子 View 的 onDraw(canvas) 获取到的画布默认是被父亲裁剪掉的,导致子 View 无法绘制到自身外面

那么问题就来了,为毛 RippleDrawable 可以绘制到外面,用了什么原理?莫非有特权?

带着问题先看了下 RippleDrawable 的源码,恩!很好,完全没看出啥东西
(╯°□°)╯︵┻━┻ 。。。

网上搜了一波,恩!nice,没找到答案
(╯°□°)╯︵┻━┻

正当我漏气时,突然想到会不会是硬件加速搞的鬼?写了一个简单的 demo,关掉硬件加速的时候,波纹就画不出去了,启用硬件加速的时候,就画出去了

沿着这个思路重新看了下 RippleDrawable 的源码,发现一个没看懂的 override 函数,而且还是 @hide 的:

/**
 * @hide
 */
@Override
public boolean isProjected() {
    // If the layer is bounded, then we don't need to project.
    if (isBounded()) {
        return false;
    }
    
    ...
    
    return true;
}

这是啥?网上搜了一波这函数,终于在老罗这篇文章中搜到了答案:

... 本来DisplayListRenderer类的成员函数addRenderNodeOp执行到这里,就已经完成任务了。但是在Android 5.0中,增加了一个新的API——RippleDrawable。RippleDrawable有一个属性,当它没有包含任何的Layer时,它将被投影到当前视图的设置有Background的最近的一个父视图的Background去。这一点可以参考官方文档

为了达到上述目的,每一个Render Node都具有三个属性:Projection Receive Index、Projection Receiver和Projection Backwards。其中,Projection Receive Index是一个整型变量,而Projection Receiver和Projection Backwards是两个布尔变量。注意,在一个应用程序窗口的视图结构中,每一个View及其设置的Background都对应一个Render Node。上述三个属性构成了Render Node里面的一个Projection Nodes的概念,如图3所示 ...

摘自老罗的文章

如果你想详细了解硬件加速的原理的话,看老罗的文章,我下面就对 RippleDrawable 做一下简单的解释


硬件加速的情况下,Android 5.0 以后的应用程序 UI 绘制是分为两步的

第一步构建 DisplayList,里面记录了 View 的绘制命令集合,发生在主进程 MainThread 中

第二步是渲染 DisplayList,把这些绘制命令转为 Open GL 的命令,然后交给 GPU 执行

Android 中的 View 都被抽象成一个 RenderNode(一个 RenderNode包含了自己和儿子的DisplayList,除了TextureView和软件渲染的子视图不包含DisplayList) ,如果这个 View 有背景的话,也会被抽象成一个 RenderNode

也就是说,硬件加速最后绘制的东西全部都存在 DisplayList 中,那么就来看看 View 是怎么更新背景的 DisplayList 的

先看 View 的 drawBackground(Canvas) 方法

/**
* Draws the background onto the specified canvas.
*
* @param canvas Canvas on which to draw the background
*/
private void drawBackground(Canvas canvas) {
    final Drawable background = mBackground;
    if (background == null) {
        return;
    }

    setBackgroundBounds();

    // Attempt to use a display list if requested.
    if (canvas.isHardwareAccelerated() && mAttachInfo != null
            && mAttachInfo.mHardwareRenderer != null) {
        // 获取一个背景的 render node
        mBackgroundRenderNode = getDrawableRenderNode(background, mBackgroundRenderNode);

        final RenderNode renderNode = mBackgroundRenderNode;
        if (renderNode != null && renderNode.isValid()) {
            setBackgroundRenderNodeProperties(renderNode);
            ((DisplayListCanvas) canvas).drawRenderNode(renderNode);
            return;
        }
    }

    // ...
}

上面的代码是 View 被调用 draw(Canvas) 的时候,绘制背景的函数,如果启用了硬件加速,背景被转换成一个 RenderNode,然后被绘制到 Canvas 上

看一下 DisplayListCanvas 内部的 drawRenderNode 函数

/**
     * Draws the specified display list onto this canvas. The display list can only
     * be drawn if {@link android.view.RenderNode#isValid()} returns true.
     *
     * @param renderNode The RenderNode to draw.
     */
public void drawRenderNode(RenderNode renderNode) {
    nDrawRenderNode(mNativeCanvasWrapper, renderNode.getNativeDisplayList());
}

调用了 native 方法,需要查看 framework 方法才看得到了,查找了一波后,找到了实现的地方

void DisplayListCanvas::drawRenderNode(RenderNode* renderNode) {
    LOG_ALWAYS_FATAL_IF(!renderNode, "missing rendernode");
    DrawRenderNodeOp* op = new (alloc()) DrawRenderNodeOp(
            renderNode,
            *mState.currentTransform(),
            mState.clipIsSimple());
    addRenderNodeOp(op);
}

size_t DisplayListCanvas::addRenderNodeOp(DrawRenderNodeOp* op) {
    int opIndex = addDrawOp(op);
#if !HWUI_NEW_OPS
    int childIndex = mDisplayList->addChild(op);

    // update the chunk's child indices
    DisplayList::Chunk& chunk = mDisplayList->chunks.back();
    chunk.endChildIndex = childIndex + 1;

    if (op->renderNode->stagingProperties().isProjectionReceiver()) {
        // use staging property, since recording on UI thread
        mDisplayList->projectionReceiveIndex = opIndex;
    }
#endif
    return opIndex;
}

谢天谢地的终于找到了这个函数的用处 isProjectionReceiver(),让我稍微讲解下吧

drawRenderNode() 函数是把 RenderNode 封装成一个 DrawRenderNodeOp 然后丢给 addRenderNodeOp() 函数

RenderNode 包含了绘制命令

addRenderNodeOp() 中首先把 op 加入到了 Canvas 的 mDisplayList 中,opIndex 就是当前的 op 在 mDisplayList 中的位置。

然后在最后,那个 if 判断,判断当前的 RenderNode 是否是isProjectionReceiver 的,如果是的,那么把当前 op 的 index 赋值给 mDisplayList->projectReceiveIndex 这有什么用呢?这样 mDisplayList 不就知道了我那个需要特殊待遇的 RenderNode 是谁了吗?

前面说了 Backgound 自成一个 RenderNode,所以 RippleDrawable 自成一个 RenderNode,既然 DisplayList 都知道了这个特殊的 RenderNode,那么绘制的时候优先绘制这个 RenderNode,就可以画到外面了


硬件加速涉及的东西有点多,建议还是看下老罗的文章,虽然长篇大论,不过源码分析透彻

原文链接

最后编辑于 :
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容