在一年的Android自學中,Canvas一直是我能避且避的類,甚至不惜封裝自己的繪相簿來替代它。
如今回首,虐我千萬次的Canvas也不過如此,靜下心看看,其實也沒有想象中的那麼糟糕。
就像曾經等級30的我去打點等級40的副本(Canvas)非常吃力,現在等級50的我回來吊打它一樣。
所以朋友,遇到承受不了的困擾,不要太沮喪,去別的地方刷怪升級,一旦境界提升了,早晚可以"報仇雪恨"
Android技術棧C模組
,第一篇正式開講:
如果將View、Canvas、Paint、Coder(編寫程式碼的人)做個類比:
那麼Canvas是一個黑匣子裡的白紙,它的特性是可以新增圖層和平移,旋轉、縮放、斜切等,最重要的就是它的n種drawXXX...
Paint是繪製用的畫筆,它的特性是提供繪製工具與制定畫筆的特殊效果(如筆頭Cap,線接方式Join,六種Effect)
View則是讓黑匣子變成透明的視口,也是我們最熟悉。那Coder就是在操縱畫筆的在白紙上繪製的人,是最核心的
一、前期準備:
1.自定義View中的canvas:
說起Canvas物件,貌似很少去new它,更多的是在自定義控制元件時的Ondraw方法裡回撥有canvas物件
public class CanvasView extends View {
public CanvasView(Context context) {
this(context, null);
}
public CanvasView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
//TODO init 初始化
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//TODO drawGrid 繪製網格
//TODO drawCoo 繪製座標系
}
}
複製程式碼
這裡值得提一下:onDarw中儘量不要建立物件,因為檢視更新都會走onDarw(),而不斷開闢空間
2.準備網格與座標系
如果要演示繪製,這兩者必不可少,放在analyze包裡
實現效果:給出座標原點後會自動繪製座標系以及網格和數字
1).使用方式:
//成員變數
private Paint mGridPaint;//網格畫筆
private Point mWinSize;//螢幕尺寸
private Point mCoo;//座標系原點
//TODO init 初始化:release:
//準備螢幕尺寸
mWinSize = new Point();
mCoo = new Point(500, 500);
Utils.loadWinSize(getContext(), mWinSize);
mGridPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
//TODO drawGrid 繪製網格:release:
HelpDraw.drawGrid(canvas, mWinSize, mGridPaint);
//TODO drawCoo 繪製座標系:release:
HelpDraw.drawCoo(canvas, mCoo, mWinSize, mGridPaint);
複製程式碼
2).網格--路徑輔助:com.toly1994.c.view.analyze.HelpPath#gridPath
/**
* 繪製網格:注意只有用path才能繪製虛線
*
* @param step 小正方形邊長
* @param winSize 螢幕尺寸
*/
public static Path gridPath(int step, Point winSize) {
Path path = new Path();
for (int i = 0; i < winSize.y / step + 1; i++) {
path.moveTo(0, step * i);
path.lineTo(winSize.x, step * i);
}
for (int i = 0; i < winSize.x / step + 1; i++) {
path.moveTo(step * i, 0);
path.lineTo(step * i, winSize.y);
}
return path;
}
複製程式碼
2).網格--繪製:com.toly1994.c.view.analyze.HelpDraw#drawGrid
/**
* 繪製網格
* @param canvas 畫布
* @param winSize 螢幕尺寸
* @param paint 畫筆
*/
public static void drawGrid(Canvas canvas, Point winSize, Paint paint) {
//初始化網格畫筆
paint.setStrokeWidth(2);
paint.setColor(Color.GRAY);
paint.setStyle(Paint.Style.STROKE);
//設定虛線效果new float[]{可見長度, 不可見長度},偏移值
paint.setPathEffect(new DashPathEffect(new float[]{10, 5}, 0));
canvas.drawPath(HelpPath.gridPath(50, winSize), paint);
}
複製程式碼
3).輔助--獲取螢幕尺寸:com.toly1994.c.view.analyze.Utils#loadWinSize
/**
* 獲得螢幕高度
*
* @param ctx 上下文
* @param winSize 螢幕尺寸
*/
public static void loadWinSize(Context ctx, Point winSize) {
WindowManager wm = (WindowManager) ctx.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics outMetrics = new DisplayMetrics();
if (wm != null) {
wm.getDefaultDisplay().getMetrics(outMetrics);
}
winSize.x = outMetrics.widthPixels;
winSize.y = outMetrics.heightPixels;
}
複製程式碼
4).座標系--路徑:com.toly1994.c.view.analyze.HelpPath#cooPath
/**
* 座標系路徑
*
* @param coo 座標點
* @param winSize 螢幕尺寸
* @return 座標系路徑
*/
public static Path cooPath(Point coo, Point winSize) {
Path path = new Path();
//x正半軸線
path.moveTo(coo.x, coo.y);
path.lineTo(winSize.x, coo.y);
//x負半軸線
path.moveTo(coo.x, coo.y);
path.lineTo(coo.x - winSize.x, coo.y);
//y負半軸線
path.moveTo(coo.x, coo.y);
path.lineTo(coo.x, coo.y - winSize.y);
//y負半軸線
path.moveTo(coo.x, coo.y);
path.lineTo(coo.x, winSize.y);
return path;
}
複製程式碼
5).座標系--繪製:com.toly1994.c.view.analyze.HelpDraw#drawCoo
/**
* 繪製座標系
* @param canvas 畫布
* @param coo 座標系原點
* @param winSize 螢幕尺寸
* @param paint 畫筆
*/
public static void drawCoo(Canvas canvas, Point coo, Point winSize, Paint paint) {
//初始化網格畫筆
paint.setStrokeWidth(4);
paint.setColor(Color.BLACK);
paint.setStyle(Paint.Style.STROKE);
//設定虛線效果new float[]{可見長度, 不可見長度},偏移值
paint.setPathEffect(null);
//繪製直線
canvas.drawPath(HelpPath.cooPath(coo, winSize), paint);
//左箭頭
canvas.drawLine(winSize.x, coo.y, winSize.x - 40, coo.y - 20, paint);
canvas.drawLine(winSize.x, coo.y, winSize.x - 40, coo.y + 20, paint);
//下箭頭
canvas.drawLine(coo.x, winSize.y, coo.x - 20, winSize.y - 40, paint);
canvas.drawLine(coo.x, winSize.y, coo.x + 20, winSize.y - 40, paint);
//為座標系繪製文字
drawText4Coo(canvas, coo, winSize, paint);
}
/**
* 為座標系繪製文字
*
* @param canvas 畫布
* @param coo 座標系原點
* @param winSize 螢幕尺寸
* @param paint 畫筆
*/
private static void drawText4Coo(Canvas canvas, Point coo, Point winSize, Paint paint) {
//繪製文字
paint.setTextSize(50);
canvas.drawText("x", winSize.x - 60, coo.y - 40, paint);
canvas.drawText("y", coo.x - 40, winSize.y - 60, paint);
paint.setTextSize(25);
//X正軸文字
for (int i = 1; i < (winSize.x - coo.x) / 50; i++) {
paint.setStrokeWidth(2);
canvas.drawText(100 * i + "", coo.x - 20 + 100 * i, coo.y + 40, paint);
paint.setStrokeWidth(5);
canvas.drawLine(coo.x + 100 * i, coo.y, coo.x + 100 * i, coo.y - 10, paint);
}
//X負軸文字
for (int i = 1; i < coo.x / 50; i++) {
paint.setStrokeWidth(2);
canvas.drawText(-100 * i + "", coo.x - 20 - 100 * i, coo.y + 40, paint);
paint.setStrokeWidth(5);
canvas.drawLine(coo.x - 100 * i, coo.y, coo.x - 100 * i, coo.y - 10, paint);
}
//y正軸文字
for (int i = 1; i < (winSize.y - coo.y) / 50; i++) {
paint.setStrokeWidth(2);
canvas.drawText(100 * i + "", coo.x + 20, coo.y + 10 + 100 * i, paint);
paint.setStrokeWidth(5);
canvas.drawLine(coo.x, coo.y + 100 * i, coo.x + 10, coo.y + 100 * i, paint);
}
//y負軸文字
for (int i = 1; i < coo.y / 50; i++) {
paint.setStrokeWidth(2);
canvas.drawText(-100 * i + "", coo.x + 20, coo.y + 10 - 100 * i, paint);
paint.setStrokeWidth(5);
canvas.drawLine(coo.x, coo.y - 100 * i, coo.x + 10, coo.y - 100 * i, paint);
}
}
複製程式碼
二、Canvas繪製基礎圖形(如果覺得簡單可跳過)
以前看到一個類有很多方法都有些不耐煩,這麼多,怎麼記得住。
現在看到一個類有很多方法,--哇,太好了,哈哈,竟然連這方法都有,作者真給力省的我實現了。
Canvas圖形繪製的API,所有的我分了一下類:如下(顏色、點、線、矩形、類圓、文字、圖片、其他)
下面一一介紹:
1.繪製顏色
/**
* 繪製顏色(注意在畫座標系前繪製,否則後者覆蓋)
* @param canvas
*/
private void drawColor(Canvas canvas) {
// canvas.drawColor(Color.parseColor("#E0F7F5"));
// canvas.drawARGB(255, 224, 247, 245);
// 三者等價
canvas.drawRGB(224, 247, 245);
}
複製程式碼
2.繪製點
/**
* 繪製點
* @param canvas
*/
private void drawPoint(Canvas canvas) {
//繪製點
canvas.drawPoint(100, 100, mRedPaint);
////繪製一組點,座標位置由float陣列指定(必須是2的倍數個)
canvas.drawPoints(new float[]{
400, 400, 500, 500,
600, 400, 700, 350,
800, 300, 900, 300
}, mRedPaint);
}
複製程式碼
3.繪製直線
/**
* 繪製線
* @param canvas
*/
private void drawLine(Canvas canvas) {
canvas.drawLine(500, 200, 900, 400, mRedPaint);
//繪製一組點,座標位置由float陣列指定(必須是4的倍數個)
canvas.drawLines(new float[]{
200, 200, 400, 200,
400, 200, 200, 400,
200, 400, 400, 400
}, mRedPaint);
}
複製程式碼
4.繪製矩形
/**
* 繪製矩形
*
* @param canvas
*/
private void drawRect(Canvas canvas) {
canvas.drawRect(100, 100, 500, 300, mRedPaint);
//等價上行
// Rect rect = new Rect(100, 100, 500, 300);
// canvas.drawRect(rect,mRedPaint);
//(左上右下X圓角,Y圓角)
canvas.drawRoundRect(100 + 500, 100, 500 + 500, 300, 50, 50, mRedPaint);
}
複製程式碼
5.繪製類圓
/**
* 繪製類圓
*
* @param canvas
*/
private void drawLikeCircle(Canvas canvas) {
//繪製圓(矩形邊界,畫筆)
canvas.drawCircle(650, 200, 100, mRedPaint);
// canvas.drawOval(100, 100, 500, 300, mRedPaint);
//等價上行
//繪製橢圓(矩形邊界,畫筆)
RectF rect = new RectF(100, 100, 500, 300);
canvas.drawOval(rect, mRedPaint);
RectF rectArc = new RectF(100 + 500, 100, 500 + 500, 300);
//繪製圓弧(矩形邊界,開始角度,掃過角度,使用中心?邊緣兩點與中心連線區域:邊緣兩點連線區域)
canvas.drawArc(rectArc, 0, 90, true, mRedPaint);
RectF rectArc2 = new RectF(100 + 500 + 300, 100, 500 + 500 + 300, 300);
//繪製圓弧(矩形邊界,開始角度,掃過角度,使用中心?邊緣兩點與中心連線區域:邊緣兩點連線區域)
canvas.drawArc(rectArc2, 0, 90, false, mRedPaint);
}
複製程式碼
6.繪製圖片
/**
* 繪製圖片
* @param canvas
*/
private void drawBitmap(Canvas canvas) {
//1.定點繪製圖片
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.menu_bg);
canvas.drawBitmap(bitmap, 100, 100, mRedPaint);
//2.適用變換矩陣繪製圖片
Matrix matrix = new Matrix();
//設定變換矩陣:縮小3倍,斜切0.5,右移150,下移100
matrix.setValues(new float[]{
1, 0.5f, 1500 * 3,
0, 1, 100 * 3,
0, 0, 3
});
canvas.drawBitmap(bitmap, matrix, mRedPaint);
//3.圖片適用矩形區域不剪裁
RectF rectf1 = new RectF(100 + 900, 100, 600 + 900, 400);
canvas.drawBitmap(bitmap, null, rectf1, mRedPaint);
//4.圖片裁剪出的矩形區域
Rect rect = new Rect(300, 300, 400, 400);
//圖片適用矩形區域
RectF rectf2 = new RectF(100 + 900, 100 + 400, 600 + 900, 400 + 400);
canvas.drawBitmap(bitmap, rect, rectf2, mRedPaint);
}
複製程式碼
7.繪製Picture
1).一開始挺納悶Picture不就是圖片嗎?然後翻了一下API:
/**
* A Picture records drawing calls (via the canvas returned by beginRecording)
* and can then play them back into Canvas (via {@link Picture#draw(Canvas)} or
* {@link Canvas#drawPicture(Picture)}).For most content (e.g. text, lines, rectangles),
* drawing a sequence from a picture can be faster than the equivalent API
* calls, since the picture performs its playback without incurring any
* method-call overhead.
*
* <p class="note"><strong>Note:</strong> Prior to API level 23 a picture cannot
* be replayed on a hardware accelerated canvas.</p>
一個Picture類記錄繪製(通過beginRecording方法返回的Canvas),可以展示這個Canvas
到其他Canvas上(通過Picture#draw(Canvas)或者Canvas#drawPicture(Picture)),
對於大多數的內容,從picture繪製都要比相應的API要快速,因為picture的展現不會招致方法呼叫開銷
在API級別23之前,無法在硬體加速畫布上展示Picture(自譯,僅供參考)
複製程式碼
2).通過一段程式碼你就能很清楚它是幹嘛用的
如果繪製一個品字,需要這樣:
private void drawPicture(Canvas canvas) {
canvas.drawRect(100, 0, 200, 100, mRedPaint);
canvas.drawRect(0, 100, 100, 200, mRedPaint);
canvas.drawRect(200, 100, 300, 200, mRedPaint);
}
複製程式碼
如果想要複用這個品字形,大多數人知道,平移畫布,複製貼上,
對於少量的程式碼,這還可以接收,如果是非常複雜的圖形,每次繪製重複的內容,會浪費效能
private void drawPicture(Canvas canvas) {
//建立Picture物件
Picture picture = new Picture();
//確定picture產生的Canvas元件的大小,並生成Canvas元件
Canvas recodingCanvas = picture.beginRecording(canvas.getWidth(), canvas.getHeight());
//Canvas元件的操作
recodingCanvas.drawRect(100, 0, 200, 100, mRedPaint);
recodingCanvas.drawRect(0, 100, 100, 200, mRedPaint);
recodingCanvas.drawRect(200, 100, 300, 200, mRedPaint);
//Canvas元件繪製結束
picture.endRecording();
canvas.save();
canvas.drawPicture(picture);//使用picture的Canvas元件
canvas.translate(0, 300);
picture.draw(canvas);//同上:使用picture的Canvas元件
canvas.drawPicture(picture);
canvas.translate(350, 0);
canvas.drawPicture(picture);
canvas.restore();
複製程式碼
Picture相當於先拍一張照片,並且是在別的Canvas上,在別的Canvas上,在別的Canvas上!
重要的話說三遍:當需要的時候在貼在當前的canvas上,picture繪製的優勢就是節能減排
當有大量複雜內容需要複用,Picture這個的canvas元件是不二的選擇:
8.繪製文字(文字的效果有Paint決定,細節將在Paint篇介紹)
/**
* 繪製文字
*
* @param canvas
*/
private void drawText(Canvas canvas) {
mRedPaint.setTextSize(100);
canvas.drawText("張風捷特烈--Toly", 200, 300, mRedPaint);
}
複製程式碼
無聊的程式碼終於敲完了,進入正題。
三、Canvas的畫布變換
以前對Canvas的變換很厭倦,現在看了鍵值是神技
作為一代PS大神的我,理解Canvas狀態儲存與恢復本應易如反掌,為何最近才豁然開朗
1.先看下面的圖形:將座標系原點設為(500,500)
private void stateTest(Canvas canvas) {
canvas.drawLine(mCoo.x + 500, mCoo.y + 200, mCoo.x + 900, mCoo.y + 400, mRedPaint);
// canvas.rotate(45);
canvas.drawRect(mCoo.x + 100, mCoo.x + 100, mCoo.y + 300, mCoo.y + 200, mRedPaint);
}
複製程式碼
問題來了,想畫一個斜45度的矩形怎麼辦?
貌似沒有斜矩形的API,一個一個點找,貌似太麻煩了,我把紙轉一下不就行了嗎!
紙就是Canvas,看一下API,果然有rotate()方法,懷著忐忑的心情:
private void stateTest(Canvas canvas) {
canvas.drawLine(mCoo.x + 500, mCoo.y + 200, mCoo.x + 900, mCoo.y + 400, mRedPaint);
canvas.rotate(45);
canvas.drawRect(mCoo.x + 100, mCoo.x + 100, mCoo.y + 300, mCoo.y + 200, mRedPaint);
}
複製程式碼
果然轉得天翻地覆
2.圖層的概念
PS中的圖層可謂PS的精華,它保證了在一個圖層中繪製而不會影響到其他的圖層
在Canvas中每次的save()都存將先前的狀態儲存下來,產生一個新的繪圖層,
我們可以隨心所欲地地畫而不會影響其他已畫好的圖,最後用restore()將這個圖層合併到原圖層
這像是棧的概念,每次save(),新圖層入棧(注意可以save多次),只有棧頂的層可以進行操作,restore()彈棧
3.旋轉畫布:rotate()
private void stateTest(Canvas canvas) {
canvas.drawLine(mCoo.x + 500, mCoo.y + 200, mCoo.x + 900, mCoo.y + 400, mRedPaint);
canvas.drawRect(mCoo.x + 100, mCoo.x + 100, mCoo.y + 300, mCoo.y + 200, mRedPaint);
canvas.save();//儲存canvas狀態
//(角度,中心點x,中心點y)
canvas.rotate(45, mCoo.x + 100, mCoo.y + 100);
mRedPaint.setColor(Color.parseColor("#880FB5FD"));
canvas.drawRect(mCoo.x + 100, mCoo.x + 100, mCoo.y + 300, mCoo.y + 200, mRedPaint);
canvas.restore();//圖層向下合併
}
複製程式碼
4.平移畫布:translate():寫一堆mCoo,也就是讓畫布移動一下而已
效果必變,是不是清爽許多
private void stateTest(Canvas canvas) {
canvas.save();
canvas.translate(mCoo.x, mCoo.y);//將原點平移到座標系原點
canvas.drawLine(500, 200, 900, 400, mRedPaint);
canvas.drawRect(100, 100, 300, 200, mRedPaint);
canvas.save();//儲存canvas狀態
//(角度,中心點x,中心點y)
canvas.rotate(45, 100, 100);
mRedPaint.setColor(Color.parseColor("#880FB5FD"));
canvas.drawRect(100, 100, 300, 200, mRedPaint);
canvas.restore();//圖層向下合併
canvas.restore();
}
複製程式碼
5.縮放畫布:scale()
private void stateTest(Canvas canvas) {
canvas.save();
canvas.translate(mCoo.x, mCoo.y);//將原點平移到座標系原點
canvas.drawLine(500, 200, 900, 400, mRedPaint);
canvas.drawRect(100, 100, 300, 200, mRedPaint);
canvas.save();//儲存canvas狀態
//(角度,中心點x,中心點y)
canvas.scale(2, 2, 100, 100);
mRedPaint.setColor(Color.parseColor("#880FB5FD"));
canvas.drawRect(100, 100, 300, 200, mRedPaint);
canvas.restore();//圖層向下合併
canvas.restore();
}
複製程式碼
6.斜切畫布:scale()
private void stateTest(Canvas canvas) {
canvas.save();
canvas.translate(mCoo.x, mCoo.y);//將原點平移到座標系原點
canvas.drawLine(500, 200, 900, 400, mRedPaint);
canvas.drawRect(100, 100, 300, 200, mRedPaint);
canvas.save();//儲存canvas狀態
canvas.skew(1f,0f);
mRedPaint.setColor(Color.parseColor("#880FB5FD"));
canvas.drawRect(100, 100, 300, 200, mRedPaint);
canvas.restore();//圖層向下合併
canvas.restore();
}
複製程式碼
7.畫布選擇儲存狀態:
public int save (int saveFlags)
預設:MATRIX_SAVE_FLAG | CLIP_SAVE_FLAG
ALL_SAVE_FLAG 儲存全部狀態
CLIP_SAVE_FLAG 過期---僅儲存剪輯區(不作為圖層)
CLIP_TO_LAYER_SAVE_FLAG 過期---僅剪裁區作為圖層儲存
FULL_COLOR_LAYER_SAVE_FLAG 過期---僅儲存圖層的全部色彩通道
HAS_ALPHA_LAYER_SAVE_FLAG 過期---僅儲存圖層的alpha(不透明度)通道
MATRIX_SAVE_FLAG 過期---僅儲存Matrix資訊( translate, rotate, scale, skew)
複製程式碼
int count = canvas.getSaveCount();//獲取圖層的個數3
canvas.restoreToCount(1);//直接恢復到第幾個圖層
複製程式碼
四、Canvas的裁剪
1.可見主要就兩種型別,內裁剪和外裁剪,Op的操作被廢棄了
2.內剪裁:(區域內的之後繪製的內容儲存)
private void clip(Canvas canvas) {
//剪裁區域
Rect rect = new Rect(20, 100, 250, 300);
canvas.clipRect(rect);
canvas.drawRect(0, 0, 200, 300, mRedPaint);
}
複製程式碼
3.外剪裁:(區域外的之後繪製的內容儲存)--注意API26及以上可用
private void clip(Canvas canvas) {
//剪裁區域
Rect rect = new Rect(20, 100, 250, 300);
canvas.clipOutRect(rect);
canvas.drawRect(0, 0, 200, 300, mRedPaint);
}
複製程式碼
Canvas的相關內容就到這裡(注:路徑的繪製會在Path篇精講),下一節將帶來畫筆Paint的知識
後記:捷文規範
1.本文成長記錄及勘誤表
專案原始碼 | 日期 | 備註 |
---|---|---|
V0.1--無 | 2018-11-5 | Android關於Canvas你所知道的和不知道的一切 |
V0.2--無 | 2018-11-6 | 增加繪製Picture的內容 |
2.更多關於我
筆名 | 微信 | 愛好 | |
---|---|---|---|
張風捷特烈 | 1981462002 | zdl1994328 | 語言 |
我的github | 我的簡書 | 我的CSDN | 個人網站 |
3.宣告
1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大程式設計愛好者共同交流
3----個人能力有限,如有不正之處歡迎大家批評指證,必定虛心改正
4----看到這裡,我在此感謝你的喜歡與支援