第9章 CustomView Canvas與圖層

一、獲取Canvas對象的方法

方法一:重寫onDraw()、dispatchDraw()函數

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
}

protected void dispatchDraw(Canvas canvas) {
    super.dispatch(canvas);
}

可以看到,onDraw()、dispatchDraw()函數在傳入的參數中都有一個Canvas對象,這個Canvas對象是View中的Canvas對象,利用這個Canvas對象繪圖,效果會直接反映在View中。

區別:

● 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。

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.BLACK);
        mBmp = Bitmap.createBitmap(500, 500, Bitmap.Config.ARGB_8888);
        mBmpCanvas = new Canvas(mBmp);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPaint.setTextSize(50);
        mBmpCanvas.drawText("歡迎光臨", 0, 100, mPaint);
        // canvas.drawBitmap(mBmp, 0, 0, mPaint);
    }
}

運行這段代碼後會發現,結果是一片空白,我們寫的字去哪兒了?在onDraw()函數中,我們只是將文字寫在了mBmpCanvas上,也就是我們新建的mBmp圖片上,而最終沒有將圖片畫在畫布上。因爲文字被寫在了圖片上,而畫布上卻沒有任何內容,所以結果是一片空白。如果將註釋掉的最後一句打開,即可將圖片畫在畫布上,在視圖上就會顯示文字了。
方法三:調用SurfaceHolder.lockCanvas()函數

在使用SurfaceView時,當調用SurfaceHolder.lockCanvas()函數時,也會創建Canvas對象,有關SurfaceView知識在下一章有講解。

二、圖層與畫布

前面講過Canvas的save()和restore()函數,除這兩函數以外,還有其他一些函數用來保存和恢復畫布狀態。

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)
● bounds:要保存的區域所對應的矩形對象
● saveFlags:取值有ALL_SAVE_FLAG、MATRIX_SAVA_FLAG、CLIP_SAVE_FLAG、HAS_ALPHA_LAYER_SAVE_FLAG、FULL_COLOR_LAYER_SAVE_FLAG、CLIP_TO_LAYER_SAVE_FLAG,其中ALL_SAVE_FLAG表示保存全部內容。
public class CustomView extends View {
    private int width = 400;
    private int height = 400;
    private Bitmap dstBmp;
    private Bitmap srcBmp;
    private Paint mPaint;

    public CustomView(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)); // 算法:[Sa*Da, Sc*Da]
        canvas.drawBitmap(srcBmp, width / 2, height / 2, mPaint);
        mPaint.setXfermode(null);
        canvas.restoreToCount(layerID);
    }

    //創建一張圓形圖片
    private 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;
    }

    //創建一張矩形圖片
    private 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;
    }
}

顯示正常。但如果把saveLayer()函數去掉,則會怎樣?

    @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)); // 算法:[Sa*Da, Sc*Da]
        canvas.drawBitmap(srcBmp, width / 2, height / 2, mPaint);
        mPaint.setXfermode(null);
        // canvas.restoreToCount(layerID);
    }

這時源圖像矩形怎麼全部顯示出來了?

1.調用saveLayer()函數時的繪圖流程

在調用saveLayer()函數時,會生成一塊全新的畫布(Bitmap),這塊畫布的大小就是我們指定的所要保存區域的大小。新生成的畫布是全透明的,在調用savaLayer()函數後所有的繪圖操作都是在這塊畫布上進行的。

在利用Xfermode畫源圖像時,會把之前畫布上所有的內容都作爲目標圖像;而在調用saveLayer()函數新生成的畫布上,只有dstBmp對應的圓形。所以,在Mode.SRC_IN模式下,除與圓形相交之外的位置都是空白像素。

對於Xfermode而言,在繪圖完成之後,會把調用saveLayer()函數所生成的透明畫布覆蓋在原來的畫布上面,以形成最終的顯示結果。

此時的Xfermode的合成過程如下圖所示。

中間的透明畫布就是調用saveLayer()函數自動生成的,最上方的透明圖層是調用drawBitmap()函數生成的。

每次調用canvas.drawXXX系列函數,都會生成一個透明圖層專門繪製這個圖形,而每次生成的圖層都會疊加到最近的畫布上。

因爲在這裏對源圖像(矩形)應用了Xfermode算法,所以在疊加到就近的調用saveLayer()函數生成的畫布上時,會進行計算。在新建的畫布上繪製完成以後,整體覆蓋在原始畫布上顯示出來。

