Canvas操作以及圖層

一、Canvas概念

畫布,應用程序繪製圖形以及控制圖形的動畫都是在其上面實現的,它提供了圖形繪製的真實表面和繪製圖形相關的接口,你的繪畫操作真正通過它被渲染在窗口的Bitmap上,使得用戶可見。

在View的OnDraw回調事件裏,可以獲得當前View的Canvas;對於SurfaceView對象你也可以通過 SurfaceHolder.lockCanvas()來獲得Canvas;假如你需要創建的一個新的Canvas,你首先需要創建的一個用於真正繪製的Bitmap,通過Bitmap來創建Canvas,代碼如下:

Bitmap b = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(b);

二、Canvas與View之間的關係

在應用程序窗口的繪製過程中,可以逐步理解到Canvas和View之間的關係。
首先由該窗口的ViewRootImpl通過SurfaceHolder.lockCanvas()來獲得創建一個Canvas,並把該Canvas傳給窗口的DecorView的draw方法,源代碼如下:

/**
     * Manually render this view (and all of its children) to the given Canvas.
     * The view must have already done a full layout before this function is
     * called.  When implementing a view, implement
     * {@link #onDraw(android.graphics.Canvas)} instead of overriding this method.
     * If you do need to override this method, call the superclass version.
     *
     * @param canvas The Canvas to which the View is rendered.
     */
    public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

        /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background
         *      2. If necessary, save the canvas' layers to prepare for fading
         *      3. Draw view's content
         *      4. Draw children
         *      5. If necessary, draw the fading edges and restore layers
         *      6. Draw decorations (scrollbars for instance)
         */

        // Step 1, draw the background, if needed
        int saveCount;

        if (!dirtyOpaque) {
            drawBackground(canvas);
        }

       .......

        // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);

        // Step 4, draw the children
        dispatchDraw(canvas);

       .......

        // Step 6, draw decorations (foreground, scrollbars)
        onDrawForeground(canvas);
    }

接下來通過dispatchDraw(canvas)分發給窗口的子View,源代碼如下:

    @Override
    protected void dispatchDraw(Canvas canvas) {

        .........
        // We will draw our child's animation, let's reset the flag
        mPrivateFlags &= ~PFLAG_DRAW_ANIMATION;
        mGroupFlags &= ~FLAG_INVALIDATE_REQUIRED;

        boolean more = false;
        final long drawingTime = getDrawingTime();

        if (usingRenderNodeProperties) canvas.insertReorderBarrier();
        final int transientCount = mTransientIndices == null ? 0 : mTransientIndices.size();
        int transientIndex = transientCount != 0 ? 0 : -1;
        // Only use the preordered list if not HW accelerated, since the HW pipeline will do the
        // draw reordering internally
        final ArrayList<View> preorderedList = usingRenderNodeProperties
                ? null : buildOrderedChildList();
        final boolean customOrder = preorderedList == null
                && isChildrenDrawingOrderEnabled();
        for (int i = 0; i < childrenCount; i++) {
            while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
                final View transientChild = mTransientViews.get(transientIndex);
                if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                        transientChild.getAnimation() != null) {
                    more |= drawChild(canvas, transientChild, drawingTime);
                }
                transientIndex++;
                if (transientIndex >= transientCount) {
                    transientIndex = -1;
                }
            }
            int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;
            final View child = (preorderedList == null)
                    ? children[childIndex] : preorderedList.get(childIndex);
            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                more |= drawChild(canvas, child, drawingTime);
            }
        }
        while (transientIndex >= 0) {
            // there may be additional transient views after the normal views
            final View transientChild = mTransientViews.get(transientIndex);
            if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                    transientChild.getAnimation() != null) {
                more |= drawChild(canvas, transientChild, drawingTime);
            }
            transientIndex++;
            if (transientIndex >= transientCount) {
                break;
            }
        }
        if (preorderedList != null) preorderedList.clear();

        // Draw any disappearing views that have animations
        if (mDisappearingChildren != null) {
            final ArrayList<View> disappearingChildren = mDisappearingChildren;
            final int disappearingCount = disappearingChildren.size() - 1;
            // Go backwards -- we may delete as animations finish
            for (int i = disappearingCount; i >= 0; i--) {
                final View child = disappearingChildren.get(i);
                more |= drawChild(canvas, child, drawingTime);
            }
        }

       ........

    }

通過ViewGroup.drawChild(Canvas canvas, View child, long drawingTime)繪製子View,源代碼如下:

 /**
     * Draw one child of this View Group. This method is responsible for getting
     * the canvas in the right state. This includes clipping, translating so
     * that the child's scrolled origin is at 0, 0, and applying any animation
     * transformations.
     *
     * @param canvas The canvas on which to draw the child
     * @param child Who to draw
     * @param drawingTime The time at which draw is occurring
     * @return True if an invalidate() was issued
     */
    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
    }

