前言
在前面我們把Paint關于UI顏色樣式的處理進行了學習, 其實真正高級部分就是三個點,渲染,濾鏡,圖形組合,而我們圖形繪制比較重要的另一個物件Canvas也是需要我們去重點掌握的,那么這次咱們來進行Canvas的深層次的學習,主要了解有兩個點
- Canvas的變換使用技巧
- Canvas的狀態,Canvas Layer
1.Canvas基本概念
直面意思是畫布,其實是分裝的一個工具類(繪制會話,用來和底層溝通最終交給底層繪制),一個Canvas類物件有四大基本要素:
- 一個是用來保存像素的bitmap
- 一個Canvas在Bitmap上進行繪制操作
- 繪制的東西
- 繪制的畫筆Paint
2.Canvas變換操作----坐標系概念
在我們進行canvas操作的時候我們會有一個問題產生,在進行圖形的平移,旋轉操作時,我們沒有去更改原始的坐標,只通過了非常簡單的幾個api就直接進行了移動,那么中間他的具體到底是發生了什么,通過之前在繪制流程當中draw時我們發現在下面我已經縮減了之后的代碼上我門發現, 在繪制之初就產生了一個矩形,并且他通過面板進行了一次初始化
private void draw(boolean fullRedrawNeeded) {
Surface surface = mSurface;
...
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;
}
if (fullRedrawNeeded) {
mAttachInfo.mIgnoreDirtyState = true;
dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
}
int xOffset = -mCanvasOffsetX;
int yOffset = -mCanvasOffsetY + curScrollY;
final WindowManager.LayoutParams params = mWindowAttributes;
final Rect surfaceInsets = params != null ? params.surfaceInsets : null;
if (surfaceInsets != null) {
xOffset -= surfaceInsets.left;
yOffset -= surfaceInsets.top;
// Offset dirty rect for surface insets.
dirty.offset(surfaceInsets.left, surfaceInsets.right);
}
......
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
return;
}
}
}
if (animating) {
mFullRedrawNeeded = true;
scheduleTraversals();
}
}
那么在上面的代碼當中我門可以看到在繪制開始之初, 在底層就確定了一個繪制區域,確定了canvas繪制位置的坐標,那么這個就是被稱之為我門canvas的坐標系,確定我們canvas繪制圖形的位置,那么,當我們進行了
canvas.translate(50, 50);
canvas.rotate(45);
canvas.scale(1.5f, 0.5f);
canvas.skew(1.73f, 0);
等操作的時候,我們的圖形繪制會直接發生改變,那么這個時候我門考慮一個問題,下圖中綠色的點移動到紅色的點,我們剛才所設定的canvas移動了嗎

其實很多通過會在這里認為我們canvas的坐標進行了移動,其實不然,在Canvas里面牽扯兩種坐標系:Canvas自己的坐標系、繪圖坐標系
2.1 Canvas的坐標系
它就在View的左上角,做坐標原點往右是X軸正半軸,往下是Y軸的正半軸,有且只有一個,唯一不變這一個其實就是在我們canvas當中在繪制之初由surface所初始化的那個點
2.2 繪圖坐標系
它不是唯一不變的,它與Canvas的Matrix有關系,當Matrix發生改變的時候,繪圖坐標系對應的進行改變,他有一個特性就是在這個程序中是不可逆的
那么其實實際就是我們在畫圖的時候,有一塊總面板,總面板不動, 而當我在開始進行繪制圖形的時候,有一個時時刻刻在動的面板,而這個面板就是具體去繪制我們圖形的畫板

