前言
- 自定義View是Android開發者必須了解的基礎;而Canvas類的使用在自定義View繪制中發揮著非常重要的作用
- 網上有大量關于自定義View中Canvas類的文章,但存在一些問題:內容不全、思路不清晰、簡單問題復雜化等等
- 今天,我將全面總結自定義View中的Canvas類的使用,我能保證這是市面上的最全面、最清晰、最易懂的
Carson帶你學Android自定義View文章系列:
Carson帶你學Android:自定義View基礎
Carson帶你學Android:一文梳理自定義View工作流程
Carson帶你學Android:自定義View繪制準備-DecorView創建
Carson帶你學Android:自定義View Measure過程
Carson帶你學Android:自定義View Layout過程
Carson帶你學Android:自定義View Draw過程
Carson帶你學Android:手把手教你寫一個完整的自定義View
Carson帶你學Android:Canvas類全面解析
Carson帶你學Android:Path類全面解析
目錄
1. 簡介
- 定義:畫布,是一種繪制時的規則
是安卓平臺2D圖形繪制的基礎
- 作用:規定繪制內容時的規則 & 內容
- 記住:繪制內容是根據畫布的規定繪制在屏幕上的
- 理解為:畫布只是繪制時的規則,但內容實際上是繪制在屏幕上的
2. 本質
請務必記住:
- 繪制內容是根據畫布(Canvas)的規定繪制在屏幕上的
- 畫布(Canvas)只是繪制時的規則,但內容實際上是繪制在屏幕上的
為了更好地說明繪制內容的本質和Canvas,請看下面例子:
2.1 實例
- 實例情況:先畫一個矩形(藍色);然后移動畫布;再畫一個矩形(紅色)
- 代碼分析:
// 畫一個矩形(藍色)
canvas.drawRect(100, 100, 150, 150, mPaint1);
// 將畫布的原點移動到(400,500)
canvas.translate(400,500);
// 再畫一個矩形(紅色)
canvas.drawRect(100, 100, 150, 150, mPaint2);
- 效果圖
- 具體流程分析
看完上述分析,你應該非常明白Canvas的本質了。
- 總結
繪制內容是根據畫布的規定繪制在屏幕上的
- 內容實際上是繪制在屏幕上;
- 畫布,即Canvas,只是規定了繪制內容時的規則;
- 內容的位置由坐標決定,而坐標是相對于畫布而言的
注:關于對畫布的操作(縮放、旋轉和錯切)原理都是相同的,下面會詳細說明。
3. 基礎
3.1 Paint類
- 定義:畫筆
- 作用:確定繪制內容的具體效果(如顏色、大小等等)
在繪制內容時需要畫筆Paint
- 具體使用:
步驟1:創建一個畫筆對象
步驟2:畫筆設置,即設置繪制內容的具體效果(如顏色、大小等等)
步驟3:初始化畫筆(盡量選擇在View的構造函數)
具體使用如下:
// 步驟1:創建一個畫筆
private Paint mPaint = new Paint();
// 步驟2:初始化畫筆
// 根據需求設置畫筆的各種屬性,具體如下:
private void initPaint() {
// 設置最基本的屬性
// 設置畫筆顏色
// 可直接引入Color類,如Color.red等
mPaint.setColor(int color);
// 設置畫筆模式
mPaint.setStyle(Style style);
// Style有3種類型:
// 類型1:Paint.Style.FILLANDSTROKE(描邊+填充)
// 類型2:Paint.Style.FILL(只填充不描邊)
// 類型3:Paint.Style.STROKE(只描邊不填充)
// 具體差別請看下圖:
// 特別注意:前兩種就相差一條邊
// 若邊細是看不出分別的;邊粗就相當于加粗
//設置畫筆的粗細
mPaint.setStrokeWidth(float width)
// 如設置畫筆寬度為10px
mPaint.setStrokeWidth(10f);
// 不常設置的屬性
// 得到畫筆的顏色
mPaint.getColor()
// 設置Shader
// 即著色器,定義了圖形的著色、外觀
// 可以繪制出多彩的圖形
// 具體請參考文章:http://blog.csdn.net/iispring/article/details/50500106
Paint.setShader(Shader shader)
//設置畫筆的a,r,p,g值
mPaint.setARGB(int a, int r, int g, int b)
//設置透明度
mPaint.setAlpha(int a)
//得到畫筆的Alpha值
mPaint.getAlpha()
// 對字體進行設置(大小、顏色)
//設置字體大小
mPaint.setTextSize(float textSize)
// 文字Style三種模式:
mPaint.setStyle(Style style);
// 類型1:Paint.Style.FILLANDSTROKE(描邊+填充)
// 類型2:Paint.Style.FILL(只填充不描邊)
// 類型3:Paint.Style.STROKE(只描邊不填充)
// 設置對齊方式
setTextAlign()
// LEFT:左對齊
// CENTER:居中對齊
// RIGHT:右對齊
//設置文本的下劃線
setUnderlineText(boolean underlineText)
//設置文本的刪除線
setStrikeThruText(boolean strikeThruText)
//設置文本粗體
setFakeBoldText(boolean fakeBoldText)
// 設置斜體
Paint.setTextSkewX(-0.5f);
// 設置文字陰影
Paint.setShadowLayer(5,5,5,Color.YELLOW);
}
// 步驟3:在構造函數中初始化
public CarsonView(Context context, AttributeSet attrs) {
super(context, attrs);
initPaint();
}
Style模式效果如下:
3.2 Path類
具體請看我寫的另外一篇文章:Path類的最全面詳解 - 自定義View應用系列
3.3 關閉硬件加速
- 在Android4.0的設備上,在打開硬件加速的情況下,使用自定義View可能會出現問題
具體問題可以看這里。
- 所以測試前,請先關閉硬件加速
- 具體關閉方式:在AndroidMenifest.xml的application節點添加
android:hardwareAccelerated="false"
4. Canvas的使用
4.1 對象創建 & 獲取
Canvas對象 & 獲取的方法有4個:
// 方法1
// 利用空構造方法直接創建對象
Canvas canvas = new Canvas();
// 方法2
// 通過傳入裝載畫布Bitmap對象創建Canvas對象
// CBitmap上存儲所有繪制在Canvas的信息
Canvas canvas = new Canvas(bitmap)
// 方法3
// 通過重寫View.onDraw()創建Canvas對象
// 在該方法里可以獲得這個View對應的Canvas對象
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//在這里獲取Canvas對象
}
// 方法4
// 在SurfaceView里畫圖時創建Canvas對象
SurfaceView surfaceView = new SurfaceView(this);
// 從SurfaceView的surfaceHolder里鎖定獲取Canvas
SurfaceHolder surfaceHolder = surfaceView.getHolder();
//獲取Canvas
Canvas c = surfaceHolder.lockCanvas();
// ...(進行Canvas操作)
// Canvas操作結束之后解鎖并執行Canvas
surfaceHolder.unlockCanvasAndPost(c);
官方推薦方法4來創建并獲取Canvas,原因:
- SurfaceView里有一條線程是專門用于畫圖,所以方法4的畫圖性能最好,并適用于高質量的、刷新頻率高的圖形
- 而方法3刷新頻率低于方法3,但系統花銷小,節省資源
4.2 繪制方法使用
- 利用Canvas類可繪畫出很多內容,如圖形、文字、線條等等;
- 對應使用的方法如下:
僅列出常用方法,更加詳細的方法可參考官方文檔 Canvas
下面我將逐個方法進行詳細講解
特別注意
Canvas具體使用時是在復寫的onDraw()里:
@Override
protected void onDraw(Canvas canvas){
super.onDraw(canvas);
// 對Canvas進行一系列設置
// 如畫圓、畫直線等等
canvas.drawColor(Color.BLUE);
// ...
}
}
具體為什么,請看我寫的自定義View原理系列文章:
(1)自定義View基礎 - 最易懂的自定義View原理系列
(2)自定義View Measure過程 - 最易懂的自定義View原理系列
(3)自定義View Layout過程 - 最易懂的自定義View原理系列
(4)自定義View Draw過程- 最易懂的自定義View原理系列
4.2.1 繪制顏色
- 作用:將顏色填充整個畫布,常用于繪制底色
- 具體使用
// 傳入一個Color類的常量參數來設置畫布顏色
// 繪制藍色
canvas.drawColor(Color.BLUE);
4.2.2 繪制基本圖形
a. 繪制點(drawPoint)
- 原理:在某個坐標處繪制點
可畫一個點或一組點(多個點)
- 具體使用
// 特別注意:需要用到畫筆Paint
// 所以之前記得創建畫筆
// 為了區分,這里使用了兩個不同顏色的畫筆
// 描繪一個點
// 在坐標(200,200)處
canvas.drawPoint(300, 300, mPaint1);
// 繪制一組點,坐標位置由float數組指定
// 此處畫了3個點,位置分別是:(600,500)、(600,600)、(600,700)
canvas.drawPoints(new float[]{
600,500,
600,600,
600,700
},mPaint2);
b. 繪制直線(drawLine)
- 原理:兩點(初始點 & 結束點)確定一條直線
- 具體使用:
// 畫一條直線
// 在坐標(100,200),(700,200)之間繪制一條直線
canvas.drawLine(100,200,700,200,mPaint1);
// 繪制一組線
// 在坐標(400,500),(500,500)之間繪制直線1
// 在坐標(400,600),(500,600)之間繪制直線2
canvas.drawLines(new float[]{
400,500,500,500,
400,600,500,600
},mPaint2);
}
c. 繪制矩形(drawRect)
- 原理:矩形的對角線頂點確定一個矩形
一般是采用左上角和右下角的兩個點的坐標。
- 具體使用
// 關于繪制矩形,Canvas提供了三種重載方法
// 方法1:直接傳入兩個頂點的坐標
// 兩個頂點坐標分別是:(100,100),(800,400)
canvas.drawRect(100,100,800,400,mPaint);
// 方法2:將兩個頂點坐標封裝為RectRectF
Rect rect = new Rect(100,100,800,400);
canvas.drawRect(rect,mPaint);
// 方法3:將兩個頂點坐標封裝為RectF
RectF rectF = new RectF(100,100,800,400);
canvas.drawRect(rectF,mPaint);
// 特別注意:Rect類和RectF類的區別
// 精度不同:Rect = int & RectF = float
// 三種方法畫出來的效果是一樣的。
d. 繪制圓角矩形
- 原理:矩形的對角線頂點確定一個矩形
類似于繪制矩形
- 具體使用
// 方法1:直接傳入兩個頂點的坐標
// API21時才可使用
// 第5、6個參數:rx、ry是圓角的參數,下面會詳細描述
canvas.drawRoundRect(100,100,800,400,30,30,mPaint);
// 方法2:使用RectF類
RectF rectF = new RectF(100,100,800,400);
canvas.drawRoundRect(rectF,30,30,mPaint);
- 與矩形相比,圓角矩形多了兩個參數rx 和 ry
- 圓角矩形的角是橢圓的圓弧,rx 和 ry實際上是橢圓的兩個半徑,如下圖:
- 特別注意:當 rx大于寬度的一半, ry大于高度一半 時,畫出來的為橢圓
實際上,在rx為寬度的一半,ry為高度的一半時,剛好是一個橢圓;但由于當rx大于寬度一半,ry大于高度一半時,無法計算出圓弧,所以drawRoundRect對大于該數值的參數進行了修正,凡是大于一半的參數均按照一半來處理
e. 繪制橢圓
- 原理:矩形的對角線頂點確定矩形,根據傳入矩形的長寬作為長軸和短軸畫橢圓
- 橢圓傳入的參數和矩形是一樣的;
- 繪制橢圓實際上是繪制一個矩形的內切圖形。
- 具體使用
// 方法1:使用RectF類
RectF rectF = new RectF(100,100,800,400);
canvas.drawOval(rectF,mPaint);
// 方法2:直接傳入與矩形相關的參數
canvas.drawOval(100,100,800,400,mPaint);
// 為了方便表示,畫一個和橢圓一樣參數的矩形
canvas.drawRect(100,100,800,400,mPaint);
f. 繪制圓
- 原理:圓心坐標+半徑決定圓
- 具體使用
// 參數說明:
// 1、2:圓心坐標
// 3:半徑
// 4:畫筆
// 繪制一個圓心坐標在(500,500),半徑為400 的圓。
canvas.drawCircle(500,500,400,mPaint);
g. 繪制圓弧
- 原理:通過圓弧角度的起始位置和掃過的角度確定圓弧
- 具體使用
// 繪制圓弧共有兩個方法
// 相比于繪制橢圓,繪制圓弧多了三個參數:
startAngle // 確定角度的起始位置
sweepAngle // 確定掃過的角度
useCenter // 是否使用中心(下面會詳細說明)
// 方法1
public void drawArc(@NonNull RectF oval, float startAngle, float sweepAngle, boolean useCenter, @NonNull Paint paint){}
// 方法2
public void drawArc(float left, float top, float right, float bottom, float startAngle,
float sweepAngle, boolean useCenter, @NonNull Paint paint) {}
為了理解第三個參數:useCenter
,看以下示例:
// 以下示例:繪制兩個起始角度為0度、掃過90度的圓弧
// 兩者的唯一區別就是是否使用了中心點
// 繪制圓弧1(無使用中心)
RectF rectF = new RectF(100, 100, 800,400);
// 繪制背景矩形
canvas.drawRect(rectF, mPaint1);
// 繪制圓弧
canvas.drawArc(rectF, 0, 90, false, mPaint2);
// 繪制圓弧2(使用中心)
RectF rectF2 = new RectF(100,600,800,900);
// 繪制背景矩形
canvas.drawRect(rectF2, mPaint1);
// 繪制圓弧
canvas.drawArc(rectF2,0,90,true,mPaint2);
從示例可以發現:
- 不使用中心點:圓弧的形狀 = (起、止點連線+圓弧)構成的面積
- 使用中心店:圓弧面積 = (起點、圓心連線 + 止點、圓心連線+圓弧)構成的面積
類似扇形
4.2.3 繪制文字
繪制文字分為三種應用場景:
- 情況1:指定文本開始的位置
- 即指定文本基線位置
- 基線x默認在字符串左側,基線y默認在字符串下方
- 情況2:指定每個文字的位置
- 情況3:指定路徑,并根據路徑繪制文字
下面分別細說:
文字的樣式(大小,顏色,字體等)具體由畫筆Paint控制,詳細請會看上面基礎的介紹
情況1:指定文本開始的位置
// 參數text:要繪制的文本
// 參數x,y:指定文本開始的位置(坐標)
// 參數paint:設置的畫筆屬性
public void drawText (String text, float x, float y, Paint paint)
// 實例
canvas.drawText("abcdefg",300,400,mPaint1);
// 僅繪制文本的一部分
// 參數start,end:指定繪制文本的位置
// 位置以下標標識,由0開始
public void drawText (String text, int start, int end, float x, float y, Paint paint)
public void drawText (CharSequence text, int start, int end, float x, float y, Paint paint)
// 對于字符數組char[]
// 截取文本使用起始位置(index)和長度(count)
public void drawText (char[] text, int index, int count, float x, float y, Paint paint)
// 實例:繪制從位置1-3的文本
canvas.drawText("abcdefg",1,4,300,400,mPaint1);
// 字符數組情況
// 字符數組(要繪制的內容)
char[] chars = "abcdefg".toCharArray();
// 參數為 (字符數組 起始坐標 截取長度 基線x 基線y 畫筆)
canvas.drawText(chars,1,3,200,500,textPaint);
// 效果同上
情況2:分別指定文本的位置
// 參數text:繪制的文本
// 參數pos:數組類型,存放每個字符的位置(坐標)
// 注意:必須指定所有字符位置
public void drawPosText (String text, float[] pos, Paint paint)
// 對于字符數組char[],可以截取部分文本進行繪制
// 截取文本使用起始位置(index)和長度(count)
public void drawPosText (char[] text, int index, int count, float[] pos, Paint paint)
// 特別注意:
// 1. 在字符數量較多時,使用會導致卡頓
// 2. 不支持emoji等特殊字符,不支持字形組合與分解
// 實例
canvas.drawPosText("abcde", new float[]{
100, 100, // 第一個字符位置
200, 200, // 第二個字符位置
300, 300, // ...
400, 400,
500, 500
}, mPaint1);
// 數組情況(繪制部分文本)
char[] chars = "abcdefg".toCharArray();
canvas.drawPosText(chars, 1, 3, new float[]{
300, 300, // 指定的第一個字符位置
400, 400, // 指定的第二個字符位置
500, 500, // 指定的第三個字符位置
}, mPaint1);
情況3:指定路徑,并根據路徑繪制文字
關于Path類的使用請看我寫的文章具體請看我寫的另外一篇文章:Path類的最全面詳解 - 自定義View應用系列
// 在路徑(540,750,640,450,840,600)寫上"在Path上寫的字:Carson_Ho"字樣
// 1.創建路徑對象
Path path = new Path();
// 2. 設置路徑軌跡
path.cubicTo(540, 750, 640, 450, 840, 600);
// 3. 畫路徑
canvas.drawPath(path,mPaint2);
// 4. 畫出在路徑上的字
canvas.drawTextOnPath("在Path上寫的字:Carson_Ho", path, 50, 0, mPaint2);
4.2.4 繪制圖片
繪制圖片分為:繪制矢量圖(drawPicture)和 繪制位圖(drawBitmap)
a. 繪制矢量圖(drawPicture)
- 作用:繪制矢量圖的內容,即繪制存儲在矢量圖里某個時刻Canvas繪制內容的操作
矢量圖(Picture)的作用:存儲(錄制)某個時刻Canvas繪制內容的操作
- 應用場景:繪制之前繪制過的內容
- 相比于再次調用各種繪圖API,使用Picture能節省操作 & 時間
- 如果不手動調用,錄制的內容不會顯示在屏幕上,只是存儲起來
特別注意:使用繪制矢量圖時前請關閉硬件加速,以免引起不必要的問題!
具體使用方法:
// 獲取寬度
Picture.getWidth ();
// 獲取高度
Picture.getHeight ()
// 開始錄制
// 即將Canvas中所有的繪制內容存儲到Picture中
// 返回一個Canvas
Picture.beginRecording(int width, int height)
// 結束錄制
Picture.endRecording ()
// 將Picture里的內容繪制到Canvas中
Picture.draw (Canvas canvas)
// 還有兩種方法可以將Picture里的內容繪制到Canvas中
// 方法2:Canvas.drawPicture()
// 方法3:將Picture包裝成為PictureDrawable,使用PictureDrawable的draw方法繪制。
// 下面會詳細介紹
一般使用的具體步驟
// 步驟1:創建Picture對象
Picture mPicture = new Picture();
// 步驟2:開始錄制
mPicture.beginRecording(int width, int height);
// 步驟3:繪制內容 or 操作Canvas
canvas.drawCircle(500,500,400,mPaint);
...(一系列操作)
// 步驟4:結束錄制
mPicture.endRecording ();
步驟5:某個時刻將存儲在Picture的繪制內容繪制出來
mPicture.draw (Canvas canvas);
下面我將用一個實例去表示如何去使用:
- 實例介紹
將坐標系移動到(450,650);繪制一個圓,將上述Canvas操作錄制下來,并在某個時刻重新繪制出來。
步驟1:創建Picture對象
Picture mPicture = new Picture();
步驟2:開始錄制
Canvas recordingCanvas = mPicture.beginRecording(500, 500);
// 注:要創建Canvas對象來接收beginRecording()返回的Canvas對象
步驟3:繪制內容 or 操作Canvas
// 位移
// 將坐標系的原點移動到(450,650)
recordingCanvas.translate(450,650);
// 記得先創建一個畫筆
Paint paint = new Paint();
paint.setColor(Color.BLUE);
paint.setStyle(Paint.Style.FILL);
// 繪制一個圓
// 圓心為(0,0),半徑為100
recordingCanvas.drawCircle(0,0,100,paint);
步驟4:結束錄制
mPicture.endRecording();
步驟5:將存儲在Picture的繪制內容繪制出來
有三種方法:
- Picture.draw (Canvas canvas)
- Canvas.drawPicture()
- PictureDrawable.draw()
將Picture包裝成為PictureDrawable
主要區別如下:
方法1:Picture提供的draw()
// 在復寫的onDraw()里
@Override
protected void onDraw(Canvas canvas){
super.onDraw(canvas);
// 將錄制的內容顯示在當前畫布里
mPicture.draw(canvas);
// 注:此方法繪制后可能會影響Canvas狀態,不建議使用
}
方法2:Canvas提供的drawPicture()
不會影響Canvas狀態
// 提供了三種方法
// 方法1
public void drawPicture (Picture picture)
// 方法2
// Rect dst代表顯示的區域
// 若區域小于圖形,繪制的內容根據選區進行縮放
public void drawPicture (Picture picture, Rect dst)
// 方法3
public void drawPicture (Picture picture, RectF dst)
@Override
protected void onDraw(Canvas canvas){
super.onDraw(canvas);
// 實例1:將錄制的內容顯示(區域剛好布滿圖形)
canvas.drawPicture(mPicture, new RectF(0, 0, mPicture.getWidth(), mPicture.getHeight()));
// 實例2:將錄制的內容顯示在當前畫布上(區域小于圖形)
canvas.drawPicture(mPicture, new RectF(0, 0, mPicture.getWidth(), 200));
方法3:使用PictureDrawable的draw方法繪制
將Picture包裝成為PictureDrawable
@Override
protected void onDraw(Canvas canvas){
super.onDraw(canvas);
// 將錄制的內容顯示出來
// 將Picture包裝成為Drawable
PictureDrawable drawable = new PictureDrawable(mPicture);
// 設置在畫布上的繪制區域(類似drawPicture (Picture picture, Rect dst)的Rect dst參數)
// 每次都從Picture的左上角開始繪制
// 并非根據該區域進行縮放,也不是剪裁Picture。
// 實例1:將錄制的內容顯示(區域剛好布滿圖形)
drawable.setBounds(0, 0,mPicture.getWidth(), mPicture.getHeight());
// 繪制
drawable.draw(canvas);
// 實例2:將錄制的內容顯示在當前畫布上(區域小于圖形)
drawable.setBounds(0, 0,250, mPicture.getHeight());
b. 繪制位圖(drawBitmap)
- 作用:將已有的圖片轉換為位圖(Bitmap),最后再繪制到Canvas上
位圖,即平時我們使用的圖片資源
獲取Bitmap對象的方式
要繪制Bitmap,就要先獲取一個Bitmap對象,具體獲取方式如下:
特別注意:繪制位圖(Bitmap)是讀取已有的圖片轉換為Bitmap,最后再繪制到Canvas。
所以:
- 對于第1種方式:排除
- 對于第2種方式:雖然滿足需求,但一般不推薦使用
具體請自行了解關于Drawble的內容
- 對于第3種方式:滿足需求,下面會著重講解
通過BitmapFactory獲取Bitmap (從不同位置獲取):
// 共3個位置:資源文件、內存卡、網絡
// 位置1:資源文件(drawable/mipmap/raw)
Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(),R.raw.bitmap);
// 位置2:資源文件(assets)
Bitmap bitmap=null;
try {
InputStream is = mContext.getAssets().open("bitmap.png");
bitmap = BitmapFactory.decodeStream(is);
is.close();
} catch (IOException e) {
e.printStackTrace();
}
// 位置3:內存卡文件
Bitmap bitmap = BitmapFactory.decodeFile("/sdcard/bitmap.png");
// 位置4:網絡文件:
// 省略了獲取網絡輸入流的代碼
Bitmap bitmap = BitmapFactory.decodeStream(is);
is.close();
繪制Bitmap
繪制Bitmap共有四種方法:
// 方法1
public void drawBitmap (Bitmap bitmap, Matrix matrix, Paint paint)
// 方法2
public void drawBitmap (Bitmap bitmap, float left, float top, Paint paint)
// 方法3
public void drawBitmap (Bitmap bitmap, Rect src, Rect dst, Paint paint)
// 方法4
public void drawBitmap (Bitmap bitmap, Rect src, RectF dst, Paint paint)
// 下面詳細說
方法1
public void drawBitmap (Bitmap bitmap, Matrix matrix, Paint paint)
// 后兩個參數matrix, paint是在繪制時對圖片進行一些改變
// 后面會專門說matrix
// 如果只是將圖片內容繪制出來只需將傳入新建的matrix, paint對象即可:
canvas.drawBitmap(bitmap,new Matrix(),new Paint());
// 記得選取一種獲取Bitmap的方式
// 注:圖片左上角位置默認為坐標原點。
方法2
// 參數 left、top指定了圖片左上角的坐標(距離坐標原點的距離):
public void drawBitmap (Bitmap bitmap, float left, float top, Paint paint)
canvas.drawBitmap(bitmap,300,400,new Paint());
方法3
public void drawBitmap (Bitmap bitmap, Rect src, Rect dst, Paint paint)
// 參數(src,dst) = 兩個矩形區域
// Rect src:指定需要繪制圖片的區域(即要繪制圖片的哪一部分)
// Rect dst 或RectF dst:指定圖片在屏幕上顯示(繪制)的區域
// 下面我將用實例來說明
// 實例
// 指定圖片繪制區域
// 僅繪制圖片的二分之一
Rect src = new Rect(0,0,bitmap.getWidth()/2,bitmap.getHeight());
// 指定圖片在屏幕上顯示的區域
Rect dst = new Rect(100,100,250,250);
// 繪制圖片
canvas.drawBitmap(bitmap,src,dst,null);
// 下面我們一步步分析:
特別注意的是:如果src規定繪制圖片的區域大于dst指定顯示的區域的話,那么圖片的大小會被縮放。
方法3的應用場景:
-
便于素材管理
當我需要畫很多個圖時,如果1張圖=1個素材的話,那么管理起來很不方便;如果素材都放在一個圖,那么按需繪制會便于管理
Paste_Image.png 實現動態效果
動態效果 = 逐漸繪制圖形部分,如下:
在繪制時,只需要一個資源文件,然后逐漸描繪就可以
繪制過程如下:
4.2.5 繪制路徑
// 通過傳入具體路徑Path對象 & 畫筆
canvas.drawPath(mPath, mPaint)
關于Path類的使用,具體請看我寫的另外一篇文章:Path類的最全面詳解 - 自定義View應用系列
4.2.6 畫布操作
- 作用:改變畫布的性質
改變之后,任何的后續操作都會受到影響
A. 畫布變換
a. 平移(translate)
- 作用:移動畫布(實際上是移動坐標系,如下圖)
- 具體使用
// 將畫布原點向右移200px,向下移100px
canvas.translate(200, 100)
// 注:位移是基于當前位置移動,而不是每次都是基于屏幕左上角的(0,0)點移動
b. 縮放(scale)
- 作用:放大 / 縮小 畫布的倍數
- 具體使用:
// 共有兩個方法
// 方法1
// 以(px,py)為中心,在x方向縮放sx倍,在y方向縮放sy倍
// 縮放中心默認為(0,0)
public final void scale(float sx, float sy)
// 方法2
// 比方法1多了兩個參數(px,py),用于控制縮放中心位置
// 縮放中心為(px,py)
public final void scale (float sx, float sy, float px, float py)
我將用下面的例子說明縮放的使用和縮放中心的意義。
// 實例:畫兩個對比圖
// 相同:都有兩個矩形,第1個= 正常大小,第2個 = 放大1.5倍
// 不同點:第1個縮放中心在(0,0),第2個在(px,py)
// 第一個圖
// 設置矩形大小
RectF rect = new RectF(0,-200,200,0);
// 繪制矩形(藍色)
canvas.drawRect(rect, mPaint1);
// 將畫布放大到1.5倍
// 不移動縮放中心,即縮放中心默認為(0,0)
canvas.scale(1.5f, 1.5f);
// 繪制放大1.5倍后的藍色矩形(紅色)
canvas.drawRect(rect,mPaint2);
// 第二個圖
// 設置矩形大小
RectF rect = new RectF(0,-200,200,0);
// 繪制矩形(藍色)
canvas.drawRect(rect, mPaint1);
// 將畫布放大到1.5倍,并將縮放中心移動到(100,0)
canvas.scale(1.5f, 1.5f, 100,0);
// 繪制放大1.5倍后的藍色矩形(紅色)
canvas.drawRect(rect,mPaint2);
// 縮放的本質是:把形狀先畫到畫布,然后再縮小/放大。所以當放大倍數很大時,會有明顯鋸齒
當縮放倍數為負數時,會先進行縮放,然后根據不同情況進行圖形翻轉:
(設縮放倍數為(a,b),旋轉中心為(px,py)):
- a<0,b>0:以px為軸翻轉
- a>0,b<0:以py為軸翻轉
- a<0,b<0:以旋轉中心翻轉
具體如下圖:(縮放倍數為1.5,旋轉中心為(0,0)為例)
c. 旋轉(rotate)
注意:角度增加方向為順時針(區別于數學坐標系)
// 方法1
// 以原點(0,0)為中心旋轉 degrees 度
public final void rotate(float degrees)
// 以原點(0,0)為中心旋轉 90 度
canvas.rotate(90);
// 方法2
// 以(px,py)點為中心旋轉degrees度
public final void rotate(float degrees, float px, float py)
// 以(30,50)為中心旋轉 90 度
canvas.rotate(90,30,50);
d. 錯切(skew)
- 作用:將畫布在x方向傾斜a角度、在y方向傾斜b角度
- 具體使用:
// 參數 sx = tan a ,sx>0時表示向X正方向傾斜(即向左)
// 參數 sy = tan b ,sy>0時表示向Y正方向傾斜(即向下)
public void skew(float sx, float sy)
// 實例
// 為了方便觀察,我將坐標系移到屏幕中央
canvas.translate(300, 500);
// 初始矩形
canvas.drawRect(20, 20, 400, 200, mPaint2);
// 向X正方向傾斜45度
canvas.skew(1f, 0);
canvas.drawRect(20, 20, 400, 200, mPaint1);
//向X負方向傾斜45度
canvas.skew(-1f, 0);
canvas.drawRect(20, 20, 400, 200, mPaint1);
// 向Y正方向傾斜45度
canvas.skew(0, 1f);
canvas.drawRect(20, 20, 400, 200, mPaint1);
// 向Y負方向傾斜45度
canvas.skew(0, -1f);
canvas.drawRect(20, 20, 400, 200, mPaint1);
B. 畫布裁剪
即從畫布上裁剪一塊區域,之后僅能編輯該區域
特別注意:其余的區域只是不能編輯,但是并沒有消失,如下圖
裁剪共分為:裁剪路徑、裁剪矩形、裁剪區域
// 裁剪路徑
// 方法1
public boolean clipPath(@NonNull Path path)
// 方法2
public boolean clipPath(@NonNull Path path, @NonNull Region.Op op)
// 裁剪矩形
// 方法1
public boolean clipRect(int left, int top, int right, int bottom)
// 方法2
public boolean clipRect(float left, float top, float right, float bottom)
// 方法3
public boolean clipRect(float left, float top, float right, float bottom,
@NonNull Region.Op op)
// 裁剪區域
// 方法1
public boolean clipRegion(@NonNull Region region)
// 方法2
public boolean clipRegion(@NonNull Region region, @NonNull Region.Op op)
這里特別說明一下參數Region.Op op
作用:在剪下多個區域下來的情況,當這些區域有重疊的時候,這個參數決定重疊部分該如何處理,多次裁剪之后究竟獲得了哪個區域,有以下幾種參數:
以三個參數為例講解:
Region.Op.DIFFERENCE:顯示第一次裁剪與第二次裁剪不重疊的區域
// 為了方便觀察,我將坐標系移到屏幕中央
canvas.translate(300, 500);
//原來畫布設置為灰色
canvas.drawColor(Color.GRAY);
//第一次裁剪
canvas.clipRect(0, 0, 600, 600);
//將第一次裁剪后的區域設置為紅色
canvas.drawColor(Color.RED);
//第二次裁剪,并顯示第一次裁剪與第二次裁剪不重疊的區域
canvas.clipRect(0, 200, 600, 400, Region.Op.DIFFERENCE);
//將第一次裁剪與第二次裁剪不重疊的區域設置為黑色
canvas.drawColor(Color.BLACK);
Region.Op.REPLACE:顯示第二次裁剪的區域
//原來畫布設置為灰色)
canvas.drawColor(Color.GRAY);
//第一次裁剪
canvas.clipRect(0, 0, 600, 600);
//將第一次裁剪后的區域設置為紅色
canvas.drawColor(Color.RED);
//第二次裁剪,并顯示第二次裁剪的區域
canvas.clipRect(0, 200, 600, 400, Region.Op.REPLACE);
//將第二次裁剪的區域設置為藍色
canvas.drawColor(Color.BLUE);
Region.Op.INTERSECT:顯示第二次與第一次的重疊區域
//原來畫布設置為灰色)
canvas.drawColor(Color.GRAY);
//第一次裁剪
canvas.clipRect(0, 0, 600, 600);
//將第一次裁剪后的區域設置為紅色
canvas.drawColor(Color.RED);
//第二次裁剪,并顯示第一次裁剪與第二次裁剪重疊的區域
canvas.clipRect(-100, 200, 600, 400, Region.Op.INTERSECT);
//將第一次裁剪與第二次裁剪重疊的區域設置為黑色
canvas.drawColor(Color.BLACK);
關于其他參數,較為簡單,此處不作過多展示。
C. 畫布快照
這里先理清幾個概念
- 畫布狀態:當前畫布經過的一系列操作
-
狀態棧:存放畫布狀態和圖層的棧(后進先出)
狀態棧 - 畫布的構成:由多個圖層構成,如下圖
- 在畫布上操作 = 在圖層上操作
- 如無設置,繪制操作和畫布操作是默認在默認圖層上進行
- 在通常情況下,使用默認圖層就可滿足需求;若需要繪制復雜的內容(如地圖),則需使用更多的圖層
- 最終顯示的結果 = 所有圖層疊在一起的效果
a. 保存當前畫布狀態(save)
- 作用:保存畫布狀態(即保存畫布的一系列操作)
- 應用場景:畫布的操作是不可逆的,而且會影響后續的步驟,假如需要回到之前畫布的狀態去進行下一次操作,就需要對畫布的狀態進行保存和回滾
// 方法1:
// 保存全部狀態
public int save ()
// 方法2:
// 根據saveFlags參數保存一部分狀態
// 使用該參數可以只保存一部分狀態,更加靈活
public int save (int saveFlags)
// saveFlags參數說明:
// 1.ALL_SAVE_FLAG(默認):保存全部狀態
// 2. CLIP_SAVE_FLAG:保存剪輯區
// 3. CLIP_TO_LAYER_SAVE_FLAG:剪裁區作為圖層保存
// 4. FULL_COLOR_LAYER_SAVE_FLAG:保存圖層的全部色彩通道
// 5. HAS_ALPHA_LAYER_SAVE_FLAG:保存圖層的alpha(不透明度)通道
// 6. MATRIX_SAVE_FLAG:保存Matrix信息(translate, rotate, scale, skew)
// 每調用一次save(),都會在棧頂添加一條狀態信息(入棧)
b. 保存某個圖層狀態(saveLayer)
- 作用:新建一個圖層,并放入特定的棧中
- 具體使用
使用起來非常復雜,因為圖層之間疊加會導致計算量成倍增長,營盡量避免使用。
// 無圖層alpha(不透明度)通道
public int saveLayer (RectF bounds, Paint paint)
public int saveLayer (RectF bounds, Paint paint, int saveFlags)
public int saveLayer (float left, float top, float right, float bottom, Paint paint)
public int saveLayer (float left, float top, float right, float bottom, Paint paint, int saveFlags)
// 有圖層alpha(不透明度)通道
public int saveLayerAlpha (RectF bounds, int alpha)
public int saveLayerAlpha (RectF bounds, int alpha, int saveFlags)
public int saveLayerAlpha (float left, float top, float right, float bottom, int alpha)
public int saveLayerAlpha (float left, float top, float right, float bottom, int alpha, int saveFlags)
c. 回滾上一次保存的狀態(restore)
- 作用:恢復上一次保存的畫布狀態
- 具體使用
// 采取狀態棧的形式。即從棧頂取出一個狀態進行恢復。
canvas.restore();
d. 回滾指定保存的狀態(restoreToCount)
- 作用:恢復指定狀態;將指定位置以及以上所有狀態出棧
- 具體使用:
canvas.restoreToCount(3) ;
// 彈出 3、4、5的狀態,并恢復第3次保存的畫布狀態
e. 獲取保存的次數(getSaveCount)
- 作用:獲取保存過圖層的次數
即獲取狀態棧中保存狀態的數量
canvas.getSaveCount();
// 以上面棧為例,則返回5
// 注:即使彈出所有的狀態,返回值依舊為1,代表默認狀態。(返回值最小為1)
總結
對于畫布狀態的保存和回滾的套路,一般如下:
// 步驟1:保存當前狀態
// 把Canvas的當前狀態信息入棧
save();
// 步驟2:對畫布進行各種操作(旋轉、平移Blabla)
...
// 步驟3:回滾到之前的畫布狀態
// 把棧里面的信息出棧,取代當前的Canvas信息
restore();
5. 總結
通過閱讀本文,相信你已經全面了解Canvas類的使用。Carson帶你學Android自定義View文章系列:
Carson帶你學Android:自定義View基礎
Carson帶你學Android:一文梳理自定義View工作流程
Carson帶你學Android:自定義View繪制準備-DecorView創建
Carson帶你學Android:自定義View Measure過程
Carson帶你學Android:自定義View Layout過程
Carson帶你學Android:自定義View Draw過程
Carson帶你學Android:手把手教你寫一個完整的自定義View
Carson帶你學Android:Canvas類全面解析
Carson帶你學Android:Path類全面解析
歡迎關注Carson_Ho的簡書
不定期分享關于安卓開發的干貨,追求短、平、快,但卻不缺深度。