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

未完待续

猜你喜欢

转载自blog.csdn.net/CrazyMo_/article/details/102734134
今日推荐