1. 程式人生 > >安卓自定義View進階-Path之基本操作

安卓自定義View進階-Path之基本操作

在上一篇Canvas之圖片文字中我們瞭解瞭如何使用Canvas中繪製圖片文字,結合前幾篇文章,Canvas的基本操作已經差不多完結了,然而Canvas不僅僅具有這些基本的操作,還可以更加炫酷,本次會了解到path(路徑)這個Canvas中的神器,有了這個神器,就能創造出更多炫(zhuang)酷(B)的東東了。


一.Path常用方法表

為了相容性(偷懶) 本表格中去除了部分API21(即安卓版本5.0)以上才新增的方法。

作用 相關方法 備註
移動起點 moveTo 移動下一次操作的起點位置
設定終點 setLastPoint 重置當前path中最後一個點位置,如果在繪製之前呼叫,效果和moveTo相同
連線直線 lineTo 新增上一個點到當前點之間的直線到Path
閉合路徑 close 連線第一個點連線到最後一個點,形成一個閉合區域
新增內容 addRect, addRoundRect, addOval, addCircle, addPath, addArc, arcTo 新增(矩形, 圓角矩形, 橢圓, 圓, 路徑, 圓弧) 到當前Path (注意addArc和arcTo的區別)
是否為空 isEmpty 判斷Path是否為空
是否為矩形 isRect 判斷path是否是一個矩形
替換路徑 set 用新的路徑替換到當前路徑所有內容
偏移路徑 offset 對當前路徑之前的操作進行偏移(不會影響之後的操作)
貝塞爾曲線 quadTo, cubicTo 分別為二次和三次貝塞爾曲線的方法
rXxx方法 rMoveTo, rLineTo, rQuadTo, rCubicTo 不帶r的方法是基於原點的座標系(偏移量), rXxx方法是基於當前點座標系(偏移量)
填充模式 setFillType, getFillType, isInverseFillType, toggleInverseFillType 設定,獲取,判斷和切換填充模式
提示方法 incReserve 提示Path還有多少個點等待加入(這個方法貌似會讓Path優化儲存結構)
布林操作(API19) op 對兩個Path進行布林運算(即取交集、並集等操作)
計算邊界 computeBounds 計算Path的邊界
重置路徑 reset, rewind 清除Path中的內容
reset不保留內部資料結構,但會保留FillType.
rewind會保留內部的資料結構,但不保留FillType
矩陣操作 transform 矩陣變換

二.Path詳解

請關閉硬體加速,以免引起不必要的問題!
請關閉硬體加速,以免引起不必要的問題!
請關閉硬體加速,以免引起不必要的問題!

在AndroidMenifest檔案中application節點下添上 android:hardwareAccelerated=”false”以關閉整個應用的硬體加速。
更多請參考這裡:Android的硬體加速及可能導致的問題

Path作用

本次特地開了一篇詳細講解Path,為什麼要單獨摘出來呢,這是因為Path在2D繪圖中是一個很重要的東西。

在前面我們講解的所有繪製都是簡單圖形(如 矩形 圓 圓弧等),而對於那些複雜一點的圖形則沒法去繪製(如繪製一個心形 正多邊形 五角星等),而使用Path不僅能夠繪製簡單圖形,也可以繪製這些比較複雜的圖形。另外,根據路徑繪製文字和剪裁畫布都會用到Path。

關於Path的作用先簡單地說這麼多,具體的我們接下來慢慢研究。

Path含義

官方介紹:

The Path class encapsulates compound (multiple contour) geometric paths consisting of straight line segments, quadratic curves, and cubic curves. It can be drawn with canvas.drawPath(path, paint), either filled or stroked (based on the paint’s Style), or it can be used for clipping or to draw text on a path.

嗯,沒錯依舊是拿來裝逼的,如果你看不懂的話,不用擔心,其實並沒有什麼卵用。

通俗解釋(sloop個人版):

Path封裝了由直線和曲線(二次,三次貝塞爾曲線)構成的幾何路徑。你能用Canvas中的drawPath來把這條路徑畫出來(同樣支援Paint的不同繪製模式),也可以用於剪裁畫布和根據路徑繪製文字。我們有時會用Path來描述一個影象的輪廓,所以也會稱為輪廓線(輪廓線僅是Path的一種使用方法,兩者並不等價)

另外路徑有開放和封閉的區別。

影象 名稱 備註
封閉路徑 首尾相接形成了一個封閉區域
開放路徑 沒有首位相接形成封閉區域

