Android進階——高級UI必知必會之圖形座標系與Canvas詳解(三)

引言

前面系列文章總結了Paint 的相關知識,圖形繪製中另一個十分重要的對象Canvas也是需要我們去重點掌握的,在Android無論是繪製圖像還是控件都離不開Canvas,而進行繪製則需要座標體系作爲參照,那麼接下來這篇文章就進行Canvas和座標體系的相關總結。

相關文章鏈接如下:

一、Android圖形座標系

如果把Android繪畫當成現實中的畫家作畫,Paint是畫家手中的“畫筆”保存了繪製的“色彩和筆刷”,Canvas自然就是畫家筆下的畫板,而畫家自然就是GPU(由Framework 層通過JNI去調用),在現實生活中畫家可以自主決定從哪個點開始起筆,又延伸到哪點,而在機器世界裏都是需要去一系列的邏輯計算的,因而圖形座標系(即在Canvas去具體繪製圖形的位置叫做座標系)應運而生,而在Android Canvas中存在兩種座標系概念::Android座標系(Canvas自己的座標系)視圖座標系(繪製座標系)
這裏寫圖片描述

1、Android座標系(Canvas自己的座標系)

Android座標系可以看成是物理存在的座標系,也可以理解爲絕對座標,是由Surface創建出來的矩形區域決定的,看成最外層面板的位置,就是以屏幕的左上角是座標系統原點(0,0),原點向右延伸是X軸正方向,原點向下延伸是Y軸正方向,準確地來說是以最頂層View的左上角爲原點,而Canvas 默認的大小就爲屏幕分辨率的大小,所以相當於是屏幕的左上角,Android座標系唯一的且一經確定不能修改,比如系統的getLocationOnScreen(int[] location)實際上獲取Android座標系中位置(即該View左上角在Android座標系中的座標),還有getRawX()、getRawY()獲取的座標也是Android座標系的座標。

2、視圖座標系(繪製座標系)

視圖座標系是相對座標系,繪製過程是以父視圖爲參照物,可以修改但過程不可逆以父視圖的左上角爲座標原點(0,0),原點向右延伸是X軸正方向,原點向下延伸是Y軸正方向,getX()、getY()就是獲取視圖座標系下的座標。

3、兩種座標系在Android的應用

這裏寫圖片描述

3.1、子View獲取自身尺寸信息

  • getHeight():獲取View自身高度
  • getWidth():獲取View自身寬度

3.2、子View獲取自身座標信息

子View的存在是依附於父View的,所以用的是相對座標來表示,如下方法可以獲得子View到其父View(ViewGroup)的距離:

  • getLeft():獲取子View自身左邊到其父View左邊的距離
  • getTop():獲取子View自身頂邊到其父View頂邊的距離
  • getRight():獲取子View自身右邊到其父View左邊的距離
  • getBottom():獲取子View自身底邊到其父View頂邊的距離
  • getMargingXxxx:獲取子View的邊框距離父ViewGroup邊框的距離即外邊距,Xxxx代表Left、Right、Top、Bootom。
  • getPaddingXxxx:獲取子View內部的內容的邊框距離子View的邊框的距離即內邊距,Xxxx代表Left、Right、Top、Bootom。

3.3、獲取MotionEvent中對應座標信息

無論是View還是ViewGroup,Touch事件都會經由onTouchEvent(MotionEvent event)方法來處理,通過MotionEvent實例event可以獲取相關座標信息。

  • getX():獲取Touch事件當前觸摸點距離控件左邊的距離,即視圖座標下對應的X軸的值
  • getY():獲取Touch事件距離控件頂邊的距離,即視圖座標系下對應的Y軸的值
  • getRawX():獲取Touch事件距離整個屏幕左邊距離,即絕對座標系下對應的X軸的值
  • getRawY():獲取Touch事件距離整個屏幕頂邊的的距離,即絕對座標系下對應的Y軸的值

3.4、獲取view在屏幕中的位置