正是因爲在使用Xfermode計算時,目標圖像是繪製在新建的透明畫布上的,所以除圓形以外的區域全部是透明像素,最終的顯示結果是正確的。

2.沒有saveLayer()函數時的繪圖流程

在去掉saveLayer()函數後,就不會新建畫布了。當然,所有的繪圖操作都會在原始畫布上進行。

由於先把整塊畫布染成了綠色,再畫一個圓形,所以在應用Xfermode來畫源圖像時,在目標畫布上是沒有透明像素的。所以,當矩形與其相交時,就是直接與原始畫布上的所有圖像進行計算的。結果也就是那樣的。

結論:調用saveLayer()函數會創建一塊全新的透明畫布,大小與指定的區域大小一致,其後的繪圖操作都放在這塊畫布上進行。在繪製結束後,會直接覆蓋在原始畫布上顯示。

畫布與圖層:

畫布(Bitmap)、圖層(Layer)、Canvas,這三者之間的關係:

● 圖層(Layer):每次調用canvas.drawXXX系列函數,都會生成一個透明圖層專門來繪製這個圖形,比如前面在繪製矩形時的透明圖層就是這個概念。
● 畫布(Bitmap):每塊畫布都是一個Bitmap,所有的圖像都是畫在這個Bitmap上的。我們知道,每次調用canvas.drawXXX系列函數,都會生成一個專用的透明圖層來繪製這個圖形,繪製完成以後,就覆蓋在畫布上。所以,如果我們連續調用5個draw函數,就會生成5個透明圖層,畫完之後依次覆蓋在畫布上顯示。畫布有兩種:一種是View的原始畫布,是通過onDraw(Canvas canvas)函數傳入的,參數中的canvas對應的是View的原始畫布,控件的背景就是畫在這塊畫布上的;另一種是人造畫布,通過saveLayer()、newCanvas(bitmap)等函數來人爲地新建一塊畫布。尤其是saveLayer()函數,一旦調用saveLayer()函數新建一塊畫布,以後所有draw函數所畫的圖像都是畫在這塊畫布上的,只有在調用restore()、resoreToCount()函數以後,纔會返回到原始畫布上進行繪製。
● Canvas:Canvas是畫布的表現形式,我們所要繪製的任何東西都是利用Canvas來實現的。在代碼中,Canvas的生成方式只有一種——new Canvas(bitmap),即只能通過Bitmap生成,無論是原始畫布還是人造畫布,所有的畫布最後都是通過Canvas畫到Bitmap上的。可以把Canvas理解成繪圖的工具,利用它所封裝的繪圖函數來繪圖,而所要繪製的內容最後是畫在Bitmap上的。所以,如果我們利用Canvas.clipXXX系列函數將畫布進行裁剪,其實就是把它對應的Bitmap進行裁剪,與之對應的結果是以後再利用Canvas繪圖的區域會減小。

saveLayer()和saveLayerAlpha()函數的用法:

1.saveLayer()函數的用法

saveLayer()函數會新建一塊畫布(Bitmap),後續的所有操作都是在這塊畫布上進行的。

使用saveLayer()函數注意事項:

(1)saveLayer()函數後的所有動作都只對新建畫布有效。

public class SaveLayerUseExample extends View {
	private Paint mPaint; 
	private Bitmap mBitmap;
	public SaveLayerUseExample(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);// 將新建圖層水平傾斜60°(tan60°=√3)
		canvas.drawRect(0, 0, 150, 160, mPaint);
		canvas.restoreToCount(layerID);
	}
}
/**
 * @params sx 將畫布在x方向上傾斜相應的角度,sx傾斜角度的tan值,其實就是將y逆時針旋轉相應的角度
 * @params sy 將畫布在y方向上傾斜相應的角度,sx傾斜角度的tan值,其實就是將x順時針旋轉相應的角度
 */
public void skew(float sx, float sy)

 

在onDraw()函數中,我們先在View的原始畫布上畫上了小狗圖像,然後利用saveLayer()函數新建了一個圖層,接着利用canvas.skew()函數將新建的圖層水平斜切60°,所以之後畫的矩形(0,0,150,160)就是傾斜的。
而正是由於在新建畫布後的各種操作都是針對新建畫布進行的,所以不會對以前的畫布產生影響。從效果圖中也可以明顯看出,將畫布水平傾斜60°隻影響了saveLayer()函數的新建畫布,並沒有對原始畫布產生影響。

