自定義控制元件三部曲之繪圖篇(十三)——Canvas與圖層(一)
前言:猛然知道姥姥79了,我好怕,好想哭
系列文章:
一、如何獲得一個Canvas物件
方法一:自定義view時, 重寫onDraw、dispatchDraw方法
(1)、定義
我們先來看一下onDraw、dispatchDraw方法的定義protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
}
可以看到onDraw、dispatchDraw在傳入的引數中都有一個canvas物件。這個canvas物件是View中的Canvas物件,利用這個canvas物件繪圖,效果會直接反應在View中; (2)、onDraw、dispatchDraw區別
- onDraw()的意思是繪製檢視自身
- dispatchDraw()是繪製子檢視
但在ViewGroup中,當它有背景的時候就會呼叫onDraw()方法,否則就會跳過onDraw()直接呼叫dispatchDraw();所以如果要在ViewGroup中繪圖時,往往是重寫dispatchDraw()方法
在View中,onDraw()和dispatchDraw()都會被呼叫的,所以我們無論把繪圖程式碼放在onDraw()或者dispatchDraw()中都是可以得到效果的,但是由於dispatchDraw()的含義是繪製子控制元件,所以原則來上講,在繪製View控制元件時,我們是重新onDraw()函式
所以結論來了:
在繪製View控制元件時,需要重寫onDraw()函式,在繪製ViewGroup時,需要重寫dispatchDraw()函式。
方法二:使用Bitmap建立
1、構建方法
使用:Canvas c = new Canvas(bitmap);
或Canvas c = new Canvas();
c.setBitmap(bitmap);
其中bitmap可以從圖片載入,也可以建立,有下面幾種方法這兩個方法是最常用的,除了這兩個方法以外,還有其它幾個方法(比如構造一個具有matrix的影象副本——前面示例中的倒影影象),這裡就不再涉及了,大家可以去檢視Bitmap的建構函式。//方法一:新建一個空白bitmap Bitmap bmp = Bitmap.createBitmap(width ,height Bitmap.Config.ARGB_8888); //方法二:從圖片中載入 Bitmap bmp = BitmapFactory.decodeResource(getResources(),R.drawable.wave_bg,null);
2、在OnDraw()中使用
我們一定要注意的是,如果我們用bitmap構造了一個canvas,那這個canvas上繪製的影象也都會儲存在這個bitmap上,而不是畫在View上,如果想畫在View上就必須使用OnDraw(Canvas canvas)函式中傳進來的canvas畫一遍bitmap才能畫到view上。下面舉個例子:
public class BitmapCanvasView extends View {
private Bitmap mBmp;
private Paint mPaint;
private Canvas mBmpCanvas;
public BitmapCanvasView(Context context, AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint();
mPaint.setColor(Color.RED);
mBmp = Bitmap.createBitmap(500 ,500 , Bitmap.Config.ARGB_8888);
mBmpCanvas = new Canvas(mBmp);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setTextSize(100);
mBmpCanvas.drawText("啟艦大SB",0,100,mPaint);
}
}
我們先看一下執行結果:可以看到,毛線也沒有,這是為什麼呢?
我們仔細來看一下onDraw函式:
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setTextSize(100);
mBmpCanvas.drawText("啟艦大SB",0,100,mPaint);
}
在onDraw函式中,我們只是將文字畫在了mBmpCanvas上,也就是我們新建mBmp圖片上!這個圖片跟我們view沒有任何關係好吧,我們需要把mBmp圖片畫到view上才行,所以我們在onDraw中需要加下面這句,將mBmp畫到view上canvas.drawBitmap(mBmp,0,0,mPaint);
所以改造後的程式碼為:public class BitmapCanvasView extends View {
private Bitmap mBmp;
private Paint mPaint;
private Canvas mBmpCanvas;
public BitmapCanvasView(Context context, AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint();
mPaint.setColor(Color.RED);
mBmp = Bitmap.createBitmap(500 ,500 , Bitmap.Config.ARGB_8888);
mBmpCanvas = new Canvas(mBmp);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setTextSize(100);
mBmpCanvas.drawText("啟艦大SB",0,100,mPaint);
canvas.drawBitmap(mBmp,0,0,mPaint);
}
}
這時候效果為:方法三:SurfaceHolder.lockCanvas()
在操作SurfaceView時需要用到Canvas,有關SurfaceView的知識後面會單獨開一篇講解,這裡就先過了。二、圖層與畫布
在《自定義控制元件之繪圖篇(四):canvas變換與操作》中,我們講過了canvas的save()和restore(),如果沒有看過的同學,務必先回去看一遍再回來。其實除了save()和restore()以外,還有其它一些函式來儲存和恢復畫布狀態,這部分我們就來看看。
1、saveLayer()
saveLayer()有兩個函式:/**
* 儲存指定矩形區域的canvas內容
*/
public int saveLayer(RectF bounds, Paint paint, int saveFlags)
public int saveLayer(float left, float top, float right, float bottom,Paint paint, int saveFlags)
- RectF bounds:要儲存的區域的矩形。
- int saveFlags:取值有:ALL_SAVE_FLAG、MATRIX_SAVE_FLAG、CLIP_SAVE_FLAG、HAS_ALPHA_LAYER_SAVE_FLAG、FULL_COLOR_LAYER_SAVE_FLAG、CLIP_TO_LAYER_SAVE_FLAG總共有這六個,其中ALL_SAVE_FLAG表示儲存全部內容,這些標識的具體意義我們後面會具體講;
下面我們來看一下例子,拿xfermode來做下試驗,來看看saveLayer都幹了什麼:
public class XfermodeView extends View {
private int width = 400;
private int height = 400;
private Bitmap dstBmp;
private Bitmap srcBmp;
private Paint mPaint;
public XfermodeView(Context context, AttributeSet attrs) {
super(context, attrs);
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
srcBmp = makeSrc(width, height);
dstBmp = makeDst(width, height);
mPaint = new Paint();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.GREEN);
int layerID = canvas.saveLayer(0, 0, width * 2, height * 2, mPaint, Canvas.ALL_SAVE_FLAG);
canvas.drawBitmap(dstBmp, 0, 0, mPaint);
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(srcBmp, width / 2, height / 2, mPaint);
mPaint.setXfermode(null);
canvas.restoreToCount(layerID);
}
// create a bitmap with a circle, used for the "dst" image
static Bitmap makeDst(int w, int h) {
Bitmap bm = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(bm);
Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
p.setColor(0xFFFFCC44);
c.drawOval(new RectF(0, 0, w, h), p);
return bm;
}
// create a bitmap with a rect, used for the "src" image
static Bitmap makeSrc(int w, int h) {
Bitmap bm = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(bm);
Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
p.setColor(0xFF66AAFF);
c.drawRect(0, 0, w, h, p);
return bm;
}
}
這段程式碼大家應該很熟悉,這是我們在講解setXfermode()時的示例程式碼,但在saveLayer前把整個螢幕畫成了綠色,效果圖如下:那麼問題來了,如果我們把saveLayer給去掉,看看會怎樣:
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.GREEN);
canvas.drawBitmap(dstBmp, 0, 0, mPaint);
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(srcBmp, width / 2, height / 2, mPaint);
mPaint.setXfermode(null);
}
效果圖就變這樣了:我擦類……去掉saveLayer()居然效果都不一樣了……
我們先回顧下Mode.SRC_IN的效果:在處理源影象時,以顯示源影象為主,在相交時利用目標影象的透明度來改變源影象的透明度和飽和度。當目標影象透明度為0時,源影象就完全不顯示。
再回過來看結果,第一個結果是對的,因為不與圓相交以外的區域透明度都是0,而第二個影象怎麼就變成了這屌樣,源影象全部都顯示出來了。
(1)、saveLayer的繪圖流程
這是因為在呼叫saveLayer時,會生成了一個全新的bitmap,這個bitmap的大小就是我們指定的儲存區域的大小,新生成的bitmap是全透明的,在呼叫saveLayer後所有的繪圖操作都是在這個bitmap上進行的。所以:
int layerID = canvas.saveLayer(0, 0, width * 2, height * 2, mPaint, Canvas.ALL_SAVE_FLAG);
canvas.drawBitmap(dstBmp, 0, 0, mPaint);
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(srcBmp, width / 2, height / 2, mPaint);
我們講過,在畫源影象時,會把之前畫布上所有的內容都做為目標影象,而在saveLayer新生成的bitmap上,只有dstBmp對應的圓形,所以除了與圓形相交之外的位置都是空畫素。 在畫圖完成之後,會把saveLayer所生成的bitmap蓋在原來的canvas上面。
所以此時的xfermode的合成過程如下圖所示:
savelayer新建的畫布上的影象做為目標影象,矩形所在的透明圖層與之相交,計算結果畫在新建的透明畫布上。最終將計算結果直接蓋在原始畫布上,形成最終的顯示效果。
(2)、沒有saveLayer的繪圖流程
然後我們再來看第二個示例,在第二個示例中,唯一的不同就是把saveLayer去掉了;在saveLayer去掉後,所有的繪圖操作都放在了原始View的Canvas所對應的Bitmap上了
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.GREEN);
canvas.drawBitmap(dstBmp, 0, 0, mPaint);
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(srcBmp, width / 2, height / 2, mPaint);
mPaint.setXfermode(null);
}
由於我們先把整個畫布給染成了綠色,然後再畫上了一個圓形,所以在應用xfermode來畫源影象的時候,目標影象當前Bitmap上的所有影象了,也就是整個綠色的螢幕和一個圓形了。所以這時候源影象的相交區域是沒有透明畫素的,透明度全是100%,這也就不難解釋結果是這樣的原因了。 此時的xfermode合成過程如下:
由於沒有呼叫saveLayer,所以圓形是直接畫在原始畫布上的,而當矩形與其相交時,就是直接與原始畫布上的所有影象做計算的。
所以有關saveLayer的結論來了:
saveLayer會建立一個全新透明的bitmap,大小與指定儲存的區域一致,其後的繪圖操作都放在這個bitmap上進行。在繪製結束後,會直接蓋在上一層的Bitmap上顯示。
2、畫布與圖層
上面我們講到了畫布(Bitmap)、圖層(Layer)和Canvas的概念,估計大家都會被繞暈了;下面我們下面來具體講解下它們之間的關係。圖層(Layer):每一次呼叫canvas.drawXXX系列函式時,都會生成一個透明圖層來專門來畫這個圖形,比如我們上面在畫矩形時的透明圖層就是這個概念。
畫布(bitmap):每一個畫布都是一個bitmap,所有的影象都是畫在bitmap上的!我們知道每一次呼叫canvas.drawxxx函式時,都會生成一個專用的透明圖層來畫這個圖形,畫完以後,就蓋在了畫布上。所以如果我們連續呼叫五個draw函式,那麼就會生成五個透明圖層,畫完之後依次蓋在畫布上顯示。
畫布有兩種,第一種是view的原始畫布,是通過onDraw(Canvas canvas)函式傳進來的,其中引數中的canvas就對應的是view的原始畫布,控制元件的背景就是畫在這個畫布上的!
另一種是人造畫布,通過saveLayer()、new Canvas(bitmap)這些方法來人為新建一個畫布。尤其是saveLayer(),一旦呼叫saveLayer()新建一個畫布以後,以後的所有draw函式所畫的影象都是畫在這個畫布上的,只有當呼叫restore()、resoreToCount()函式以後,才會返回到原始畫布上繪製。
Canvas:這個概念比較難理解,我們可以把Canvas理解成畫板,Bitmap理解成透明畫紙,而Layer則理解成圖層;每一個draw函式都對應一個圖層,在一個圖形畫完以後,就放在畫紙上顯示。而一張張透明的畫紙則一層層地疊加在畫板上顯示出來。我們知道畫板和畫紙都是用夾子夾在一起的,所以當我們旋轉畫板時,所有畫紙都會跟著旋轉!當我們把整個畫板裁小時,所以的畫紙也都會變小了!
這一點非常重要,當我們利用saveLayer來生成多個畫紙時,然後最上層的畫紙呼叫canvas.rotate(30)是把畫板給旋轉了,所有的畫紙也都被旋轉30度!這一點非常注意
另外,如果最上層的畫紙呼叫canvas.clipRect()將畫板裁剪了,那麼所有的畫紙也都會被裁剪。唯一能夠恢復的操作是呼叫canvas.revert()把上一次的動作給取消掉!
但在利用canvas繪圖與畫板不一樣的是,畫布的影響只體現在以後的操作上,以前畫上去的影象已經顯示在螢幕上是不會受到影響的。
這一點一定要理解出來,下面會用到。
三、save()、saveLayer()、saveLayerAlpha()中的用法
1、saveLayer的用法
saveLayer的宣告如下:public int saveLayer(RectF bounds, Paint paint, int saveFlags)
public int saveLayer(float left, float top, float right, float bottom,Paint paint, int saveFlags)
我們前面提到了saveLayer會新建一個畫布(bitmap),後續的所有操作都是在這個畫布上進行的。下面我們來分別看下saveLayer使用中的注意事項 (1)、saveLayer後的所有動作都只對新建畫布有效
我們先看個例子:public class SaveLayerUseExample_3_1 extends View{
private Paint mPaint;
private Bitmap mBitmap;
public SaveLayerUseExample_3_1(Context context, AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint();
mPaint.setColor(Color.RED);
mBitmap = BitmapFactory.decodeResource(getResources(),R.drawable.dog);;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawBitmap(mBitmap,0,0,mPaint);
int layerID = canvas.saveLayer(0,0,getWidth(),getHeight(),mPaint,Canvas.ALL_SAVE_FLAG);
canvas.skew(1.732f,0);
canvas.drawRect(0,0,150,160,mPaint);
canvas.restoreToCount(layerID);
}
}
效果圖如下:在onDraw中,我們先在view的原始畫布上畫上了小狗的影象,然後利用saveLayer新建了一個圖層,然後利用canvas.skew將新建的圖層水平斜切45度。所以之後畫的矩形(0,0,150,160)就是斜切的。
而正是由於在新建畫布後的各種操作都是針對新建畫布來操作的,不會對以前的畫布產生影響,從效果圖中也明顯可以看出,將畫布水平斜切45度也隻影響了saveLayer的新建畫布,並沒有對之前的原始畫布產生影響。
(2)、通過Rect指定矩形大小就是新建的畫布大小
在saveLayer的引數中,我們可以通過指定Rect物件或者指定四個點來來指定一個矩形,這個矩形的大小就是新建畫布的大小,我們舉例來看一下:public class SaveLayerUseExample_3_1 extends View {
private Paint mPaint;
private Bitmap mBitmap;
public SaveLayerUseExample_3_1(Context context, AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint();
mPaint.setColor(Color.RED);
mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.dog);
;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawBitmap(mBitmap, 0, 0, mPaint);
int layerID = canvas.saveLayer(0, 0, 100, 100, mPaint, Canvas.ALL_SAVE_FLAG);
canvas.drawRect(0, 0, 500, 600, mPaint);
canvas.restoreToCount(layerID);
}
}
效果圖如下:
在繪圖時,我們先把小狗圖片繪製在原始畫布上的,然後新建一個大小為(0,0,100,100)大小的透明畫布,然後再在這個畫布上畫一個(0, 0, 500, 600)的矩形。由於畫布大小隻有(0,0,100,100),所以(0, 0, 500, 600)這個矩形並不能完全顯示出來,也只能顯示出來(0,0,100,100)畫布大小的部分。
那有些同學會說了,nnd,為了避免畫布太小而出現問題,我每次都新建一個螢幕大小的畫布多好,這樣雖然是不會出現問題,但你想過沒有,螢幕大小的畫布需要多少空間嗎,按一個畫素需要8bit儲存空間算,1024*768的機器,所使用的bit數就是1024*768*8=6.2M!所以我們在使用saveLayer新建畫布時,一定要選擇適當的大小,不然你的APP很可能OOM哦。
注意,注意:在我的例子中都是直接新建全屏畫布的,因為寫例子比較方便!!!!但是我這只是示例,在現實使用中,一定要適量的建立畫布的大小哦!
2、saveLayerAlpha的用法
saveLayerAlpha的宣告如下:public int saveLayerAlpha(RectF bounds, int alpha, int saveFlags)
public int saveLayerAlpha(float left, float top, float right, float bottom,int alpha, int saveFlags)
相比saveLayer,多一個alpha引數,用以指定新建畫布透明度,取值範圍為0-255,可以用16進位制的oxAA表示; 這個函式的意義也是在呼叫的時候會新建一個bitmap畫布,以後的各種繪圖操作都作用在這個畫布上,但這個畫布是有透明度的,透明度就是通過alpha值指定的。
我們來看個示例
public class SaveLayerAlphaView extends View {
private Paint mPaint;
public SaveLayerAlphaView(Context context, AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint();
mPaint.setColor(Color.RED);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(100,100,300,300,mPaint);
int layerID = canvas.saveLayerAlpha(0,0,600,600,0x88,Canvas.ALL_SAVE_FLAG);
mPaint.setColor(Color.GREEN);
canvas.drawRect(200,200,400,400,mPaint);
canvas.restoreToCount(layerID);
}
}
效果圖如下:在saveLayerAlpha以後,我們畫了一個綠色的矩形,由於把saveLayerAlpha新建的矩形的透明度是0x88(136)大概是50%透明度,從效果圖中也可以看出在新建影象與上一畫布合成後,是具有透明度的。
好了,這篇文章就先到這裡,下一篇詳細給大家講解有關引數中各個Flag的意義。
如果本文有幫到你,記得加關注哦
如果你喜歡我的文章,那麼你將會更喜歡我的微信公眾號,將定期推送博主最新文章與收集乾貨分享給大家(一週一次)