如果在Activity的OnCreate()事件調用這些方法,那麼輸出那些參數全爲0,必須要等UI控件都加載完了才能獲取到。

  • getLocalVisibleRect() :返回一個填充的Rect對象, 所有的View都是以一塊矩形內存空間存在的

  • getGlobalVisibleRect() :獲取Android座標系的一個視圖區域, 返回一個填充的Rect對象且該Rect是基於總整個屏幕的

  • getLocationOnScreen :計算該視圖在Android座標系中的x,y值,獲取在當前屏幕內的絕對座標
    (這個值是要從屏幕頂端算起,當然包括了通知欄和狀態欄的高度)

  • getLocationInWindow ,計算該視圖在它所在的widnow的座標x,y值,獲取在整個window的絕對座標

int[] location = new int[2];
view.getLocationOnScreen(location);
int x = location[0];
int y = location[1];

二、Canvas 概述

在Google官方文檔中是這樣介紹Canvas 的(The Canvas class holds the “draw” calls),雖然字面意思翻譯爲畫布,但是本質上來說還是與我們現實中的畫布有所區別的。首先畫布並不是繪製的具體執行者,而是一個傳遞繪製信息的封裝工具類,因爲Android的2D繪製工作的核心流程是把繪製的信息保存到Canvas裏,Framework層通過JNI 調用C/C++代碼傳遞到openGL,再由openGL 傳遞給GPU,最終由GPU去完成真正的繪製,所以也可以理解爲用於與底層通信的“繪製會話”。

三、繪製的四大角色

要進行2D繪製,無論是系統控件還是自定義View都離不開Canvas,當然還有以下三大角色:

  • Bitmap——一個用於容納像素的位圖。
  • Canvas——一個用於承載繪製的具體信息,把Bitmap繪製到Canvas上,即“畫布”。
  • 繪製的基本單元,比如Rect,Path,文本,位圖
  • Paint——主要保存了文本和位圖的樣式和顏色信息,即“畫筆”。

Canvas決定了圖形繪製的位置、形狀;而Paint決定了其對應的色彩和樣式。

四、Canvas的核心創建流程淺析

在這裏插入圖片描述

涉及到到Android 源碼部分的,皆經過了精簡,只保留了與Canvas有關的重要源碼,另外在Android Studio中可以通過快鍵鍵Ctrl+Shift+N快速查找SDK中的源碼文件。

從源碼角度上來看Canvas 是由native層分配到Surface中的一塊初始大小爲屏幕分辨率的矩形繪製區域,(即我們所有的繪製都是在這個區域之內),完成了測量、佈局工作之後就開始進行繪製工作,我們先後往前推,首先從ViewRootImpl的performTraversals方法遍歷ViewTree開始

Surface——Handle onto a raw buffer that is being managed by the screen compositor.