(2)通過Rect指定的矩形大小就是新建的畫布大小。

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Bitmap mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.dog);
        Paint mPaint = new Paint();
        // 如果指定drawBitmap的第二個參數Rect對象,則是截取mBitmap部分填充到目標Rect區域
        canvas.drawBitmap(mBitmap, null, new Rect(0, 0, 700, 600), mPaint);
        int layerID = canvas.saveLayer(0, 0, 200, 200, mPaint, Canvas.ALL_SAVE_FLAG);
        canvas.drawColor(Color.GRAY);
        canvas.restoreToCount(layerID);
    }

在繪圖時 ,我們先把小狗圖像繪製在原始畫布上,然後新建一個大小爲(0,0,200,200)的透明畫布,並將畫布填充爲灰色。由於畫布大小隻有(0,0,200,200),所以從效果圖中可 以看出,也只有這一小部分區域被填充爲灰色。

可能會想,爲了避免畫布太小而出現問題,每次新建一塊屏幕大小的畫布不就好了?

這樣做雖然不會 出現問題,但屏幕大小的畫布需要多少存儲空間呢?按一個像素需要 8bit 存儲空間算,分辨率爲1024像素×768像素的機器,所佔用的存儲空間就是1024x768×8=6.2MB。所以我們在使用saveLayer()函數新建畫布時,一定要選擇適當的大小,否則你的APP很可能OOM(Out Of Memory,內存溢出)。

2.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,可以用十六進制0xAA表示。這樣創建的畫布具有透明度。

三、Flag的具體含義

在Canvas中有如下幾個save系列函數:

public int save()
public int save(int saveFlags)
public int saveLayer(RectF bounds, Paint paint, int saveFlags)
public int saveLayerAlpha(RectF bounds, Paint paint, int saveFlags)

先考慮一下:如果讓我們保存一塊畫布的狀態,以便恢復,則需要保存哪些內容呢?
第一個是位置信息,第二個是大小信 息,好像除此之外也沒什麼了。位置信息對應的是MATIX_SAVE_FLAG,大小信息對應的是 CLIP_SAVE_FLAG,這是save()和saveLayer()函數所公用的標識。而saveLayer()函數專用的三個標識用於指定saveLayer()函數新建的畫布具有哪種特性,而不是保存畫布的範疇 。

事實上,在API 26,即Android8.0開始,FLAG就只有一個了,並且還是默認值。

Android Canvas

官方原文:

Generally ALL_SAVE_FLAG is recommended for performance reasons. Value is either 0 or ALL_SAVE_FLAG.

As of API Level API level android.os.Build.VERSION_CODES.P Build.VERSION_CODES.P the only valid saveFlags is ALL_SAVE_FLAG. All other flags are ignored.

即,save()/saveLayer()/saveLayerAlpha()從Android8.0開始,已經不帶有最後一個參數:int saveFlags了。連官方文檔裏面都已經沒有帶參的save()方法了。而且到了Android9.0唯有ALL_SAVE_FLAG是有效的,其他的都將被忽略。

所以,這裏不去講解其他的_SAVE_FLAG了。


canvas.translate(平移)、canvas.rotate(旋轉)、canvas.scale(縮放)、canvas.skew(扭曲)其實都是利用位置矩陣Matrix實現的,可利用save()、restore()保存狀態。

save()函數與saveLayer()函數的區別在於:saveLayer()函數會新建一塊畫布,而save()函數不會新建畫布。

save()用來保存Canvas的狀態,save()方法之後的代碼,可以調用Canvas的平移、放縮、旋轉、裁剪等操作。

restore()用來恢復Canvas之前保存的狀態(可以想成是保存座標軸的狀態),防止save()方法代碼之後對Canvas執行的操作,繼續對後續的繪製會產生影響,通過該方法可以避免連帶的影響。

public class MATRIX_SAVE_FLAG_View extends View {
    private Paint mPaint;
    public MATRIX_SAVE_FLAG_View(Context context, AttributeSet attrs) {
        super(context, attrs);
        setLayerType(LAYER_TYPE_SOFTWARE ,null);
        mPaint = new Paint();
        mPaint.setColor(Color.GRAY);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.save();
        canvas.rotate(40);
        canvas.drawRect(l00, 0, 200, 100, mPaint);
        canvas.restore();
        mPaint.setColor(Color.BLACK);
        canvas.drawRect(l00, 0, 200, 100, mPaint);
    }
}

