目錄
一、前言
二、如何畫圖
1、繪圖座標系
2、檢視座標系
3、小結
三、Canvas的剪刀手API
1、clipPath
2、clipOutPath
3、clipPath
四、實戰
五、寫在最後
一、前言
從今天開始我們聊一聊 Canvas 的API,因為Canvas的API較多,所以我們分為幾次分享,首先分享的是裁剪型別的API使用。話不多說,先上實戰圖。
老夫的少女心
原始碼地址文末會給出,瞭解原理才能更好地駕馭。
二、如何畫圖
分享前,我們先來聊聊,在我們生活中如何繪製一張如下的圖。
我們需要兩樣東西來繪製:- 一張紙(Android 中的 canvas):用來承載我們繪製的內容。
- 一支筆(Android 中的 paint):負責繪製內容的軌跡。
有了這兩樣,我們就能在現實的場景中開始繪製了。
1、繪圖座標系
但在 Android 的體系中,我們所謂的 “筆Paint” 和 “紙Canvas” 都是由App持有的,所以我們在繪製時就出現一個問題:我們怎麼“告訴”App,確定我們想要繪製圖形的落筆點?當然需要一個座標系來進行交流。
而這個 座標系 便是我們經常所說的 繪圖座標系。初始狀態下,Canvas的左上角為原點,如下圖的藍色點所示。此時我們想畫圖中的紅點,就非常的容易,只需要“告訴” App 在座標(200,500)處畫一個紅點,這就達到了畫圖的效果了。 所以我們可以明確的一點是 我們所有的畫圖座標都是根據原點進行確定。
所以我們可以移動原點,達到整體座標點的移動,例如還是畫剛才的紅點,我們可以先將原點水平移動100,垂直移動400。然後在進行繪製,這時紅點的座標就變為(100,100),具體如下圖所示。 經過上面的簡單講述,我們可以知道,繪圖過程中,我們的繪圖座標永遠是跟隨當前的原點,而畫布的原點可以進行移動。2、檢視座標系
理論上 Canvas 這張紙是沒有邊界的,但是我們的手機螢幕是有界的。我們可以理解為我們透過一個方形的洞(手機螢幕)看一張巨畫(Canvas)。
而這裡我們就又存在一個問題了,因為剛才的移動,我們是移動的原點,也就是說我們的畫布是靜止不動的,只是落筆點一直在變動,這就導致我們繪製的圖對於使用者來說是看不全的,所以我們需要進行移動 方形的洞 來檢視這幅畫。
舉個例子,我們要檢視最開始所說的畫,可以通過移動 Screen框來檢視這幅畫,而這裡又出現了一個座標系,這一座標系則為 檢視座標系,通過 scrollerTo
和 scrollerBy
進行移動該Screen框,正數則往正半軸,負數則往負半軸。
3、小結
自定義控制元件中存在兩個座標系需要明確,用一句話總結如下:
- 繪圖座標系:決定我們的繪製的座標
- 檢視座標系:決定我們所看到的畫布範圍
三、Canvas的剪刀手API
Canvas 中以 clip開頭 的公有方法,用於裁剪畫布的內容。 我們抽取比較好玩的引數型別為Path的方法來分享,其餘的都可以一一對映進來。
1、clipPath
public boolean clipPath(@NonNull Path path)
複製程式碼
描述: 只留下 path內 的畫布區域,而處於path範圍之外的則不顯示。
舉個例子:
我們先準備好一個心形的路徑Path,然後呼叫 clipPath
從畫布中將此路徑內的區域 “裁剪” 下來,最後為了我們觀察,使用drawColor
“染”上酒紅色。
// 第一步:建立 心形路徑 mPath
....省略,具體請移步github
// 第二步:從畫布 canvas 裁剪下心形路徑之內的區域
canvas.clipPath(mPath);
// 第三步:塗酒紅色
canvas.drawColor(mBgColor);
複製程式碼
如果想了解如何繪製心形軌跡,請移步小盆友的另一篇博文:自帶美感的貝塞爾曲線原理與實戰
效果圖
此型別的方法還有以下這幾個,但他們的裁剪範圍均為矩形public boolean clipRect(float left, float top, float right, float bottom)
public boolean clipRect(int left, int top, int right, int bottom)
public boolean clipRect(@NonNull Rect rect)
public boolean clipRect(@NonNull RectF rect)
複製程式碼
2、clipOutPath
public boolean clipOutPath(@NonNull Path path)
複製程式碼
描述: 只留下 path外 的畫布區域,而處於path範圍之內的則不顯示。(與clipPath的作用範圍正好相反)
值得注意的是,該方法只能在API26版本以上呼叫。 低版本我們使用下一小節介紹的方法
舉個例子:
我們先準備好一個心形的路徑Path,然後呼叫 clipOutPath
從畫布中將此路徑之外的區域 “裁剪” 下來,最後為了我們觀察,使用 drawColor
“染”上酒紅色。
// 第一步:建立 心形路徑 mPath
....省略,具體請移步github
// 第二步:從畫布 canvas 裁剪下心形路徑之外的區域
canvas.clipOutPath(mPath);
// 第三步:塗酒紅色
canvas.drawColor(mBgColor);
複製程式碼
效果圖
此型別的方法還有以下這幾個,但他們的裁剪範圍均為矩形
public boolean clipOutRect(float left, float top, float right, float bottom)
public boolean clipOutRect(int left, int top, int right, int bottom)
public boolean clipOutRect(@NonNull Rect rect)
public boolean clipOutRect(@NonNull RectF rect)
複製程式碼
3、clipPath
public boolean clipPath(@NonNull Path path, @NonNull Region.Op op)
複製程式碼
描述: 在畫布上進行使用 path 路徑進行操作,至於其作用由 op 決定。
描述比較抽象,我們通過例子來體會。但在上例子前,我們需要先了解下 Region.Op
這個列舉型別,具體內容程式碼如下
public enum Op {
// A: 為我們先裁剪的路徑
// B: 為我們後裁剪的路徑
// A形狀中不同於B的部分顯示出來
DIFFERENCE(0),
// A和B交集的形狀
INTERSECT(1),
// A和B的全集
UNION(2),
// A和B的全集形狀,去除交集形狀之後的部分
XOR(3),
// B形狀中不同於A的部分顯示出來
REVERSE_DIFFERENCE(4),
// 只顯示B的形狀
REPLACE(5);
// ...省略不相關程式碼
}
複製程式碼
通過原始碼可以知道共有六種型別。值得一提的有以下兩點:
1)clipOutPath
方法中使用的型別就是 DIFFERENCE
,換而言之,我們可以使用以下程式碼代替,解決在API26 以下無法使用的問題clipOutPath
方法的問題
clipPath(mPath, Region.Op.DIFFERENCE)
複製程式碼
2)clipPath
方法中使用的型別就是 INTERSECT
,換而言之,我們可以使用以下程式碼代替
clipPath(mPath, Region.Op.INTERSECT)
複製程式碼
舉些例子:
接下來我們一個個講解這六種型別,兩次裁剪比較能體現出 Region.Op
引數的作用,所以我們接下來的例子需要使用兩個路徑:
1、心形路徑 (下列例子中的 A)
2、圓路徑(下列例子中的 B)(1)DIFFERENCE
描述: A形狀中不同於B的部分顯示出來
效果圖: 紅色即為最終裁剪留下區域
(2)INTERSECT
描述: A和B交集的形狀
效果圖: 紅色即為最終裁剪留下區域
(3)UNION
描述: A和B的全集
效果圖: 紅色即為最終裁剪留下區域
(4)XOR
描述: A和B的全集形狀,去除交集形狀之後的部分
效果圖: 紅色即為最終裁剪留下區域
(5)REVERSE_DIFFERENCE
描述: B形狀中不同於A的部分顯示出來
效果圖: 紅色即為最終裁剪留下區域
(6)REPLACE
描述: 只顯示B的形狀
效果圖: 紅色即為最終裁剪留下區域
此型別的方法還有以下這幾個,但他們的 裁剪範圍均為矩形public boolean clipRect(float left, float top, float right, float bottom,
@NonNull Region.Op op)
public boolean clipRect(@NonNull Rect rect, @NonNull Region.Op op)
public boolean clipRect(@NonNull RectF rect, @NonNull Region.Op op)
複製程式碼
四、實戰
上一小節我們已經瞭解了這幾些API的作用就是裁剪,這小節我們就把它使用起來。
老夫的少女心
效果圖
Github入口:傳送門
編碼思路
我們藉助下面這張小盆友手繪的思路圖(看看能不能達到一圖勝千言?)
這裡為了視覺效果易於講解,紅色即為我們demo中的粉色,藍色即為我們demo中青色,橘色就是最終的漸變色
第一步(綠色心形部分): 我們先在畫布裁剪下心形區域,這就奠定了最後呈現給使用者所看到的畫布區域為一個“心”。
第二步(紅色部分): 我們用將畫布染成紅色,然後在畫布的中心用藍色寫上 “猛猛的小盆友” ,最後使用圖中紅色框(即上邊是橫線,下邊是用貝塞爾曲線繪製的Path紅色區域)將畫布的上半部分裁剪下來,放置最終呈現的畫布中。
第三步(藍色部分): 與第二步正好相反,我們用將畫布染成藍色,然後在畫布的中心用紅色寫上 “猛猛的小盆友” ,最後使用圖中藍色框(即上邊是用貝塞爾曲線繪製,下邊是橫線的Path懶色區域)將畫布的下半部分裁剪下來,放置最終呈現的畫布中。
第四步: 經過前三步,我們的圖案已經形成了右邊的影象。我們開啟動畫,其實就是控制中間貝塞爾曲線的y軸座標,令其從底部上升至頂部,則呈現出了灌滿心形的動畫效果,所以我們可以通過讓畫布偏移一定的值達到該效果,同時讓貝塞爾曲線做水平的運動,有一種波動感。
核心程式碼
// 第一步
canvas.clipPath(mHeartPath);
// ======== 第二步start ==============
canvas.save();
// 第四步
canvas.translate(-mCurOffset, mCurPos);
canvas.clipPath(mTopPath);
mPaint.setColor(mTopBgColor);
mPaint.setStyle(Paint.Style.FILL);
canvas.drawPath(mTopPath, mPaint);
canvas.translate(mCurOffset, -mCurPos);
drawText(canvas, mBottomBgColor);
canvas.restore();
// ======== 第二步end ==============
// ======== 第三步start ==============
canvas.save();
// 第四步
canvas.translate(-mCurOffset, mCurPos);
canvas.clipPath(mBottomPath);
mPaint.setColor(ContextCompat.getColor(getContext(), R.color.canvas_light_blue_color));
mPaint.setStyle(Paint.Style.FILL);
canvas.drawPath(mBottomPath, mPaint);
canvas.translate(mCurOffset, -mCurPos);
drawText(canvas, mTopBgColor);
canvas.restore();
// ======== 第三步end ==============
複製程式碼
五、寫在最後
Canvas 中的API挺多,涉及的小知識也比較零碎,本來想在一篇文章中分享完所有的API,但寫的過於寬泛,糾結再三,小盆友最終還是選擇迴歸初心,按照自己的理解分享好每個知識點,將canvas的分享拆分為幾次。如果覺得文章對你有所啟發,請給我個贊吧,如果發現有那些欠妥的地方,請留言區與我討論,我們共同進步。
高階UI系列的Github地址:請進入傳送門,如果喜歡的話給我一個star吧?
歡迎加我微信,我們可以進行更多更有趣的交流