那么里層的繪圖坐標系他的實際是用一個Matrix矩陣表示的,這個和我門之前的濾鏡矩陣表示差不多,只不過,繪圖坐標系的矩陣是一個2x2的矩陣傳入的值是由我們的canvas進行決議之后將自己想要的資料給底層底層自己計算所得
public void drawRect(@NonNull Rect r, @NonNull Paint paint) {
throwIfHasHwBitmapInSwMode(paint);
drawRect(r.left, r.top, r.right, r.bottom, paint);
}
那么在這里我們可以看到在進入底層native方法之前,實作會根據每一種繪制的不同對底層的資料進行傳入, 然后會計算出我門的繪制坐標系(此處底層不看,涉及c,我們這里明白這一點就行)
我們通過簡單設定translate、rotate、scale、skew來改變我們繪制圖形的位置時他的計算時依賴與另外一個矩陣來對繪圖坐標系進行改變,這是一個3x3的矩陣,它里面的九個引數
cosX -sinX translateX
sinX cosX translateY
0 0 scale
其中,sinX和cosX,代表的是旋轉角度的sin和cos值,注意旋轉的正方向是順時針方向,translateX和translateY代表的是平移的X和Y,scale代表的是縮放的大小,
我們可以通過getMatrix()的到這個矩陣,而通過看到底層原始碼,這里我能清晰的看到我們是直接呼叫底層的矩陣
@Deprecated
public void getMatrix(@NonNull Matrix ctm) {
nGetMatrix(mNativeCanvasWrapper, ctm.native_instance);
}
那么這里我做了一組測驗
RectF r = new RectF(0, 0, 400, 500);
paint.setColor(Color.GREEN);
canvas.drawRect(r, paint);
float[] fs = new float[10];
canvas.getMatrix().getValues(fs);
for (int i = 0;i < fs.length;i++){
Log.i("barry","fs:"+fs[i]);
}
//平移
canvas.translate(50, 50);
float[] fs2 = new float[10];
canvas.getMatrix().getValues(fs2);
for (int i = 0;i < fs2.length;i++){
Log.i("barry","fs2:"+fs2[i]);
}
paint.setColor(Color.BLUE);
canvas.drawRect(r, paint);

可以很明顯看到,矩陣進行平移之后這個矩陣資訊的變化,那么注意,繪圖矩陣的坐標系移動是一個不可逆轉的狀態也就是說,一旦矩陣移動完成之后,那么他不能回到之前的位置,具體效果如下

但是在我們的Canvas當中提供了save和restore方法來保存和還原變化操作,
RectF r = new RectF(0, 0, 400, 500);
paint.setColor(Color.GREEN);
//畫完之后,繪圖坐標系定位在此處
canvas.drawRect(r, paint);
//save保存當前坐標
canvas.save();
//平移之后,坐標系發生改變
canvas.translate(50, 50);
paint.setColor(Color.BLUE);
canvas.drawRect(r, paint);
//通過restore進行還原到save保存時的坐標系
canvas.restore();