private void performTraversals() {
        // cache mView since it is used so much below...
        final View host = mView;
        WindowManager.LayoutParams lp = mWindowAttributes;
        boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
        if (layoutRequested) {
            final Resources res = mView.getContext().getResources();
            if (mFirst) {
            	...
                mAttachInfo.mInTouchMode = !mAddedTouchMode;
                ensureTouchModeLocally(mAddedTouchMode);
            } else {
                if (!mPendingOverscanInsets.equals(mAttachInfo.mOverscanInsets)) {
                    insetsChanged = true;
                }
                if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT
                        || lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
                    windowSizeMayChange = true;

                    if (shouldUseDisplaySize(lp)) {
                        // NOTE -- system code, won't try to do compat mode.
                        Point size = new Point();
                        mDisplay.getRealSize(size);
                        desiredWindowWidth = size.x;
                        desiredWindowHeight = size.y;
                    } else {
                        Configuration config = res.getConfiguration();
                        desiredWindowWidth = dipToPx(config.screenWidthDp);
                        desiredWindowHeight = dipToPx(config.screenHeightDp);
                    }
                }
            }
			...
            // Ask host how big it wants to be
            windowSizeMayChange |= measureHierarchy(host, lp, res,
                    desiredWindowWidth, desiredWindowHeight);
        }

            if (mSurfaceHolder != null) {
                // The app owns the surface; tell it about what is going on.
                if (mSurface.isValid()) {
                    mSurfaceHolder.mSurface = mSurface;
                }
                mSurfaceHolder.setSurfaceFrameSize(mWidth, mHeight);
                mSurfaceHolder.mSurfaceLock.unlock();
                if (mSurface.isValid()) {
                    if (!hadSurface) {
                        mSurfaceHolder.ungetCallbacks();
                        mIsCreating = true;
                        SurfaceHolder.Callback callbacks[] = mSurfaceHolder.getCallbacks();
                        if (callbacks != null) {
                            for (SurfaceHolder.Callback c : callbacks) {
                                c.surfaceCreated(mSurfaceHolder);
                            }
                        }
                        surfaceChanged = true;
                    }
                    if (surfaceChanged || surfaceGenerationId != mSurface.getGenerationId()) {
                        SurfaceHolder.Callback callbacks[] = mSurfaceHolder.getCallbacks();
                        if (callbacks != null) {
                            for (SurfaceHolder.Callback c : callbacks) {
                                c.surfaceChanged(mSurfaceHolder, lp.format,
                                        mWidth, mHeight);
                            }
                        }
                    }
                    mIsCreating = false;
                } else if (hadSurface) {
                    mSurfaceHolder.ungetCallbacks();
                    SurfaceHolder.Callback callbacks[] = mSurfaceHolder.getCallbacks();
                    if (callbacks != null) {
                        for (SurfaceHolder.Callback c : callbacks) {
                            c.surfaceDestroyed(mSurfaceHolder);
                        }
                    }
                    mSurfaceHolder.mSurfaceLock.lock();
                    try {
                        mSurfaceHolder.mSurface = new Surface();
                    } finally {
                        mSurfaceHolder.mSurfaceLock.unlock();
                    }
                }
            }
			...
            final ThreadedRenderer threadedRenderer = mAttachInfo.mThreadedRenderer;
            if (!mStopped || mReportNextDraw) {
                if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
                        || mHeight != host.getMeasuredHeight() || contentInsetsChanged ||
                        updatedConfiguration) {
                    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
                    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
                     // Ask host how big it wants to be
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                    int width = host.getMeasuredWidth();
                    int height = host.getMeasuredHeight();
                    if (lp.horizontalWeight > 0.0f) {
                        width += (int) ((mWidth - width) * lp.horizontalWeight);
                        childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width,
                                MeasureSpec.EXACTLY);
                        measureAgain = true;
                    }
                    if (measureAgain) {
						//再次執行繪製
                        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                    }
                    layoutRequested = true;
                }
            }
        }
        if (!cancelDraw && !newSurface) {
			//!!!執行繪製!!!
            performDraw();
        } else {
            if (isViewVisible) {
                // Try again
                scheduleTraversals();
            } else if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
                for (int i = 0; i < mPendingTransitions.size(); ++i) {
                    mPendingTransitions.get(i).endChangingAnimations();
                }
                mPendingTransitions.clear();
            }
        }
        ...
    }

在遍歷ViewTree的方法內部會執行ViewRootImpl的performDraw方法,

private void performDraw() {
        if (mAttachInfo.mDisplayState == Display.STATE_OFF && !mReportNextDraw) {
            return;
        } else if (mView == null) {
            return;
        }
        try {
			///執行繪製
            boolean canUseAsync = draw(fullRedrawNeeded);
            if (usingAsyncReport && !canUseAsync) {
                mAttachInfo.mThreadedRenderer.setFrameCompleteCallback(null);
                usingAsyncReport = false;
            }
        } finally {
            mIsDrawing = false;
        }
		...
    }

