文章大綱
引言
前面系列文章總結了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();
}
未完待續