Canvas中的裁剪師講解與實戰——Android高級UI

一、前言

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

老夫的少女心

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

二、如何畫圖

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

我們需要兩樣東西來繪製:

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

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

1、繪圖座標系

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

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

所以我們可以移動原點,達到整體座標點的移動,例如還是畫剛纔的紅點,我們可以先將原點水平移動100,垂直移動400。然後在進行繪製,這時紅點的座標就變爲(100,100),具體如下圖所示。

經過上面的簡單講述,我們可以知道,繪圖過程中,我們的繪圖座標永遠是跟隨當前的原點,而畫布的原點可以進行移動。

2、視圖座標系

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

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

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

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);

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

效果圖

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

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吧?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章