在performDraw內部調用ViewRootImpl的draw方法,在draw方法內部首先初始化Surface(在ViewRootImpl加載時就首先通過new 創建Surface對象)

    private boolean draw(boolean fullRedrawNeeded) {
		//在ViewRootImpl加載時就首先通過new 創建Surface對象
        Surface surface = mSurface;
        if (!surface.isValid()) {
            return false;
        }
        scrollToRectOrFocus(null, false);
		...
        if (mAttachInfo.mViewScrollChanged) {
            mAttachInfo.mViewScrollChanged = false;
            mAttachInfo.mTreeObserver.dispatchOnScrollChanged();
        }
		///通過new 創建出對應的實例,並用屏幕分辨率進行初始化{@link mDirty.set(0, 0, mWidth, mHeight);}
        final Rect dirty = mDirty;
        if (mSurfaceHolder != null) {
            // The app owns the surface, we won't draw.
            dirty.setEmpty();
            if (animating && mScroller != null) {
                mScroller.abortAnimation();
            }
            return false;
        }

        if (fullRedrawNeeded) {
            mAttachInfo.mIgnoreDirtyState = true;
            dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
        }
        mAttachInfo.mTreeObserver.dispatchOnDraw();
        boolean accessibilityFocusDirty = false;
        final Drawable drawable = mAttachInfo.mAccessibilityFocusDrawable;
        if (drawable != null) {
            final Rect bounds = mAttachInfo.mTmpInvalRect;
            final boolean hasFocus = getAccessibilityFocusedRect(bounds);
            if (!hasFocus) {
                bounds.setEmpty();
            }
        }
		...
        mAttachInfo.mDrawingTime =
                mChoreographer.getFrameTimeNanos() / TimeUtils.NANOS_PER_MS;
        boolean useAsyncReport = false;
        if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
            if (mAttachInfo.mThreadedRenderer != null && mAttachInfo.mThreadedRenderer.isEnabled()) {
                if (invalidateRoot) {
                    mAttachInfo.mThreadedRenderer.invalidateRoot();
                }
                dirty.setEmpty();
                final boolean updated = updateContentDrawBounds();
                if (mReportNextDraw) {
                    mAttachInfo.mThreadedRenderer.setStopped(false);
                }
                if (updated) {
                    requestDrawWindow();
                }
                useAsyncReport = true;
                // draw(...) might invoke post-draw, which might register the next callback already.
                final FrameDrawingCallback callback = mNextRtFrameCallback;
                mNextRtFrameCallback = null;
                mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this, callback);
            } else {
                if (mAttachInfo.mThreadedRenderer != null &&
                        !mAttachInfo.mThreadedRenderer.isEnabled() &&
                        mAttachInfo.mThreadedRenderer.isRequested() &&
                        mSurface.isValid()) {

                    try {
                        mAttachInfo.mThreadedRenderer.initializeIfNeeded(
                                mWidth, mHeight, mAttachInfo, mSurface, surfaceInsets);
                    } catch (OutOfResourcesException e) {
                        handleOutOfResourcesException(e);
                        return false;
                    }
                    mFullRedrawNeeded = true;
                    scheduleTraversals();
                    return false;
                }
				///第一次執行時候,調用這個方法
                if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
                        scalingRequired, dirty, surfaceInsets)) {
                    return false;
                }
            }
        }
        return useAsyncReport;
    }

並把Surface對象傳遞至ViewRootImpl的drawSoftware方法

    private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty, Rect surfaceInsets) {

        // Draw with software renderer.
        //通過軟件渲染器進行繪圖
        final Canvas canvas;
        int dirtyXOffset = xoff;
        int dirtyYOffset = yoff;
        if (surfaceInsets != null) {
            dirtyXOffset += surfaceInsets.left;
            dirtyYOffset += surfaceInsets.top;
        }
        try {
            dirty.offset(-dirtyXOffset, -dirtyYOffset);
            final int left = dirty.left;
            final int top = dirty.top;
            final int right = dirty.right;
            final int bottom = dirty.bottom;
			///創建並初始化Canvas
            canvas = mSurface.lockCanvas(dirty);
            // TODO: Do this in native
            canvas.setDensity(mDensity);
        } catch (Surface.OutOfResourcesException e) {
            handleOutOfResourcesException(e);
            return false;
        } catch (IllegalArgumentException e) {
            mLayoutRequested = true;    // ask wm for a new surface next time.
            return false;
        } finally {
            dirty.offset(dirtyXOffset, dirtyYOffset);  // Reset to the original value.
        }
        try {
            if (!canvas.isOpaque() || yoff != 0 || xoff != 0) {
                canvas.drawColor(0, PorterDuff.Mode.CLEAR);
            }
            dirty.setEmpty();
            try {
                canvas.translate(-xoff, -yoff);
                if (mTranslator != null) {
                    mTranslator.translateCanvas(canvas);
                }
                canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0);
				//調用View的draw方法
                mView.draw(canvas);
                drawAccessibilityFocusedDrawableIfNeeded(canvas);
            } finally {
                if (!attachInfo.mSetIgnoreDirtyState) {
                    // Only clear the flag if it was not set during the mView.draw() call
                    attachInfo.mIgnoreDirtyState = false;
                }
            }
        } finally {
            try {
                surface.unlockCanvasAndPost(canvas);
            } catch (IllegalArgumentException e) {
                mLayoutRequested = true;    // ask wm for a new surface next time.
                //noinspection ReturnInsideFinallyBlock
                return false;
            }
        }
        return true;
    }