但是想要知道這兩個方法是怎么進行操作的才能讓我們更加深入的去熟悉Canvas的使用技巧,那么我門必須去了解Canvas的狀態堆疊、Layer堆疊
3. Canvas的狀態保存—狀態堆疊、Layer堆疊
3.1 狀態堆疊
在前面我們提到坐標系的轉換是一個不可逆轉的,而我們可以通過save來進行保存restore進行恢復,其實我們在進行save操作時在canvas當中會將我門save下來的坐標系進行保存到一個堆疊當中,并且可以通過restore或者是restoreToCount進行操作下面通過一段測驗代碼我門印證下
public class MyView extends View {
private static final String TAG = "BARRY";
private Paint mPaint = null;
private Bitmap mBitmap = null;
public MyView(Context context) {
this(context, null);
}
public MyView(Context context, AttributeSet attrs) {
super(context, attires
mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.lsj);
init();
}
public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
private void init() {
mPaint = new Paint();
mPaint.setColor(Color.RED);
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setStrokeWidth(10);
}
@Override
protected void onDraw(Canvas canvas) {
//第1次保存,并通過canvas.getSaveCount的到當前狀態堆疊容量
canvas.save();
Log.i(TAG, "Current SaveCount = " + canvas.getSaveCount());
canvas.translate(400, 400);
RectF rectF = new RectF(0,0,600,600);
canvas.drawBitmap(mBitmap, null, rectF, mPaint);
//第2次保存,并通過canvas.getSaveCount的到當前狀態堆疊容量
canvas.save();
Log.i(TAG, "Current SaveCount = " + canvas.getSaveCount());
canvas.rotate(45);
canvas.drawBitmap(mBitmap, null, rectF, mPaint);
//第3次保存,并通過canvas.getSaveCount的到當前狀態堆疊容量
canvas.save();
Log.i(TAG, "Current SaveCount = " + canvas.getSaveCount());
canvas.rotate(45);
canvas.drawBitmap(mBitmap, null, rectF, mPaint);
//第4次保存,并通過canvas.getSaveCount的到當前狀態堆疊容量
canvas.save();
Log.i(TAG, "Current SaveCount = " + canvas.getSaveCount());
//通過canvas.restoreToCount出堆疊到第三層狀態
canvas.restoreToCount(3);
Log.i(TAG, "restoreToCount--Current SaveCount = " + canvas.getSaveCount());
canvas.translate(0, 200);
//rectF = new RectF(0,0,600,600);
canvas.drawBitmap(mBitmap, null, rectF, mPaint);
//通過canvas.restoreToCount出堆疊到第1層(最原始的那一層)狀態
canvas.restoreToCount(1);
Log.i(TAG, "restoreToCount--Current SaveCount = " + canvas.getSaveCount());
canvas.drawBitmap(mBitmap, null, rectF, mPaint);
}
}



那么其實我們這樣可以直接明白, 每一次的save其實實際上是用了一個堆疊保存了我的繪圖坐標系,這個堆疊被我們稱之為狀態堆疊起來, 而我們的restore就是一個出堆疊的程序,save、 restore方法來保存和還原變換操作Matrix以及Clip剪裁
3.2 Layer堆疊
在我們的canvas當中,提供了一個saveLayer的api主要做用是用來新建一個圖層,后續的繪圖操作都在新建的layer上面進行,當我們呼叫restore 或者 restoreToCount 時 更新到對應的圖層和畫布上