這個是我隨便畫的,僅為展示一下區別,請無視我靈魂畫師一般的繪圖水準。

與Path相關的還有一些比較神奇的概念,不過暫且不說,等接下來需要用到的時候再詳細說明。

Path使用方法詳解

前面扯了一大堆概念性的東西。接下來就開始實戰了,請諸位看官准備好瓜子、花生、爆米花,坐下來慢慢觀看。

第1組: moveTo、 setLastPoint、 lineTo 和 close

由於Path的有些知識點無法單獨來講,所以本次採取了一次講一組方法。

按照慣例,先建立畫筆:

Paint mPaint = new Paint();             // 建立畫筆
mPaint.setColor(Color.BLACK);           // 畫筆顏色 - 黑色
mPaint.setStyle(Paint.Style.STROKE);    // 填充模式 - 描邊
mPaint.setStrokeWidth(10);              // 邊框寬度 - 10

lineTo:

方法預覽:

public void lineTo (float x, float y)

首先講解的的LineTo,為啥先講解這個呢?

是因為moveTo、 setLastPoint、 close都無法直接看到效果,藉助有具現化效果的lineTo才能讓這些方法現出原形。

lineTo很簡單,只有一個方法,作用也很容易理解,line嘛,顧名思義就是一條線。

俗話(數學書上)說,兩點確定一條直線,但是看引數明顯只給了一個點的座標吧(這不按常理出牌啊)。

再仔細一看,這個lineTo除了line外還有一個to呢,to翻譯過來就是“至”,到某個地方的意思,lineTo難道是指從某個點到引數座標點之間連一條線?

沒錯,你猜對了,但是這某個點又是哪裡呢?

前面我們提到過Path可以用來描述一個影象的輪廓,影象的輪廓通常都是用一條線構成的,所以這裡的某個點就是上次操作結束的點,如果沒有進行過操作則預設點為座標原點。

那麼我們就來試一下:

canvas.translate(mWidth / 2, mHeight / 2);  // 移動座標系到螢幕中心(寬高資料在onSizeChanged中獲取)

Path path = new Path();                     // 建立Path

path.lineTo(200, 200);                      // lineTo
path.lineTo(200,0);

canvas.drawPath(path, mPaint);              // 繪製Path

在示例中我們呼叫了兩次lineTo,第一次由於之前沒有過操作,所以預設點就是座標原點O,結果就是座標原點O到A(200,200)之間連直線(用藍色圈1標註)。

第二次lineTo的時候,由於上次的結束位置是A(200,200),所以就是A(200,200)到B(200,0)之間的連線(用藍色圈2標註)。

moveTo 和 setLastPoint:

方法預覽:

// moveTo
public void moveTo (float x, float y)

// setLastPoint
public void setLastPoint (float dx, float dy)

這兩個方法雖然在作用上有相似之處,但實際上卻是完全不同的兩個東東,具體參照下表:

方法名 簡介 是否影響之前的操作 是否影響之後操作
moveTo 移動下一次操作的起點位置
setLastPoint 設定之前操作的最後一個點位置

廢話不多說,直接上程式碼:

canvas.translate(mWidth / 2, mHeight / 2);  // 移動座標系到螢幕中心

Path path = new Path();                     // 建立Path

path.lineTo(200, 200);                      // lineTo

path.moveTo(200,100);                       // moveTo

path.lineTo(200,0);                         // lineTo

canvas.drawPath(path, mPaint);              // 繪製Path

這個和上面演示lineTo的方法類似,只不過在兩個lineTo之間添加了一個moveTo。

moveTo只改變下次操作的起點,在執行完第一次LineTo的時候,本來的預設點位置是A(200,200),但是moveTo將其改變成為了C(200,100),所以在第二次呼叫lineTo的時候就是連線C(200,100) 到 B(200,0) 之間的直線(用藍色圈2標註)。

下面是setLastPoint的示例:

canvas.translate(mWidth / 2, mHeight / 2);  // 移動座標系到螢幕中心

Path path = new Path();                     // 建立Path

path.lineTo(200, 200);                      // lineTo

path.setLastPoint(200,100);                 // setLastPoint

path.lineTo(200,0);                         // lineTo

canvas.drawPath(path, mPaint);              // 繪製Path

setLastPoint是重置上一次操作的最後一個點,在執行完第一次的lineTo的時候,最後一個點是A(200,200),而setLastPoint更改最後一個點為C(200,100),所以在實際執行的時候,第一次的lineTo就不是從原點O到A(200,200)的連線了,而變成了從原點O到C(200,100)之間的連線了。

