1. 程式人生 > >Android高階進階——繪圖篇(七)Canvas 與 圖層(一)

Android高階進階——繪圖篇(七)Canvas 與 圖層(一)

開篇

前面很多篇文章都用到了圖層的概念,但是一直沒有詳細介紹,今天這篇文章將詳細的介紹 Canvas 與 圖層的概念

一、如何獲得一個Canvas物件

  • 方法一:自定義view時, 重寫onDraw、dispatchDraw方法
protected void onDraw(Canvas canvas) {  
    super.onDraw(canvas);  
}  

protected void dispatchDraw(Canvas canvas) {  
    super.dispatchDraw(canvas);  
}  
  • 方法二:使用Bitmap建立
    使用:

    Canvas c = new Canvas(bitmap);


Canvas c = new Canvas();
c.setBitmap(bitmap);

二、在OnDraw()中使用

我們一定要注意的是,如果我們用bitmap構造了一個canvas,那這個canvas上繪製的影象也都會儲存在這個bitmap上,而不是畫在View上,如果想畫在View上就必須使用OnDraw(Canvas canvas)函式中傳進來的canvas畫一遍bitmap才能畫到view上。 下面舉個例子: ![image.png](https://upload-images.jianshu.io/upload_images/11455341-de84946824e77d53.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
    private
void init() { //初始化畫筆 paint = new Paint(); //設定畫筆顏色 paint.setColor(Color.BLUE); paint.setStyle(Paint.Style.FILL_AND_STROKE); paint.setTextSize(50); bitmap = Bitmap.createBitmap(800, 400, Bitmap.Config.ARGB_8888); } @Override protected void
onDraw(Canvas canvas) { super.onDraw(canvas); //禁用硬體加速 setLayerType(View.LAYER_TYPE_SOFTWARE, null); Canvas canvas1 = new Canvas(bitmap); canvas1.drawText("你是來搞笑的麼!!!", 100, 100, paint); canvas.drawBitmap(bitmap, 100, 100, paint); }
可以看到,毛線也沒有,這是為什麼呢? 我們仔細來看一下onDraw函式:
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //禁用硬體加速
        setLayerType(View.LAYER_TYPE_SOFTWARE, null);

        Canvas canvas1 = new Canvas(bitmap);
        canvas1.drawText("你是來搞笑的麼!!!", 100, 100, paint);

    }
在onDraw函式中,我們只是將文字畫在了mBmpCanvas上,也就是我們新建mBmp圖片上!這個圖片跟我們view沒有任何關係好吧,我們需要把mBmp圖片畫到view上才行,所以我們在onDraw中需要加下面這句,將mBmp畫到view上
canvas.drawBitmap(bitmap, 100, 100, paint);
![](https://upload-images.jianshu.io/upload_images/11455341-c918f89cd0013070.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

三、圖層與畫布

  • (1)、畫布的平移(translate)
    canvas中有一個函式translate()是用來實現畫布平移的,畫布的原狀是以左上角為原點,向左是X軸正方向,向下是Y軸正方向

void translate(float dx, float dy)

引數說明:
- float dx:水平方向平移的距離,正數指向正方向(向右)平移的量,負數為向負方向(向左)平移的量
- flaot dy:垂直方向平移的距離,正數指向正方向(向下)平移的量,負數為向負方向(向上)平移的量

protected void onDraw(Canvas canvas) {  
    // TODO Auto-generated method stub  
    super.onDraw(canvas);  

    //translate  平移,即改變座標系原點位置  

    Paint paint = new Paint();  
    paint.setColor(Color.GREEN);  
    paint.setStyle(Style.FILL);  

//  canvas.translate(100, 100);  
    Rect rect1 = new Rect(0,0,400,220);  
    canvas.drawRect(rect1, paint);  

1、上面這段程式碼,先把canvas.translate(100, 100);註釋掉,看原來矩形的位置,然後開啟註釋,看平移後的位置,對比如下圖:

image.png

二、螢幕顯示與Canvas的關係

很多童鞋一直以為顯示所畫東西的改螢幕就是Canvas,其實這是一個非常錯誤的理解,比如下面我們這段程式碼:

這段程式碼中,同一個矩形,在畫布平移前畫一次,平移後再畫一次,大家會覺得結果會怎樣?

protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
super.onDraw(canvas);

//構造兩個畫筆,一個紅色,一個綠色  
Paint paint_green = generatePaint(Color.GREEN, Style.STROKE, 3);  
Paint paint_red   = generatePaint(Color.RED, Style.STROKE, 3);  

//構造一個矩形  
Rect rect1 = new Rect(0,0,400,220);  

//在平移畫布前用綠色畫下邊框  
canvas.drawRect(rect1, paint_green);  

//平移畫布後,再用紅色邊框重新畫下這個矩形  
canvas.translate(100, 100);  
canvas.drawRect(rect1, paint_red);  

}
private Paint generatePaint(int color,Paint.Style style,int width)
{
Paint paint = new Paint();
paint.setColor(color);
paint.setStyle(style);
paint.setStrokeWidth(width);
return paint;
}

程式碼分析:
這段程式碼中,對於同一個矩形,在平移畫布前利用綠色畫下矩形邊框,在平移後,再用紅色畫下矩形邊框。大家是不是會覺得這兩個邊框會重合?實際結果是這樣的。

image.png

從到這個結果大家可能會狠蛋疼,我第一次看到這個結果的時候蛋都碎一地了要。淡定……
這個結果的關鍵問題在於,為什麼綠色框並沒有移動?

這是由於螢幕顯示與Canvas根本不是一個概念!Canvas是一個很虛幻的概念,相當於一個透明圖層(用過PS的同學應該都知道),每次Canvas畫圖時(即呼叫Draw系列函式),都會產生一個透明圖層,然後在這個圖層上畫圖,畫完之後覆蓋在螢幕上顯示。所以上面的兩個結果是由下面幾個步驟形成的:

1、呼叫canvas.drawRect(rect1, paint_green);時,產生一個Canvas透明圖層,由於當時還沒有對座標系平移,所以座標原點是(0,0);再在系統在Canvas上畫好之後,覆蓋到螢幕上顯示出來,過程如下圖:

image.png

2、然後再第二次呼叫canvas.drawRect(rect1, paint_red);時,又會重新產生一個全新的Canvas畫布,但此時畫布座標已經改變了,即向右和向下分別移動了100畫素,所以此時的繪圖方式為:(合成檢視,從上往下看的合成方式)

image.png

上圖展示了,上層的Canvas圖層與底部的螢幕的合成過程,由於Canvas畫布已經平移了100畫素,所以在畫圖時是以新原點來產生檢視的,然後合成到螢幕上,這就是我們上面最終看到的結果了。我們看到螢幕移動之後,有一部分超出了螢幕的範圍,那超出範圍的影象顯不顯示呢,當然不顯示了!也就是說,Canvas上雖然能畫上,但超出了螢幕的範圍,是不會顯示的。當然,我們這裡也沒有超出顯示範圍,兩框框而已。

下面對上面的知識做一下總結:
- 1、每次呼叫canvas.drawXXXX系列函式來繪圖進,都會產生一個全新的Canvas畫布。
- 2、如果在DrawXXX前,呼叫平移、旋轉等函式來對Canvas進行了操作,那麼這個操作是不可逆的!每次產生的畫布的最新位置都是這些操作後的位置。(關於Save()、Restore()的畫布可逆問題的後面再講)
- 3、在Canvas與螢幕合成時,超出螢幕範圍的影象是不會顯示出來的。

四、旋轉(Rotate)

畫布的旋轉是預設是圍繞座標原點來旋轉的,這裡容易產生錯覺,看起來覺得是圖片旋轉了,其實我們旋轉的是畫布,以後在此畫布上畫的東西顯示出來的時候全部看起來都是旋轉的。其實Roate函式有兩個建構函式:

void rotate(float degrees)
void rotate (float degrees, float px, float py)

第一個建構函式直接輸入旋轉的度數,正數是順時針旋轉,負數指逆時針旋轉,它的旋轉中心點是原點(0,0)
第二個建構函式除了度數以外,還可以指定旋轉的中心點座標(px,py)

下面以第一個建構函式為例,旋轉一個矩形,先畫出未旋轉前的圖形,然後再畫出旋轉後的圖形;

protected void onDraw(Canvas canvas) {  
    // TODO Auto-generated method stub  
    super.onDraw(canvas);  


    Paint paint_green = generatePaint(Color.GREEN, Style.FILL, 5);  
    Paint paint_red   = generatePaint(Color.RED, Style.STROKE, 5);  

    Rect rect1 = new Rect(300,10,500,100);  
    canvas.drawRect(rect1, paint_red); //畫出原輪廓  

    canvas.rotate(30);//順時針旋轉畫布  
    canvas.drawRect(rect1, paint_green);//畫出旋轉後的矩形  
}   

image.png

這個最終螢幕顯示的構造過程是這樣的:

下圖顯示的是第一次畫圖合成過程,此時僅僅呼叫canvas.drawRect(rect1, paint_red); 畫出原輪廓

然後是先將Canvas正方向依原點旋轉30度,然後再與上面的螢幕合成,最後顯示出我們的複合效果。

image.png

有關Canvas與螢幕的合成關係我覺得我已經講的夠詳細了,後面的幾個操作Canvas的函式,我就不再一一講它的合成過程了。

五、縮放(scale )

public void scale (float sx, float sy)
public final void scale (float sx, float sy, float px, float py)

其實我也沒弄懂第二個建構函式是怎麼使用的,我就先講講第一個建構函式的引數吧
float sx:水平方向伸縮的比例,假設原座標軸的比例為n,不變時為1,在變更的X軸密度為n*sx;所以,sx為小數為縮小,sx為整數為放大
float sy:垂直方向伸縮的比例,同樣,小數為縮小,整數為放大

注意:這裡有X、Y軸的密度的改變,顯示到圖形上就會正好相同,比如X軸縮小,那麼顯示的圖形也會縮小。一樣的。

protected void onDraw(Canvas canvas) {  
    // TODO Auto-generated method stub  
    super.onDraw(canvas);  

//  //scale 縮放座標系密度  
    Paint paint_green = generatePaint(Color.GREEN, Style.STROKE, 5);  
    Paint paint_red   = generatePaint(Color.RED, Style.STROKE, 5);  

    Rect rect1 = new Rect(10,10,200,100);  
    canvas.drawRect(rect1, paint_green);  

    canvas.scale(0.5f, 1);  
    canvas.drawRect(rect1, paint_red);  
}   

image.png

六、扭曲(skew)

其實我覺得譯成斜切更合適,在PS中的這個功能就差不多叫斜切。但這裡還是直譯吧,大家都是這個名字。看下它的建構函式:

void skew (float sx, float sy)

引數說明:
- float sx:將畫布在x方向上傾斜相應的角度,sx傾斜角度的tan值,
- float sy:將畫布在y軸方向上傾斜相應的角度,sy為傾斜角度的tan值,

注意,這裡全是傾斜角度的tan值哦,比如我們打算在X軸方向上傾斜60度,tan60=根號3,小數對應1.732

protected void onDraw(Canvas canvas) {  
    // TODO Auto-generated method stub  
    super.onDraw(canvas);  

    //skew 扭曲  
    Paint paint_green = generatePaint(Color.GREEN, Style.STROKE, 5);  
    Paint paint_red   = generatePaint(Color.RED, Style.STROKE, 5);  

    Rect rect1 = new Rect(10,10,200,100);  

    canvas.drawRect(rect1, paint_green);  
    canvas.skew(1.732f,0);//X軸傾斜60度,Y軸不變  
    canvas.drawRect(rect1, paint_red);  
}   

image.png

七、裁剪畫布(clip系列函式)

裁剪畫布是利用Clip系列函式,通過與Rect、Path、Region取交、並、差等集合運算來獲得最新的畫布形狀。除了呼叫Save、Restore函式以外,這個操作是不可逆的,一但Canvas畫布被裁剪,就不能再被恢復!
Clip系列函式如下:

boolean clipPath(Path path)
boolean clipPath(Path path, Region.Op op)
boolean clipRect(Rect rect, Region.Op op)
boolean clipRect(RectF rect, Region.Op op)
boolean clipRect(int left, int top, int right, int bottom)
boolean clipRect(float left, float top, float right, float bottom)
boolean clipRect(RectF rect)
boolean clipRect(float left, float top, float right, float bottom, Region.Op op)
boolean clipRect(Rect rect)
boolean clipRegion(Region region)
boolean clipRegion(Region region, Region.Op op)

以上就是根據Rect、Path、Region來取得最新畫布的函式,難度都不大,就不再一一講述。利用ClipRect()來稍微一講

protected void onDraw(Canvas canvas) {  
    // TODO Auto-generated method stub  
    super.onDraw(canvas);  

    canvas.drawColor(Color.RED);  
    canvas.clipRect(new Rect(100, 100, 200, 200));  
    canvas.drawColor(Color.GREEN);  
}

先把背景色整個塗成紅色。顯示在螢幕上
然後裁切畫布,最後最新的畫布整個塗成綠色。可見綠色部分,只有一小塊,而不再是整個螢幕了。
關於兩個畫布與螢幕合成,我就不再畫圖了,跟上面的合成過程是一樣的。

image.png

八、畫布的儲存與恢復(save()、restore())

前面我們講的所有對畫布的操作都是不可逆的,這會造成很多麻煩,比如,我們為了實現一些效果不得不對畫布進行操作,但操作完了,畫布狀態也改變了,這會嚴重影響到後面的畫圖操作。如果我們能對畫布的大小和狀態(旋轉角度、扭曲等)進行實時儲存和恢復就最好了。
這小節就給大家講講畫布的儲存與恢復相關的函式——Save()、Restore()。

int save ()
void restore()

這兩個函式沒有任何的引數,很簡單。
Save():每次呼叫Save()函式,都會把當前的畫布的狀態進行儲存,然後放入特定的棧中;
restore():每當呼叫Restore()函式,就會把棧中最頂層的畫布狀態取出來,並按照這個狀態恢復當前的畫布,並在這個畫布上做畫。
為了更清晰的顯示這兩個函式的作用,下面舉個例子:

protected void onDraw(Canvas canvas) {  
    // TODO Auto-generated method stub  
    super.onDraw(canvas);  

    canvas.drawColor(Color.RED);  

    //儲存當前畫布大小即整屏  
    canvas.save();   

    canvas.clipRect(new Rect(100, 100, 800, 800));  
    canvas.drawColor(Color.GREEN);  

    //恢復整屏畫布  
    canvas.restore();  

    canvas.drawColor(Color.BLUE);  
}   

他影象的合成過程為:(最終顯示為全螢幕藍色)

image.png

下面我通過一個多次利用Save()、Restore()來講述有關儲存Canvas畫布狀態的棧的概念:程式碼如下:

protected void onDraw(Canvas canvas) {  
    // TODO Auto-generated method stub  
    super.onDraw(canvas);  

    canvas.drawColor(Color.RED);  
    //儲存的畫布大小為全螢幕大小  
    canvas.save();  

    canvas.clipRect(new Rect(100, 100, 800, 800));  
    canvas.drawColor(Color.GREEN);  
    //儲存畫布大小為Rect(100, 100, 800, 800)  
    canvas.save();  

    canvas.clipRect(new Rect(200, 200, 700, 700));  
    canvas.drawColor(Color.BLUE);  
    //儲存畫布大小為Rect(200, 200, 700, 700)  
    canvas.save();  

    canvas.clipRect(new Rect(300, 300, 600, 600));  
    canvas.drawColor(Color.BLACK);  
    //儲存畫布大小為Rect(300, 300, 600, 600)  
    canvas.save();  

    canvas.clipRect(new Rect(400, 400, 500, 500));  
    canvas.drawColor(Color.WHITE);  
}   

顯示效果為:

image.png

在這段程式碼中,總共呼叫了四次Save操作。上面提到過,每呼叫一次Save()操作就會將當前的畫布狀態儲存到棧中,所以這四次Save()所儲存的狀態的棧的狀態如下:

image.png

注意在,第四次Save()之後,我們還對畫布進行了canvas.clipRect(new Rect(400, 400, 500, 500));操作,並將當前畫布畫成白色背景。也就是上圖中最小塊的白色部分,是最後的當前的畫布。

如果,現在使用Restor(),會怎樣呢,會把棧頂的畫布取出來,當做當前畫布的畫圖,試一下:

protected void onDraw(Canvas canvas) {  
    // TODO Auto-generated method stub  
    super.onDraw(canvas);  

    canvas.drawColor(Color.RED);  
    //儲存的畫布大小為全螢幕大小  
    canvas.save();  

    canvas.clipRect(new Rect(100, 100, 800, 800));  
    canvas.drawColor(Color.GREEN);  
    //儲存畫布大小為Rect(100, 100, 800, 800)  
    canvas.save();  

    canvas.clipRect(new Rect(200, 200, 700, 700));  
    canvas.drawColor(Color.BLUE);  
    //儲存畫布大小為Rect(200, 200, 700, 700)  
    canvas.save();  

    canvas.clipRect(new Rect(300, 300, 600, 600));  
    canvas.drawColor(Color.BLACK);  
    //儲存畫布大小為Rect(300, 300, 600, 600)  
    canvas.save();  

    canvas.clipRect(new Rect(400, 400, 500, 500));  
    canvas.drawColor(Color.WHITE);  

    //將棧頂的畫布狀態取出來,作為當前畫布,並畫成黃色背景  
    canvas.restore();  
    canvas.drawColor(Color.YELLOW);  
}   

上段程式碼中,把棧頂的畫布狀態取出來,作為當前畫布,然後把當前畫布的背景色填充為黃色
image.png

那如果我連續Restore()三次,會怎樣呢?
我們先分析一下,然後再看效果:Restore()三次的話,會連續出棧三次,然後把第三次出來的Canvas狀態當做當前畫布,也就是Rect(100, 100, 800, 800),所以如下程式碼:

protected void onDraw(Canvas canvas) {  
    // TODO Auto-generated method stub  
    super.onDraw(canvas);  

    canvas.drawColor(Color.RED);  
    //儲存的畫布大小為全螢幕大小  
    canvas.save();  

    canvas.clipRect(new Rect(100, 100, 800, 800));  
    canvas.drawColor(Color.GREEN);  
    //儲存畫布大小為Rect(100, 100, 800, 800)  
    canvas.save();  

    canvas.clipRect(new Rect(200, 200, 700, 700));  
    canvas.drawColor(Color.BLUE);  
    //儲存畫布大小為Rect(200, 200, 700, 700)  
    canvas.save();  

    canvas.clipRect(new Rect(300, 300, 600, 600));  
    canvas.drawColor(Color.BLACK);  
    //儲存畫布大小為Rect(300, 300, 600, 600)  
    canvas.save();  

    canvas.clipRect(new Rect(400, 400, 500, 500));  
    canvas.drawColor(Color.WHITE);  

    //連續出棧三次,將最後一次出棧的Canvas狀態作為當前畫布,並畫成黃色背景  
    canvas.restore();  
    canvas.restore();  
    canvas.restore();  
    canvas.drawColor(Color.YELLOW);  
}   

image.png

介紹完了 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都幹了什麼:

    private void init() {
        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前把整個螢幕畫成了綠色,效果圖如下:

image.png

那麼問題來了,如果我們把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);  
}  

效果圖就變這樣了:

image.png

我擦類……去掉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的合成過程如下圖所示:

image.png

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合成過程如下:

image.png

由於沒有呼叫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);  
    }  
}  

效果圖如下:

image.png

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

效果圖如下:
image.png

在繪圖時,我們先把小狗圖片繪製在原始畫布上的,然後新建一個大小為(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);  

    }  
}  

效果圖如下:

image.png

在saveLayerAlpha以後,我們畫了一個綠色的矩形,由於把saveLayerAlpha新建的矩形的透明度是0x88(136)大概是50%透明度,從效果圖中也可以看出在新建影象與上一畫布合成後,是具有透明度的。

好了,這篇文章就先到這裡,下一篇詳細給大家講解有關引數中各個Flag的意義。