Canvas中的剪刀手講解與實戰——Android高階UI

猛猛的小盆友發表於2019-04-27

目錄
一、前言
二、如何畫圖
1、繪圖座標系
2、檢視座標系
3、小結
三、Canvas的剪刀手API
1、clipPath
2、clipOutPath
3、clipPath
四、實戰
五、寫在最後

一、前言

從今天開始我們聊一聊 Canvas 的API,因為Canvas的API較多,所以我們分為幾次分享,首先分享的是裁剪型別的API使用。話不多說,先上實戰圖。

老夫的少女心

Canvas中的剪刀手講解與實戰——Android高階UI

原始碼地址文末會給出,瞭解原理才能更好地駕馭。

二、如何畫圖

分享前,我們先來聊聊,在我們生活中如何繪製一張如下的圖。

Canvas中的剪刀手講解與實戰——Android高階UI
我們需要兩樣東西來繪製:

  1. 一張紙(Android 中的 canvas):用來承載我們繪製的內容。
  2. 一支筆(Android 中的 paint):負責繪製內容的軌跡。

有了這兩樣,我們就能在現實的場景中開始繪製了。

1、繪圖座標系

但在 Android 的體系中,我們所謂的 “筆Paint” 和 “紙Canvas” 都是由App持有的,所以我們在繪製時就出現一個問題:我們怎麼“告訴”App,確定我們想要繪製圖形的落筆點?當然需要一個座標系來進行交流。

而這個 座標系 便是我們經常所說的 繪圖座標系。初始狀態下,Canvas的左上角為原點,如下圖的藍色點所示。此時我們想畫圖中的紅點,就非常的容易,只需要“告訴” App 在座標(200,500)處畫一個紅點,這就達到了畫圖的效果了。 所以我們可以明確的一點是 我們所有的畫圖座標都是根據原點進行確定。

Canvas中的剪刀手講解與實戰——Android高階UI
所以我們可以移動原點,達到整體座標點的移動,例如還是畫剛才的紅點,我們可以先將原點水平移動100,垂直移動400。然後在進行繪製,這時紅點的座標就變為(100,100),具體如下圖所示。
Canvas中的剪刀手講解與實戰——Android高階UI
經過上面的簡單講述,我們可以知道,繪圖過程中,我們的繪圖座標永遠是跟隨當前的原點,而畫布的原點可以進行移動。

2、檢視座標系

理論上 Canvas 這張紙是沒有邊界的,但是我們的手機螢幕是有界的。我們可以理解為我們透過一個方形的洞(手機螢幕)看一張巨畫(Canvas)。

而這裡我們就又存在一個問題了,因為剛才的移動,我們是移動的原點,也就是說我們的畫布是靜止不動的,只是落筆點一直在變動,這就導致我們繪製的圖對於使用者來說是看不全的,所以我們需要進行移動 方形的洞 來檢視這幅畫。

舉個例子,我們要檢視最開始所說的畫,可以通過移動 Screen框來檢視這幅畫,而這裡又出現了一個座標系,這一座標系則為 檢視座標系,通過 scrollerToscrollerBy 進行移動該Screen框,正數則往正半軸,負數則往負半軸。

Canvas中的剪刀手講解與實戰——Android高階UI

3、小結

自定義控制元件中存在兩個座標系需要明確,用一句話總結如下:

  1. 繪圖座標系:決定我們的繪製的座標
  2. 檢視座標系:決定我們所看到的畫布範圍

三、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);
複製程式碼

如果想了解如何繪製心形軌跡,請移步小盆友的另一篇博文:自帶美感的貝塞爾曲線原理與實戰

效果圖

Canvas中的剪刀手講解與實戰——Android高階UI
此型別的方法還有以下這幾個,但他們的裁剪範圍均為矩形

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);
複製程式碼

效果圖

Canvas中的剪刀手講解與實戰——Android高階UI

此型別的方法還有以下這幾個,但他們的裁剪範圍均為矩形

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

Canvas中的剪刀手講解與實戰——Android高階UI
2、圓路徑(下列例子中的 B

Canvas中的剪刀手講解與實戰——Android高階UI

(1)DIFFERENCE

描述: A形狀中不同於B的部分顯示出來

效果圖: 紅色即為最終裁剪留下區域

Canvas中的剪刀手講解與實戰——Android高階UI

(2)INTERSECT

描述: A和B交集的形狀

效果圖: 紅色即為最終裁剪留下區域

Canvas中的剪刀手講解與實戰——Android高階UI

(3)UNION

描述: A和B的全集

效果圖: 紅色即為最終裁剪留下區域

Canvas中的剪刀手講解與實戰——Android高階UI

(4)XOR

描述: A和B的全集形狀,去除交集形狀之後的部分

效果圖: 紅色即為最終裁剪留下區域

Canvas中的剪刀手講解與實戰——Android高階UI

(5)REVERSE_DIFFERENCE

描述: B形狀中不同於A的部分顯示出來

效果圖: 紅色即為最終裁剪留下區域

Canvas中的剪刀手講解與實戰——Android高階UI

(6)REPLACE

描述: 只顯示B的形狀

效果圖: 紅色即為最終裁剪留下區域

Canvas中的剪刀手講解與實戰——Android高階UI
此型別的方法還有以下這幾個,但他們的 裁剪範圍均為矩形

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的作用就是裁剪,這小節我們就把它使用起來。

老夫的少女心

效果圖

Canvas中的剪刀手講解與實戰——Android高階UI

Github入口:傳送門

編碼思路

我們藉助下面這張小盆友手繪的思路圖(看看能不能達到一圖勝千言?)

Canvas中的剪刀手講解與實戰——Android高階UI

這裡為了視覺效果易於講解,紅色即為我們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吧?

歡迎加我微信,我們可以進行更多更有趣的交流

Canvas中的剪刀手講解與實戰——Android高階UI

相關文章