在執行完第一次lineTo和setLastPoint後,最後一個點的位置是C(200,100),所以在第二次呼叫lineTo的時候就是C(200,100) 到 B(200,0) 之間的連線(用藍色圈2標註)。

close

方法預覽:

public void close ()

close方法用於連線當前最後一個點和最初的一個點(如果兩個點不重合的話),最終形成一個封閉的圖形。

canvas.translate(mWidth / 2, mHeight / 2);  // 移動座標系到螢幕中心

Path path = new Path();                     // 建立Path

path.lineTo(200, 200);                      // lineTo

path.lineTo(200,0);                         // lineTo

path.close();                               // close

canvas.drawPath(path, mPaint);              // 繪製Path

很明顯,兩個lineTo分別代表第1和第2條線,而close在此處的作用就算連線了B(200,0)點和原點O之間的第3條線,使之形成一個封閉的圖形。

注意:close的作用是封閉路徑,與連線當前最後一個點和第一個點並不等價。如果連線了最後一個點和第一個點仍然無法形成封閉圖形,則close什麼 也不做。

第2組: addXxx與arcTo

這次內容主要是在Path中新增基本圖形,重點區分addArc與arcTo。

第一類(基本形狀)

方法預覽:

// 第一類(基本形狀)

// 圓形
public void addCircle (float x, float y, float radius, Path.Direction dir)
// 橢圓
public void addOval (RectF oval, Path.Direction dir)
// 矩形
public void addRect (float left, float top, float right, float bottom, Path.Direction dir)
public void addRect (RectF rect, Path.Direction dir)
// 圓角矩形
public void addRoundRect (RectF rect, float[] radii, Path.Direction dir)
public void addRoundRect (RectF rect, float rx, float ry, Path.Direction dir)

這一類就是在path中新增一個基本形狀,基本形狀部分和前面所講的繪製基本形狀並無太大差別,詳情參考Canvas之繪製圖形, 本次只將其中不同的部分摘出來詳細講解一下。

仔細觀察一下第一類是方法,無一例外,在最後都有一個 Path.Direction,這是一個什麼神奇的東東?

Direction的意思是 方向,趨勢。 點進去看一下會發現Direction是一個列舉(Enum)型別,裡面只有兩個列舉常量,如下:

型別 解釋 翻譯
CW clockwise 順時針
CCW counter-clockwise 逆時針

瞬間懵逼,我只是想新增一個基本的形狀啊,搞什麼順時針和逆時針, (╯‵□′)╯︵┻━┻

稍安勿躁,┬─┬ ノ( ‘ - ‘ノ) {擺好擺好) 既然存在肯定是有用的,先偷偷劇透一下這個順時針和逆時針的作用。

序號 作用
1 在新增圖形時確定閉合順序(各個點的記錄順序)
2 對圖形的渲染結果有影響(是判斷圖形渲染的重要條件)

這個先劇透這麼多,至於對閉合順序有啥影響,圖形的渲染等問題等請慢慢看下去

咱們先研究確定閉合順序的問題,新增一個矩形試試看:

canvas.translate(mWidth / 2, mHeight / 2);  // 移動座標系到螢幕中心

Path path = new Path();

path.addRect(-200,-200,200,200, Path.Direction.CW);

canvas.drawPath(path,mPaint);

將上面程式碼的CW改為CCW再執行一次。接下來就是見證奇蹟的時刻,兩次執行結果一模一樣,有木有很神奇!

(╯°Д°)╯︵ ┻━┻(再TM掀一次) 坑人也不帶這樣的啊,一毛一樣要它幹嘛。

其實啊,這個東東是自帶隱身技能的,想要讓它現出原形,就要用到咱們剛剛學到的setLastPoint(重置當前最後一個點的位置)。

canvas.translate(mWidth / 2, mHeight / 2);  // 移動座標系到螢幕中心

Path path = new Path();

path.addRect(-200,-200,200,200, Path.Direction.CW);

path.setLastPoint(-300,300);                // <-- 重置最後一個點的位置

canvas.drawPath(path,mPaint);

可以明顯看到,圖形發生了奇怪的變化。為何會如此呢?

我們先分析一下,繪製一個矩形(僅繪製邊線),實際上只需要進行四次lineTo操作就行了,也就是說,只需要知道4個點的座標,然後使用moveTo到第一個點,之後依次lineTo就行了(從上面的測試可以看出,在實際繪製中也確實是這麼幹的)。