然後又通過View.draw(Canvas canvas, ViewGroup parent, long drawingTime),來循環各個繪製自己的view,源代碼代碼如下:

 /**
     * This method is called by ViewGroup.drawChild() to have each child view draw itself.
     *
     * This is where the View specializes rendering behavior based on layer type,
     * and hardware acceleration.
     */
    boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {

       .........

       if (!drawingWithRenderNode) {
            // apply clips directly, since RenderNode won't do it for this draw
            if ((parentFlags & ViewGroup.FLAG_CLIP_CHILDREN) != 0 && cache == null) {
                if (offsetForScroll) {
                    canvas.clipRect(sx, sy, sx + getWidth(), sy + getHeight());
                } else {
                    if (!scalingRequired || cache == null) {
                        canvas.clipRect(0, 0, getWidth(), getHeight());
                    } else {
                        canvas.clipRect(0, 0, cache.getWidth(), cache.getHeight());
                    }
                }
            }

            if (mClipBounds != null) {
                // clip bounds ignore scroll
                canvas.clipRect(mClipBounds);
            }
        }
        if (!drawingWithDrawingCache) {
            if (drawingWithRenderNode) {
                mPrivateFlags &= ~PFLAG_DIRTY_MASK;
                ((DisplayListCanvas) canvas).drawRenderNode(renderNode);
            } else {
                // Fast path for layouts with no backgrounds
                if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
                    mPrivateFlags &= ~PFLAG_DIRTY_MASK;
                    dispatchDraw(canvas);
                } else {
                    draw(canvas);
                }
            }
        } else if (cache != null) {
            mPrivateFlags &= ~PFLAG_DIRTY_MASK;
            if (layerType == LAYER_TYPE_NONE) {
                // no layer paint, use temporary paint to draw bitmap
                Paint cachePaint = parent.mCachePaint;
                if (cachePaint == null) {
                    cachePaint = new Paint();
                    cachePaint.setDither(false);
                    parent.mCachePaint = cachePaint;
                }
                cachePaint.setAlpha((int) (alpha * 255));
                canvas.drawBitmap(cache, 0.0f, 0.0f, cachePaint);
            } else {
                // use layer paint to draw the bitmap, merging the two alphas, but also restore
                int layerPaintAlpha = mLayerPaint.getAlpha();
                mLayerPaint.setAlpha((int) (alpha * layerPaintAlpha));
                canvas.drawBitmap(cache, 0.0f, 0.0f, mLayerPaint);
                mLayerPaint.setAlpha(layerPaintAlpha);
            }
        }

       .......
    }

最終又回到真正繪製View的Draw(Canvas canvas)方法,從而從最外層DecorView一層層繪製,最終完成整個窗口的繪製。

分析以上過程可知,一個窗口都只有Canvas對象,通過方法draw(Canvas canvas, ViewGroup parent, long drawingTime)我們可以看到,每個子View藉助對canvas的裁剪來控制View的顯示區域,某View顯示區域對應着畫布大小,該View的座標原點對應着Canvas的座標原點。

三、Canvas操作

先了解一下Canvas.save()和Canvas.restore()這兩個方法。
Canvas.save()方法:保存畫布,將當前畫布的設置保存到一個私有堆棧,用以對畫布進行某些操作後也能夠恢復到保存之前的畫布狀態。
Canvas.restore()方法:恢復畫布,彈出當前私有堆棧中棧頂的畫布狀態,來設置當前的畫布。
對Canvas進行translate,scale,rotate,skew,concat操作,都只是對其座標系的操作, 對Canvas進行clipRect, clipPath操作,只會裁剪顯示的區域大小,不會影響到Canvas的大小。

四、Canvas層

(1)當前Screen的圖層
我們每次調用Canvas的draw…方法都會產生一個透明的層,相鄰的兩個圖層的之間效果可由Paint的Xfermode設置,Paint.setXfermode(new PorterDuffXfermode(int mode)),mode類型主要分爲以下16種:

這裏寫圖片描述

1.PorterDuff.Mode.CLEAR 所繪製不會提交到畫布上。
2.PorterDuff.Mode.SRC 顯示上層繪製圖片
3.PorterDuff.Mode.DST 顯示下層繪製圖片
4.PorterDuff.Mode.SRC_OVER 正常繪製顯示,上下層繪製疊蓋。
5.PorterDuff.Mode.DST_OVER 上下層都顯示。下層居上顯示。
6.PorterDuff.Mode.SRC_IN 取兩層繪製交集。顯示上層。
7.PorterDuff.Mode.DST_IN 取兩層繪製交集。顯示下層。
8.PorterDuff.Mode.SRC_OUT 取上層繪製非交集部分。
9.PorterDuff.Mode.DST_OUT 取下層繪製非交集部分。
10.PorterDuff.Mode.SRC_ATOP 取下層非交集部分與上層交集部分
11.PorterDuff.Mode.DST_ATOP 取上層非交集部分與下層交集部分
12.PorterDuff.Mode.XOR 異或:去除兩圖層交集部分
13.PorterDuff.Mode.DARKEN 取兩圖層全部區域,交集部分顏色加深
14.PorterDuff.Mode.LIGHTEN 取兩圖層全部,點亮交集部分顏色
15.PorterDuff.Mode.MULTIPLY 取兩圖層交集部分疊加後顏色
16.PorterDuff.Mode.SCREEN 取兩圖層全部區域,交集部分變爲透明色

通過以上不同PorterDuff.Mode的Paint,來實現多個圖層之間繪畫的重疊效果,可以理解爲相鄰兩個圖層合併爲一個新的圖層,然後又與其上的相鄰圖層合併,進而在畫布上層層合併,呈現出多次繪畫的效果。
(2)幕後Screen的圖層
saveLayer (RectF bounds, Paint paint, int saveFlags):可以理解爲在後臺創建了一個臨時圖層,相比於當前Screen的圖層而言,該圖層不會直接渲染在屏幕上,只有當restore()被調用時,纔會合併到當前Screen的圖層上,否則不會合並,看不到任何繪製效果。創建這種圖層的成本比較高,尤其是bounds區域比較大時,應儘量避免採用這種方式創建圖層。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章