在這個方法內部通過Surface的lockCanvas方法(對應的是Surface層的nativeLockCanvas方法)創建並初始化Canvas,簡單來說就是在Surface中分配了一個預訂的矩形區域。

public Canvas lockCanvas(Rect inOutDirty)
        throws Surface.OutOfResourcesException, IllegalArgumentException {
    synchronized (mLock) {
        checkNotReleasedLocked();
        if (mLockedObject != 0) {
            throw new IllegalArgumentException("Surface was already locked");
        }
        ///真正創建並初始化Canvas
        mLockedObject = nativeLockCanvas(mNativeObject, mCanvas, inOutDirty);
        return mCanvas;
    }
}

五、Canvas的基本操作

Canvas的繪圖座標系並不是唯一不變的,它與Canvas的Matrix(3x3)有關係,當對應的Matrix發生改變的時候,繪圖座標系也隨之進行對應的變換, 而且這個過程是不可逆的,可以藉助save和restore方法來保存和還原變化操,Matrix又是通過我們設置translate、rotate、scale、skew值來進行改變的。
在這裏插入圖片描述
繪圖座標系底層是通過矩陣乘法運算的。
在這裏插入圖片描述

1、save保存操作

Canvas從底層被創建時就默認構建了一個圖層,save之前所有的操作都是在這個圖層上進行繪製的,而save作用是將之前的所有已繪製的圖像保存起來,讓後續的操作就好像在一個新的圖層上操作一樣。比如你可以先保存目前畫紙的位置(save),然後旋轉90度,向下移動100像素後畫一些圖形

2、restore還原操作

可以理解爲合併圖層操作,作用是將save()之後繪製的所有圖像與sava()之前的圖像進行合併

3、改變繪圖座標系的操作

改變繪圖座標系的操作本質上都是通過改變其對應的矩陣。

3.1、canvas.translate(x,y)

繪圖矩陣的繪圖座標系移動是一個不可逆轉的狀態也就是說,一旦矩陣移動完成之後,那麼他不能回到之前的位置,translate其實是把座標系的原點座標移動,比如說canvas.translate(200,200),則是把原點移動到原來(200,200)處,原點就是繪圖的起點處
這裏寫圖片描述

3.2、canvas.rotate(degree)

rotate(float degrees)這個方法的旋轉中心是座標的原點,對繪圖座標系進行翻轉
這裏寫圖片描述

3.3、translate和rotate

這裏寫圖片描述

六、Canvas的圖層概念

1、狀態棧

雖然繪圖座標系的轉換是一個不可逆轉的過程,但是我們可通過save保存再通過restore進行恢復,其實我們在進行save操作時,就是在Canvas當中將我們save下來的座標系保存到一個狀態棧,執行restore或者是restoreToCount時再從狀態棧中還原回來。簡而言之,每一次的save操作本質上是把當前繪圖座標系入棧,而restore或者restoreToCount就是出棧的,通過save、 restore方法來保存和還原變換操作Matrix以及Clip剪裁
這裏寫圖片描述

/**
 * Auther: Crazy.Mo
 * DateTime: 2017/4/28 16:34
 * Summary:
 */
public class ClockView extends View {
    private Context context;
    private Paint paintOutSide,paintDegree;
    private float outWidth,outHeight;

    public ClockView(Context context) {
        this(context, null);
        init(context);
    }

    public ClockView(Context context, AttributeSet attrs) {
        this(context, attrs,0);
        init(context);
    }

    public ClockView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    private void init(Context context){
        this.context=context;
        initOutSize();
    }