可是為什麼要這麼做呢?確定一個矩形最少需要兩個點(對角線的兩個點),根據這兩個點的座標直接算出四條邊然後畫出來不就行了,幹嘛還要先計算出四個點座標,之後再連直線呢?

這個就要涉及一些path的儲存問題了,前面在path中的定義中說過,Path是封裝了由直線和曲線(二次,三次貝塞爾曲線)構成的幾何路徑。其中曲線部分用的是貝塞爾曲線,稍後再講。 然而除了曲線部分就只剩下直線了,對於直線的儲存最簡單的就是記錄座標點,然後直接連線各個點就行了。雖然記錄矩形只需要兩個點,但是如果只用兩個點來記錄一個矩形的話,就要額外增加一個標誌位來記錄這是一個矩形,顯然對於儲存和解析都是很不划算的事情,將矩形轉換為直線,為的就是儲存記錄方便。

扯了這麼多,該回歸正題了,就是我們的順時針和逆時針在這裡是幹啥的?

圖形在實際記錄中就是記錄各個的點,對於一個圖形來說肯定有多個點,既然有這麼多的點,肯定就需要一個先後順序,這裡順時針和逆時針就是用來確定記錄這些點的順序的。

對於上面這個矩形來說,我們採用的是順時針(CW),所以記錄的點的順序就是 A -> B -> C -> D. 最後一個點就是D,我們這裡使用setLastPoint改變最後一個點的位置實際上是改變了D的位置。

理解了上面的原理之後,設想如果我們將順時針改為逆時針(CCW),則記錄點的順序應該就是 A -> D -> C -> B, 再使用setLastPoint則改變的是B的位置,我們試試看結果和我們的猜想是否一致:

canvas.translate(mWidth / 2, mHeight / 2);  // 移動座標系到螢幕中心

Path path = new Path();

path.addRect(-200,-200,200,200, Path.Direction.CCW);

path.setLastPoint(-300,300);                // <-- 重置最後一個點的位置

canvas.drawPath(path,mPaint);

通過驗證發現,發現結果和我們猜想的一樣,但是還有一個潛藏的問題不曉得大家可否注意到。我們用兩個點的座標確定了一個矩形,矩形起始點(A)就是我們指定的第一個點的座標。

需要注意的是,交換座標點的順序可能就會影響到某些繪製內容哦,例如上面的例子,你可以嘗試交換兩個座標點,或者指定另外兩個點來作為引數,雖然指定的是同一個矩形,但實際繪製出來是不同的哦。

引數中點的順序很重要!
引數中點的順序很重要!
引數中點的順序很重要!

重要的話說三遍,本次是用矩形作為例子的,其他的幾個圖形基本上都包含了曲線,詳情參見後續的貝塞爾曲線部分。

關於順時針和逆時針對圖形填充結果的影響請參考 Path之完結篇,雖然只講了一個Path,但也是內容頗多,放進一篇中就太長了,請見諒。

第二類(Path)

方法預覽:

// 第二類(Path)
// path
public void addPath (Path src)
public void addPath (Path src, float dx, float dy)
public void addPath (Path src, Matrix matrix)

這個相對比較簡單,也很容易理解,就是將兩個Path合併成為一個。

第三個方法是將src新增到當前path之前先使用Matrix進行變換。

第二個方法比第一個方法多出來的兩個引數是將src進行了位移之後再新增進當前path中。

示例:

canvas.translate(mWidth / 2, mHeight / 2);  // 移動座標系到螢幕中心
canvas.scale(1,-1);                         // <-- 注意 翻轉y座標軸

Path path = new Path();
Path src = new Path();

path.addRect(-200,-200,200,200, Path.Direction.CW);
src.addCircle(0,0,100, Path.Direction.CW);

path.addPath(src,0,200);

mPaint.setColor(Color.BLACK);           // 繪製合併後的路徑
canvas.drawPath(path,mPaint);

首先我們新建地方兩個Path(矩形和圓形)中心都是座標原點,我們在將包含圓形的path新增到包含矩形的path之前將其進行移動了一段距離,最終繪製出來的效果就如上面所示。

第三類(addArc與arcTo)

方法預覽:

// 第三類(addArc與arcTo)

// addArc
public void addArc (RectF oval, float startAngle, float sweepAngle)
// arcTo
public void arcTo (RectF oval, float startAngle, float sweepAngle)
public void arcTo (RectF oval, float startAngle
            
           

相關推薦

no