探索RippleDrawable作為背景是如何繪制到View外的

某天某時某刻,腦內(nèi)突然發(fā)現(xiàn)一個疑問:RippleDrawable 是怎么把波紋繪制到所在 View 外面的?

稍微了解點 Android 繪制知識的就知道,子 ViewonDraw(canvas) 獲取到的畫布默認(rèn)是被父親裁剪掉的,導(dǎo)致子 View 無法繪制到自身外面

那么問題就來了,為毛 RippleDrawable 可以繪制到外面,用了什么原理?莫非有特權(quán)?

帶著問題先看了下 RippleDrawable 的源碼,恩!很好,完全沒看出啥東西
(╯°□°)╯︵┻━┻ 。。。

網(wǎng)上搜了一波,恩!nice,沒找到答案
(╯°□°)╯︵┻━┻

正當(dāng)我漏氣時,突然想到會不會是硬件加速搞的鬼?寫了一個簡單的 demo,關(guān)掉硬件加速的時候,波紋就畫不出去了,啟用硬件加速的時候,就畫出去了

沿著這個思路重新看了下 RippleDrawable 的源碼,發(fā)現(xiàn)一個沒看懂的 override 函數(shù),而且還是 @hide 的:

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

這是啥?網(wǎng)上搜了一波這函數(shù),終于在老羅這篇文章中搜到了答案:

... 本來DisplayListRenderer類的成員函數(shù)addRenderNodeOp執(zhí)行到這里,就已經(jīng)完成任務(wù)了。但是在Android 5.0中,增加了一個新的API——RippleDrawable。RippleDrawable有一個屬性,當(dāng)它沒有包含任何的Layer時,它將被投影到當(dāng)前視圖的設(shè)置有Background的最近的一個父視圖的Background去。這一點可以參考官方文檔

為了達(dá)到上述目的,每一個Render Node都具有三個屬性:Projection Receive Index、Projection Receiver和Projection Backwards。其中,Projection Receive Index是一個整型變量,而Projection Receiver和Projection Backwards是兩個布爾變量。注意,在一個應(yīng)用程序窗口的視圖結(jié)構(gòu)中,每一個View及其設(shè)置的Background都對應(yīng)一個Render Node。上述三個屬性構(gòu)成了Render Node里面的一個Projection Nodes的概念,如圖3所示 ...

摘自老羅的文章

如果你想詳細(xì)了解硬件加速的原理的話,看老羅的文章,我下面就對 RippleDrawable 做一下簡單的解釋


硬件加速的情況下,Android 5.0 以后的應(yīng)用程序 UI 繪制是分為兩步的

第一步構(gòu)建 DisplayList,里面記錄了 View 的繪制命令集合,發(fā)生在主進程 MainThread

第二步是渲染 DisplayList,把這些繪制命令轉(zhuǎn)為 Open GL 的命令,然后交給 GPU 執(zhí)行

Android 中的 View 都被抽象成一個 RenderNode(一個 RenderNode包含了自己和兒子的DisplayList,除了TextureView和軟件渲染的子視圖不包含DisplayList如果這個 View 有背景的話,也會被抽象成一個 RenderNode

也就是說,硬件加速最后繪制的東西全部都存在 DisplayList 中,那么就來看看 View 是怎么更新背景的 DisplayList

先看 ViewdrawBackground(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 被調(diào)用 draw(Canvas) 的時候,繪制背景的函數(shù),如果啟用了硬件加速,背景被轉(zhuǎn)換成一個 RenderNode,然后被繪制到 Canvas

看一下 DisplayListCanvas 內(nèi)部的 drawRenderNode 函數(shù)

/**
     * 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());
}

調(diào)用了 native 方法,需要查看 framework 方法才看得到了,查找了一波后,找到了實現(xiàn)的地方

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

謝天謝地的終于找到了這個函數(shù)的用處 isProjectionReceiver(),讓我稍微講解下吧

drawRenderNode() 函數(shù)是把 RenderNode 封裝成一個 DrawRenderNodeOp 然后丟給 addRenderNodeOp() 函數(shù)

RenderNode 包含了繪制命令

addRenderNodeOp() 中首先把 op 加入到了 CanvasmDisplayList 中,opIndex 就是當(dāng)前的 opmDisplayList 中的位置。

然后在最后,那個 if 判斷,判斷當(dāng)前的 RenderNode 是否是isProjectionReceiver 的,如果是的,那么把當(dāng)前 opindex 賦值給 mDisplayList->projectReceiveIndex 這有什么用呢?這樣 mDisplayList 不就知道了我那個需要特殊待遇的 RenderNode 是誰了嗎?

前面說了 Backgound 自成一個 RenderNode,所以 RippleDrawable 自成一個 RenderNode,既然 DisplayList 都知道了這個特殊的 RenderNode,那么繪制的時候優(yōu)先繪制這個 RenderNode,就可以畫到外面了


硬件加速涉及的東西有點多,建議還是看下老羅的文章,雖然長篇大論,不過源碼分析透徹

原文鏈接

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容