先調用canvas.save()函數將Canvas的所有標識都保存起來(當然包括位置矩陣);然後將畫布旋轉 40°,畫一個灰色矩形;接着調用canvas.restore()函數將畫布恢復;最後在同一個位置畫一個黑色矩形。

可分爲4步:

➀canvas.save()保存了當前畫布的所有狀態,特別是座標軸狀態這個位置矩陣,狀態命名爲statusOrigin。

➁對canvas進行平移、旋轉、縮放、扭曲等操作。

➂canvas.restore()讓畫布回到了當初save()時的狀態:statusOrign。要注意在恢復畫布狀態之前,在畫布上所繪製的所有一切是仍存在的!

➃畫布在狀態:statusOrign上進行系列操作。

四、恢復畫布

恢復畫布有兩個函數:restore()與restoreToCount()

restore():把回退棧中的最上層畫布狀態出棧,恢復畫布狀態。

restoreToCount(int count):

在save()、saveLayer()、saveLayerAlpha()函數保存畫布後,都會返回一個ID值,這個ID值表示當前保存的畫布信息的棧層索引(從0開始)。比如,保存在第三層,則返回2。

public void restoreToCount(int saveCount);

它表示一直退棧,直到把指定索引的畫布信息退出來,之後的棧最上層的畫布信息將作爲最新的畫布。

public class CustomView extends View {
    private Paint mPaint;

    public CustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mPaint = new Paint();
        mPaint.setColor(Color.RED);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        int id1 = canvas.save();
        canvas.clipRect(0, 0, 600, 600);
        canvas.drawColor(Color.RED);
        Log.d("TAG", "count:" + canvas.getSaveCount() + "  id1:" + id1);

        int id2 = canvas.saveLayer(0, 0, getWidth(), getHeight(), mPaint, Canvas.ALL_SAVE_FLAG);
        canvas.clipRect(100, 100, 500, 500);
        canvas.drawColor(Color.GREEN);
        Log.d("TAG", "count:" + canvas.getSaveCount() + "  id2:" + id2);

        int id3 = canvas.saveLayerAlpha(0, 0, getWidth(), getHeight(), 0xf0, Canvas.ALL_SAVE_FLAG);
        canvas.clipRect(200, 200, 400, 400);
        canvas.drawColor(Color.YELLOW);
        Log.d("TAG", "count:" + canvas.getSaveCount() + "  id3:" + id3);

        int id4 = canvas.save();
        canvas.clipRect(250, 250, 350, 350);
        canvas.drawColor(Color.BLUE);
        Log.d("TAG", "count:" + canvas.getSaveCount() + "  id4:" + id4);

        // canvas.restoreToCount(id3);
        // canvas.drawColor(Color.GRAY);
        // Log.d(TAG,"count:"+canvas.getSaveCount());
    }
}

打印日誌:

D/TAG: count:2  id1:1
D/TAG: count:3  id2:2
D/TAG: count:4  id3:3
D/TAG: count:5  id4:4

解註釋最後三行代碼運行。

D/TAG: count:2  id1:1
D/TAG: count:3  id2:2
D/TAG: count:4  id3:3
D/TAG: count:5  id4:4
D/TAG: count:3

從代碼和日誌可以看出,在調用canvas.restoreToCount(id3)函數後,將恢復到生成id3之前的畫布狀態,id3之前的畫布狀態就是(100,100,500,500)。

restore()與restoreToCount(int count)的關係:

這兩個函數針對的是同一個棧,所以完全可以通用。不同的是,restore()函數默認將棧頂內容退出還原畫布;而restoreToCount(int count)函數則一直退棧,直到把指定索引的畫布信息退出來,之後的棧最上層的畫布信息將作爲最新的畫布(即成爲當前畫布)。

結論:

(1)restore()的含義是把回退棧中的最上層畫布狀態出棧,恢復畫布狀態。restoreToCount(int count)的含義是一直退棧,直到把指定索引的畫布信息退出來,將此之前的所有動作都恢復。
(2)無論哪種save函數、哪個Flag,保存畫布時使用的都是同一個棧。
(3)restore()與restoreToCount(int count)針對的是同一個棧,所以完全可以通用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章