下面通過這段測驗代碼的效果我們來驗證當前的結論
public class MyView extends View {
Paint mPaint;
float mItemSize = 0;
float mItemHorizontalOffset = 0;
float mItemVerticalOffset = 0;
float mCircleRadius = 0;
float mRectSize = 0;
int mCircleColor = 0xffffcc44;//黃色
int mRectColor = 0xff66aaff;//藍色
float mTextSize = 25;
private static final Xfermode[] sModes = {
new PorterDuffXfermode(PorterDuff.Mode.CLEAR),
new PorterDuffXfermode(PorterDuff.Mode.SRC),
new PorterDuffXfermode(PorterDuff.Mode.DST),
new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER),
new PorterDuffXfermode(PorterDuff.Mode.DST_OVER),
new PorterDuffXfermode(PorterDuff.Mode.SRC_IN),
new PorterDuffXfermode(PorterDuff.Mode.DST_IN),
new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT),
new PorterDuffXfermode(PorterDuff.Mode.DST_OUT),
new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP),
new PorterDuffXfermode(PorterDuff.Mode.DST_ATOP),
new PorterDuffXfermode(PorterDuff.Mode.XOR),
new PorterDuffXfermode(PorterDuff.Mode.DARKEN),
new PorterDuffXfermode(PorterDuff.Mode.LIGHTEN),
new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY),
new PorterDuffXfermode(PorterDuff.Mode.SCREEN)
};
private static final String[] sLabels = {
"Clear", "Src", "Dst", "SrcOver",
"DstOver", "SrcIn", "DstIn", "SrcOut",
"DstOut", "SrcATop", "DstATop", "Xor",
"Darken", "Lighten", "Multiply", "Screen"
};
public MyView(Context context) {
super(context);
init(null, 0);
}
public MyView(Context context, AttributeSet attrs) {
super(context, attires
init(attrs, 0);
}
public MyView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(attrs, defStyle);
}
private void init(AttributeSet attrs, int defStyle) {
if(Build.VERSION.SDK_INT >= 11){
setLayerType(LAYER_TYPE_SOFTWARE, null);
}
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setTextSize(mTextSize);
mPaint.setTextAlign(Paint.Align.CENTER);
mPaint.setStrokeWidth(2);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//設定背景色
canvas.drawARGB(255, 139, 197, 186);
int canvasWidth = canvas.getWidth();
int canvasHeight = canvas.getHeight();
for(int row = 0; row < 4; row++){
for(int column = 0; column < 4; column++){
canvas.save();
//此處是建立新的圖層
int layer = canvas.saveLayer(0, 0, canvasWidth, canvasHeight, null, Canvas.ALL_SAVE_FLAG);
mPaint.setXfermode(null);
int index = row * 4 + column;
float translateX = (mItemSize + mItemHorizontalOffset) * column;
float translateY = (mItemSize + mItemVerticalOffset) * row;
canvas.translate(translateX, translateY);
//畫文字
String text = sLabels[index];
mPaint.setColor(Color.BLACK);
float textXOffset = mItemSize / 2;
float textYOffset = mTextSize + (mItemVerticalOffset - mTextSize) / 2;
canvas.drawText(text, textXOffset, textYOffset, mPaint);
canvas.translate(0, mItemVerticalOffset);
//畫邊框
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(0xff000000);
canvas.drawRect(2, 2, mItemSize - 2, mItemSize - 2, mPaint);
mPaint.setStyle(Paint.Style.FILL);
//畫圓
mPaint.setColor(mCircleColor);
float left = mCircleRadius + 3;
float top = mCircleRadius + 3;
canvas.drawCircle(left, top, mCircleRadius, mPaint);
mPaint.setXfermode(sModes[index]);
//畫矩形
mPaint.setColor(mRectColor);
float rectRight = mCircleRadius + mRectSize;
float rectBottom = mCircleRadius + mRectSize;
canvas.drawRect(left, top, rectRight, rectBottom, mPaint);
mPaint.setXfermode(null);
//canvas.restore();
canvas.restoreToCount(layer);
}
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, old);)
mItemSize = w / 4.5f;
mItemHorizontalOffset = mItemSize / 6;
mItemVerticalOffset = mItemSize * 0.426f;
mCircleRadius = mItemSize / 3;
mRectSize = mItemSize * 0.6f;
}
}
沒有加saveLayer的效果

加了saveLayer的效果