    private void initOutSize(){
        WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);//獲取WM對象
        DisplayMetrics dm = new DisplayMetrics();
        manager.getDefaultDisplay().getMetrics(dm);
        outHeight=(float) dm.heightPixels;//獲取真實屏幕的高度以px爲單位
        outWidth=(float)dm.widthPixels;
    }

    /**
     * 畫外圈圓
     * @param canvas
     */
    private void drawOutCircle(Canvas canvas){
        paintOutSide=new Paint();
        paintOutSide.setColor(Color.GREEN);
        paintOutSide.setStyle(Paint.Style.STROKE);
        paintOutSide.setAntiAlias(true);
        paintOutSide.setDither(true);
        paintOutSide.setStrokeWidth(6f);
        canvas.drawCircle(outWidth/2.0f,outHeight/2.0f,outWidth/2.0f,paintOutSide);
    }

    /**
     * 畫刻度
     */
    private void drawDegree(Canvas canvas){
        paintDegree=new Paint();
        paintDegree.setColor(Color.RED);
        paintDegree.setStyle(Paint.Style.STROKE);
        paintDegree.setAntiAlias(true);
        paintDegree.setDither(true);
        paintDegree.setStrokeWidth(3f);
        for(int i=0;i<24;i++){
            if(i==0||i==6||i==12||i==18){
                paintDegree.setStrokeWidth(6f);
                paintDegree.setTextSize(30);
                canvas.drawLine(outWidth/2.0f,(outHeight/2.0f-outWidth/2.0f),outWidth/2.0f,(outHeight/2.0f-outWidth/2.0f+60),paintDegree);
                String degreeTxt=String.valueOf(i);
                canvas.drawText(degreeTxt,(outWidth/2-paintDegree.measureText(degreeTxt)/2),(outHeight/2-outWidth/2+90),paintDegree);
            }else {
                paintDegree.setStrokeWidth(4f);
                paintDegree.setTextSize(20);
                canvas.drawLine(outWidth/2.0f,(outHeight/2.0f-outWidth/2.0f),outWidth/2.0f,(outHeight/2.0f-outWidth/2.0f+40),paintDegree);
                String degreeTxt=String.valueOf(i);
                canvas.drawText(degreeTxt,(outWidth/2-paintDegree.measureText(degreeTxt)/2)+20,(outHeight/2-outWidth/2+40),paintDegree);
            }
            canvas.rotate(15,outWidth/2,outHeight/2);
        }
    }

    private void drawPointor(Canvas canvas){
        Paint paintHour=new Paint();
        paintHour.setColor(Color.RED);
        paintHour.setStyle(Paint.Style.STROKE);
        paintHour.setAntiAlias(true);
        paintHour.setDither(true);
        paintHour.setStrokeWidth(12f);
        Paint paintMin=new Paint();
        paintMin.setColor(Color.RED);
        paintMin.setStyle(Paint.Style.STROKE);
        paintMin.setAntiAlias(true);
        paintMin.setDither(true);
        paintMin.setStrokeWidth(8f);
        canvas.save();
        canvas.translate(outWidth/2,outHeight/2);
        canvas.drawLine(0,0,100,100,paintHour);
        canvas.drawLine(0,0,100,150,paintMin);
        canvas.restore();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawOutCircle(canvas);
        drawDegree(canvas);
        drawPointor(canvas);
    }
}

2、Layer棧

與狀態棧概念類似的還有一個Layer棧,Android中的繪圖機制很多都是借鑑了Photoshop的概念,在Photoshop中一張原始的素材可能是由很多圖層疊加而成,Android也借鑑了這一機制,所謂Layer圖層其本質就是內存中一塊矩形的區域,在Android中圖層是基於棧的數據結果進行管理的,通過方法canvas.saveLayersaveLayerAlpha創建新的帶有透明度的圖層並且放入到圖層棧中(離屏Bitmap-離屏緩衝),並且會將saveLayer之前的一些Canvas操作延續過來,後續的繪圖操作都在新建的layer上面進行,出棧則是通過方法restore、restoreToCount,出入棧造成的操作區別是:入棧時所有的繪製操作都發生在當前這個圖層,而出棧之後則會把操作繪製到上一個圖層。


    @Override
    protected void onDraw(Canvas canvas) {
        //相當於是默認繪製白色背景、藍色圓在整個畫布上,可以看成PS中的背景
        canvas.drawColor(Color.WHITE);
        paintOutSide.setColor(Color.BLUE);
        canvas.drawCircle(100,100,100,paintOutSide);
   
        canvas.saveLayerAlpha(0,0,400,400,125,ALL_SAVE_FLAG);//執行saveLayerAlpha 相當於是創建了一個新的圖層繪製紅色圓,其中125代表alpha值0~255,你可以嘗試着修改透明值進行測試可以加深對於圖層的理解
        paintOutSide.setColor(Color.RED);
        canvas.drawCircle(150,150,100,paintOutSide);
        canvas.restore();
        
    }

未完待續

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