文章大纲
引言
前面系列文章总结了Paint 的相关知识,图形绘制中另一个十分重要的对象Canvas也是需要我们去重点掌握的,在Android无论是绘制图像还是控件都离不开Canvas,而进行绘制则需要坐标体系作为参照,那么接下来这篇文章就进行Canvas和坐标体系的相关总结。
相关文章链接如下:
- Android进阶——高级UI必知必会之2D绘制与Paint的基础应用(一)
- Android进阶——高级UI必知必会之2D绘制与使用Paint对图形进行渲染和滤镜混合处理(二)
- Android进阶——高级UI必知必会之Android坐标系与Canvas小结(三)
- Android 进阶——高级UI必知必会之统一可绘制概念Drawable详解(四)
- Android 进阶——高级UI必知必会之Path和贝塞尔曲线(五)
- Android 进阶——高级UI必知必会之借助PathMeasure打造酷炫Path特效(六)
一、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.saveLayer或saveLayerAlpha来创建新的带有透明度的图层并且放入到图层栈中(离屏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();
}
未完待续