這段代碼我們可以看到,其實實際上就是我們之前的文章當中,xfermode的演示代碼,而在這段代碼當中我才用了saveLayer進行操作,通過上面兩個結果,一個是我加了saveLayer的,一個是沒加的, 那么從中我門可以明顯看到在沒有加的時候,xfermode的像素輸出效果直接將外層背景色也給清空了,而加入之后沒有,那么其實我們可以很明顯的明白如果用了layer那么其實實際上我們是在當前這個canvas圖形上面新建了一個圖層當我們呼叫restore 或者 restoreToCount 時 我們的繪制會更新到當前圖層
那么這個時候我們來詳細分析saveLayer的引數
canvas.saveLayer(0, 0, canvasWidth, canvasHeight, null, Canvas.ALL_SAVE_FLAG);
/**
* Helper version of saveLayer() that takes 4 values rather than a RectF.
*
* @deprecated Use {@link #saveLayer(float, float, float, float, Paint)} instead.
*/
public int saveLayer(float left, float top, float right, float bottom, @Nullable Paint paint,
@Saveflags int saveFlags) {
return nSaveLayer(mNativeCanvasWrapper, left, top, right, bottom,
paint != null ? paint.getNativeInstance() : 0,
saveFlags);
}
通過上訴方法的注釋,以及代碼我們明顯知道,前面四個引數,為上下左右四個點構成一個圖層區,Paint畫筆也可以繼承過來,而最后一個引數表示的是我們當前的保存形式,總共下面6種,這六個模式其實實際上講的就是告訴canvas當前保存那些資訊
- MATRIX_SAVE_FLAG:只保存圖層的matrix矩陣 save,saveLayer
- CLIP_SAVE_FLAG:只保存大小資訊 save,saveLayer
- HAS_ALPHA_LAYER_SAVE_FLAG:表明該圖層有透明度,和下面的標識沖突,都設定時以下面的標志為準
- FULL_COLOR_LAYER_SAVE_FLAG:完全保留該圖層顏色(和上一圖層合并時,清空上一圖層的重疊區域,保留該圖層的顏色)
- CLIP_TO_LAYER_SAVE_:創建圖層時,會把canvas(所有圖層)裁剪到引數指定的范圍,如果省略這個flag將導致圖層開銷巨大(實際上圖層沒有裁剪,與原圖層一樣大)
- ALL_SAVE_FLAG:保存所有資訊 save,saveLayer
從原始碼當中我發現其他幾種模式在高版本當中已經剔除,只保留了一種,就是我門的all_save_flag
/** @hide */
@IntDef(flag = true,
value = {
ALL_SAVE_FLAG
})
@Retention(RetentionPolicy.SOURCE)
public @interface Saveflags {}
那么這個時候我們來測驗一下
public class MyView3 extends View {
public MyView3(Context context) {
super(context);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
RectF rectF = new RectF(0,0,400,500);
Paint paint = new Paint();
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(10);
paint.setColor(Color.GREEN);
canvas.drawRect(rectF, paint);
canvas.translate(50,50);
canvas.saveLayer(0,0,canvas.getWidth(),canvas.getHeight(),null,Canvas.ALL_SAVE_FLAG);
//canvas.save();
canvas.drawColor(Color.BLUE);// 通過drawColor可以發現saveLayer是新建了一個圖層,
// 同時結合Lsn5的16種Xfermode疊加形式Demo可以驗證是新建的透明圖層
paint.setColor(Color.YELLOW);
canvas.drawRect(rectF,paint);
//canvas.restore();
canvas.restore();
RectF rectF1 = new RectF(10,10,300,400);
paint.setColor(Color.RED);
canvas.drawRect(rectF1,paint);
}
加了saveLayer

沒加saveLayer

那么這段代碼也驗證了我們上訴的理論,在加了saveLayer之后,背景色被繪制到了另外一個圖層導致前面有一節空白的,同時也得出了一個有趣的結論,貌似,平移操作也被繼承了,其實這里我們的出一個結論saveLayer會將之前的一些Canvas狀態操作延續過來,這里是通過之前的最后一個引數設定成ALL_SAVE_FLAG完成,他在新建圖層的時候完成了保留當前所有資訊狀態的操作.
總結
Canvas里面牽扯兩種坐標系: Canvas自己的坐標系、繪圖坐標系
- Canvas的坐標系:它就在View的左上角,做坐標原點往右是X軸正半軸,往下是Y軸的正半軸,有且只有一個,唯一不變
- 繪圖坐標系:它不是唯一不變的,它與Canvas的Matrix有關系,當Matrix發生改變的時候,繪圖坐標系對應的進行改變,同時這個程序是不可逆的(save和restore方法來保存和還原變化操作),Matrix又是通過我們設定translate、rotate、scale、skew來進行改變的
Canvas的狀態保存—狀態堆疊、Layer堆疊
- 狀態堆疊– save、 restore方法來保存和還原變換操作Matrix以及Clip剪裁,也可以通過restoretoCount直接還原到對應堆疊的保存狀態
- Layer堆疊— saveLayer的時候都會新建一個透明的圖層(離屏Bitmap-離屏緩沖),并且會將saveLayer之前的一些Canvas操作延續過來,后續的繪圖操作都在新建的layer上面進行,當我們呼叫restore 或者 restoreToCount 時 更新到對應的圖層和畫布上
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/377138.html
標籤:其他
上一篇:記事本(一)
