Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 自定義控件三部曲之繪圖篇(十三)——Canvas與圖層(一)

自定義控件三部曲之繪圖篇(十三)——Canvas與圖層(一)

編輯:關於Android編程

在給大家講解了paint的幾個方法之後,我覺得有必要插一篇有關Canvas畫布的知識,在開始paint之前,我們講解了canvas繪圖的幾篇文章和cavas的save()、store()的知識,這篇是對Canvas的一個系統的補充,

一、如何獲得一個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()是繪制子視圖無論是View還是ViewGroup對它們倆的調用順序都是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可以從圖片加載,也可以創建,有下面幾種方法
//方法一:新建一個空白bitmap
Bitmap bmp = Bitmap.createBitmap(width ,height Bitmap.Config.ARGB_8888);
//方法二:從圖片中加載
Bitmap bmp = BitmapFactory.decodeResource(getResources(),R.drawable.wave_bg,null);
這兩個方法是最常用的,除了這兩個方法以外,還有其它幾個方法(比如構造一個具有matrix的圖像副本——前面示例中的倒影圖像),這裡就不再涉及了,大家可以去查看Bitmap的構造函數。

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的意義。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved