一、Path常用方法表
為了兼容性(偷懶) 本表格中去除了部分API21(即安卓版本5.0)以上才添加的方法。
二、Path詳解
請(qǐng)關(guān)閉硬件加速,以免引起不必要的問題!
請(qǐng)關(guān)閉硬件加速,以免引起不必要的問題!
請(qǐng)關(guān)閉硬件加速,以免引起不必要的問題!
在AndroidMenifest文件中application節(jié)點(diǎn)下添上 android:hardwareAccelerated=”false”以關(guān)閉整個(gè)應(yīng)用的硬件加速。
Path作用
本次特地開了一篇詳細(xì)講解Path,為什么要單獨(dú)摘出來呢,這是因?yàn)镻ath在2D繪圖中是一個(gè)很重要的東西。
在前面我們講解的所有繪制都是簡單圖形(如 矩形 圓 圓弧等),而對(duì)于那些復(fù)雜一點(diǎn)的圖形則沒法去繪制(如繪制一個(gè)心形 正多邊形 五角星等),而使用Path不僅能夠繪制簡單圖形,也可以繪制這些比較復(fù)雜的圖形。另外,根據(jù)路徑繪制文本和剪裁畫布都會(huì)用到Path。
Path含義
Path封裝了由直線和曲線(二次,三次貝塞爾曲線)構(gòu)成的幾何路徑。你能用Canvas中的drawPath來把這條路徑畫出來(同樣支持Paint的不同繪制模式),也可以用于剪裁畫布和根據(jù)路徑繪制文字。我們有時(shí)會(huì)用Path來描述一個(gè)圖像的輪廓,所以也會(huì)稱為輪廓線(輪廓線僅是Path的一種使用方法,兩者并不等價(jià))
另外路徑有開放和封閉的區(qū)別。
Path使用方法詳解
第1組: moveTo、 setLastPoint、 lineTo 和 close
由于Path的有些知識(shí)點(diǎn)無法單獨(dú)來講,所以本次采取了一次講一組方法。
按照慣例,先創(chuàng)建畫筆:
Paint mPaint = new Paint(); // 創(chuàng)建畫筆
mPaint.setColor(Color.BLACK); // 畫筆顏色 - 黑色
mPaint.setStyle(Paint.Style.STROKE); // 填充模式 - 描邊
mPaint.setStrokeWidth(10); // 邊框?qū)挾?- 10
lineTo:
方法預(yù)覽:
public void lineTo (float x, float y)
首先講解的的LineTo,為啥先講解這個(gè)呢?
是因?yàn)閙oveTo、 setLastPoint、 close都無法直接看到效果,借助有具現(xiàn)化效果的lineTo才能讓這些方法現(xiàn)出原形。
lineTo很簡單,只有一個(gè)方法,作用也很容易理解,line嘛,顧名思義就是一條線。
俗話(數(shù)學(xué)書上)說,兩點(diǎn)確定一條直線,但是看參數(shù)明顯只給了一個(gè)點(diǎn)的坐標(biāo)吧(這不按常理出牌啊)。
再仔細(xì)一看,這個(gè)lineTo除了line外還有一個(gè)to呢,to翻譯過來就是“至”,到某個(gè)地方的意思,lineTo難道是指從某個(gè)點(diǎn)到參數(shù)坐標(biāo)點(diǎn)之間連一條線?
沒錯(cuò),你猜對(duì)了,但是這某個(gè)點(diǎn)又是哪里呢?
前面我們提到過Path可以用來描述一個(gè)圖像的輪廓,圖像的輪廓通常都是用一條線構(gòu)成的,所以這里的某個(gè)點(diǎn)就是上次操作結(jié)束的點(diǎn),如果沒有進(jìn)行過操作則默認(rèn)點(diǎn)為坐標(biāo)原點(diǎn)。
那么我們就來試一下:
canvas.translate(mWidth / 2, mHeight / 2); // 移動(dòng)坐標(biāo)系到屏幕中心(寬高數(shù)據(jù)在onSizeChanged中獲取)
Path path = new Path(); // 創(chuàng)建Path
path.lineTo(200, 200); // lineTo
path.lineTo(200,0);
canvas.drawPath(path, mPaint); // 繪制Path
在示例中我們調(diào)用了兩次lineTo,第一次由于之前沒有過操作,所以默認(rèn)點(diǎn)就是坐標(biāo)原點(diǎn)O,結(jié)果就是坐標(biāo)原點(diǎn)O到A(200,200)之間連直線(用藍(lán)色圈1標(biāo)注)。
第二次lineTo的時(shí)候,由于上次的結(jié)束位置是A(200,200),所以就是A(200,200)到B(200,0)之間的連線(用藍(lán)色圈2標(biāo)注)。
moveTo 和 setLastPoint:
方法預(yù)覽:
// moveTo
public void moveTo (float x, float y)
// setLastPoint
public void setLastPoint (float dx, float dy)
這兩個(gè)方法雖然在作用上有相似之處,但實(shí)際上卻是完全不同的兩個(gè)東東,具體參照下表:
廢話不多說,直接上代碼:
canvas.translate(mWidth / 2, mHeight / 2); // 移動(dòng)坐標(biāo)系到屏幕中心
Path path = new Path(); // 創(chuàng)建Path
path.lineTo(200, 200); // lineTo
path.moveTo(200,100); // moveTo
path.lineTo(200,0); // lineTo
canvas.drawPath(path, mPaint); // 繪制Path
這個(gè)和上面演示lineTo的方法類似,只不過在兩個(gè)lineTo之間添加了一個(gè)moveTo。
moveTo只改變下次操作的起點(diǎn),在執(zhí)行完第一次LineTo的時(shí)候,本來的默認(rèn)點(diǎn)位置是A(200,200),但是moveTo將其改變成為了C(200,100),所以在第二次調(diào)用lineTo的時(shí)候就是連接C(200,100) 到 B(200,0) 之間的直線(用藍(lán)色圈2標(biāo)注)。
下面是setLastPoint的示例:
canvas.translate(mWidth / 2, mHeight / 2); // 移動(dòng)坐標(biāo)系到屏幕中心
Path path = new Path(); // 創(chuàng)建Path
path.lineTo(200, 200); // lineTo
path.setLastPoint(200,100); // setLastPoint
path.lineTo(200,0); // lineTo
canvas.drawPath(path, mPaint); // 繪制Path
setLastPoint是重置上一次操作的最后一個(gè)點(diǎn),在執(zhí)行完第一次的lineTo的時(shí)候,最后一個(gè)點(diǎn)是A(200,200),而setLastPoint更改最后一個(gè)點(diǎn)為C(200,100),所以在實(shí)際執(zhí)行的時(shí)候,第一次的lineTo就不是從原點(diǎn)O到A(200,200)的連線了,而變成了從原點(diǎn)O到C(200,100)之間的連線了。
在執(zhí)行完第一次lineTo和setLastPoint后,最后一個(gè)點(diǎn)的位置是C(200,100),所以在第二次調(diào)用lineTo的時(shí)候就是C(200,100) 到 B(200,0) 之間的連線(用藍(lán)色圈2標(biāo)注)。
close
方法預(yù)覽:
public void close ()
close方法用于連接當(dāng)前最后一個(gè)點(diǎn)和最初的一個(gè)點(diǎn)(如果兩個(gè)點(diǎn)不重合的話),最終形成一個(gè)封閉的圖形。
canvas.translate(mWidth / 2, mHeight / 2); // 移動(dòng)坐標(biāo)系到屏幕中心
Path path = new Path(); // 創(chuàng)建Path
path.lineTo(200, 200); // lineTo
path.lineTo(200,0); // lineTo
path.close(); // close
canvas.drawPath(path, mPaint); // 繪制Path
很明顯,兩個(gè)lineTo分別代表第1和第2條線,而close在此處的作用就算連接了B(200,0)點(diǎn)和原點(diǎn)O之間的第3條線,使之形成一個(gè)封閉的圖形。
注意:close的作用是封閉路徑,與連接當(dāng)前最后一個(gè)點(diǎn)和第一個(gè)點(diǎn)并不等價(jià)。如果連接了最后一個(gè)點(diǎn)和第一個(gè)點(diǎn)仍然無法形成封閉圖形,則close什么 也不做。
第2組: addXxx與arcTo
這次內(nèi)容主要是在Path中添加基本圖形,重點(diǎn)區(qū)分addArc與arcTo。
第一類(基本形狀)
方法預(yù)覽:
// 第一類(基本形狀)
// 圓形
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)
仔細(xì)觀察一下第一類是方法,無一例外,在最后都有一個(gè) Path.Direction,這是一個(gè)什么神奇的東東?
Direction的意思是 方向,趨勢。 點(diǎn)進(jìn)去看一下會(huì)發(fā)現(xiàn)Direction是一個(gè)枚舉(Enum)類型,里面只有兩個(gè)枚舉常量,如下:
先偷偷劇透一下這個(gè)順時(shí)針和逆時(shí)針的作用。
這個(gè)先劇透這么多,至于對(duì)閉合順序有啥影響,圖形的渲染等問題等請(qǐng)慢慢看下去
咱們先研究確定閉合順序的問題,添加一個(gè)矩形試試看:
canvas.translate(mWidth / 2, mHeight / 2); // 移動(dòng)坐標(biāo)系到屏幕中心
Path path = new Path();
path.addRect(-200,-200,200,200, Path.Direction.CW);
canvas.drawPath(path,mPaint);
將上面代碼的CW改為CCW再運(yùn)行一次。接下來就是見證奇跡的時(shí)刻,兩次運(yùn)行結(jié)果一模一樣,有木有很神奇!(神奇?zhèn)€毛啊??????????)
其實(shí)啊,這個(gè)東東是自帶隱身技能的,想要讓它現(xiàn)出原形,就要用到咱們剛剛學(xué)到的setLastPoint(重置當(dāng)前最后一個(gè)點(diǎn)的位置)。
canvas.translate(mWidth / 2, mHeight / 2); // 移動(dòng)坐標(biāo)系到屏幕中心
Path path = new Path();
path.addRect(-200,-200,200,200, Path.Direction.CW);
path.setLastPoint(-300,300); // <-- 重置最后一個(gè)點(diǎn)的位置
canvas.drawPath(path,mPaint);
可以明顯看到,圖形發(fā)生了奇怪的變化。為何會(huì)如此呢?
我們先分析一下,繪制一個(gè)矩形(僅繪制邊線),實(shí)際上只需要進(jìn)行四次lineTo操作就行了,也就是說,只需要知道4個(gè)點(diǎn)的坐標(biāo),然后使用moveTo到第一個(gè)點(diǎn),之后依次lineTo就行了(從上面的測試可以看出,在實(shí)際繪制中也確實(shí)是這么干的)。
可是為什么要這么做呢?確定一個(gè)矩形最少需要兩個(gè)點(diǎn)(對(duì)角線的兩個(gè)點(diǎn)),根據(jù)這兩個(gè)點(diǎn)的坐標(biāo)直接算出四條邊然后畫出來不就行了,干嘛還要先計(jì)算出四個(gè)點(diǎn)坐標(biāo),之后再連直線呢?
這個(gè)就要涉及一些path的存儲(chǔ)問題了,前面在path中的定義中說過,Path是封裝了由直線和曲線(二次,三次貝塞爾曲線)構(gòu)成的幾何路徑。其中曲線部分用的是貝塞爾曲線,稍后再講。 然而除了曲線部分就只剩下直線了,對(duì)于直線的存儲(chǔ)最簡單的就是記錄坐標(biāo)點(diǎn),然后直接連接各個(gè)點(diǎn)就行了。雖然記錄矩形只需要兩個(gè)點(diǎn),但是如果只用兩個(gè)點(diǎn)來記錄一個(gè)矩形的話,就要額外增加一個(gè)標(biāo)志位來記錄這是一個(gè)矩形,顯然對(duì)于存儲(chǔ)和解析都是很不劃算的事情,將矩形轉(zhuǎn)換為直線,為的就是存儲(chǔ)記錄方便。
扯了這么多,該回歸正題了,就是我們的順時(shí)針和逆時(shí)針在這里是干啥的?
圖形在實(shí)際記錄中就是記錄各個(gè)的點(diǎn),對(duì)于一個(gè)圖形來說肯定有多個(gè)點(diǎn),既然有這么多的點(diǎn),肯定就需要一個(gè)先后順序,這里順時(shí)針和逆時(shí)針就是用來確定記錄這些點(diǎn)的順序的。
對(duì)于上面這個(gè)矩形來說,我們采用的是順時(shí)針(CW),所以記錄的點(diǎn)的順序就是 A -> B -> C -> D. 最后一個(gè)點(diǎn)就是D,我們這里使用setLastPoint改變最后一個(gè)點(diǎn)的位置實(shí)際上是改變了D的位置。
理解了上面的原理之后,設(shè)想如果我們將順時(shí)針改為逆時(shí)針(CCW),則記錄點(diǎn)的順序應(yīng)該就是 A -> D -> C -> B, 再使用setLastPoint則改變的是B的位置,我們試試看結(jié)果和我們的猜想是否一致:
canvas.translate(mWidth / 2, mHeight / 2); // 移動(dòng)坐標(biāo)系到屏幕中心
Path path = new Path();
path.addRect(-200,-200,200,200, Path.Direction.CCW);
path.setLastPoint(-300,300); // <-- 重置最后一個(gè)點(diǎn)的位置
canvas.drawPath(path,mPaint);
通過驗(yàn)證發(fā)現(xiàn),發(fā)現(xiàn)結(jié)果和我們猜想的一樣,但是還有一個(gè)潛藏的問題不曉得大家可否注意到。我們用兩個(gè)點(diǎn)的坐標(biāo)確定了一個(gè)矩形,矩形起始點(diǎn)(A)就是我們指定的第一個(gè)點(diǎn)的坐標(biāo)。
需要注意的是,交換坐標(biāo)點(diǎn)的順序可能就會(huì)影響到某些繪制內(nèi)容哦,例如上面的例子,你可以嘗試交換兩個(gè)坐標(biāo)點(diǎn),或者指定另外兩個(gè)點(diǎn)來作為參數(shù),雖然指定的是同一個(gè)矩形,但實(shí)際繪制出來是不同的哦。
參數(shù)中點(diǎn)的順序很重要!
參數(shù)中點(diǎn)的順序很重要!
參數(shù)中點(diǎn)的順序很重要!
重要的話說三遍,本次是用矩形作為例子的,其他的幾個(gè)圖形基本上都包含了曲線,詳情參見后續(xù)的貝塞爾曲線部分。
重要的話說三遍,本次是用矩形作為例子的,其他的幾個(gè)圖形基本上都包含了曲線,詳情參見后續(xù)的貝塞爾曲線部分。
第二類(Path)
方法預(yù)覽:
// 第二類(Path)
// path
public void addPath (Path src)
public void addPath (Path src, float dx, float dy)
public void addPath (Path src, Matrix matrix)
這個(gè)相對(duì)比較簡單,也很容易理解,就是將兩個(gè)Path合并成為一個(gè)。
第三個(gè)方法是將src添加到當(dāng)前path之前先使用Matrix進(jìn)行變換。
第二個(gè)方法比第一個(gè)方法多出來的兩個(gè)參數(shù)是將src進(jìn)行了位移之后再添加進(jìn)當(dāng)前path中。
示例:
canvas.translate(mWidth / 2, mHeight / 2); // 移動(dòng)坐標(biāo)系到屏幕中心
canvas.scale(1,-1); // <-- 注意 翻轉(zhuǎn)y坐標(biāo)軸
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);
首先我們新建地方兩個(gè)Path(矩形和圓形)中心都是坐標(biāo)原點(diǎn),我們在將包含圓形的path添加到包含矩形的path之前將其進(jìn)行移動(dòng)了一段距離,最終繪制出來的效果就如上面所示。
第三類(addArc與arcTo)
方法預(yù)覽:
// 第三類(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, float sweepAngle, boolean forceMoveTo)
參數(shù)表:
PS: sweepAngle取值范圍是 [-360, 360),不包括360,當(dāng) >= 360 或者 < -360 時(shí)將不會(huì)繪制任何內(nèi)容, 對(duì)于360,你可以用一個(gè)接近的值替代,例如: 359.99。
從名字就可以看出,這兩個(gè)方法都是與圓弧相關(guān)的,作用都是添加一個(gè)圓弧到path中,但既然存在兩個(gè)方法,兩者之間肯定是有區(qū)別的:
可以看到addArc有1個(gè)方法(實(shí)際上是兩個(gè)的,但另一個(gè)重載方法是API21添加的), 而arcTo有2個(gè)方法,其中一個(gè)最后多了一個(gè)布爾類型的變量forceMoveTo。
forceMoveTo是什么作用呢?
這個(gè)變量意思為“是否強(qiáng)制使用moveTo”,也就是說,是否使用moveTo將變量移動(dòng)到圓弧的起點(diǎn)位移,也就意味著:
示例(addArc):
canvas.translate(mWidth / 2, mHeight / 2); // 移動(dòng)坐標(biāo)系到屏幕中心
canvas.scale(1,-1); // <-- 注意 翻轉(zhuǎn)y坐標(biāo)軸
Path path = new Path();
path.lineTo(100,100);
RectF oval = new RectF(0,0,300,300);
path.addArc(oval,0,270);
// path.arcTo(oval,0,270,true); // <-- 和上面一句作用等價(jià)
canvas.drawPath(path,mPaint);
示例(arcTo):
canvas.translate(mWidth / 2, mHeight / 2); // 移動(dòng)坐標(biāo)系到屏幕中心
canvas.scale(1,-1); // <-- 注意 翻轉(zhuǎn)y坐標(biāo)軸
Path path = new Path();
path.lineTo(100,100);
RectF oval = new RectF(0,0,300,300);
path.arcTo(oval,0,270);
// path.arcTo(oval,0,270,false); // <-- 和上面一句作用等價(jià)
canvas.drawPath(path,mPaint);
第3組:isEmpty、 isRect、isConvex、 set 和 offset
這一組比較簡單,稍微說一下就可以了。
isEmpty
方法預(yù)覽:
public boolean isEmpty ()
判斷path中是否包含內(nèi)容。
Path path = new Path();
Log.e("1",path.isEmpty()+"");
path.lineTo(100,100);
Log.e("2",path.isEmpty()+"");
log輸出結(jié)果:
com.sloop.canvas E/1: true
com.sloop.canvas E/2: false
isRect
方法預(yù)覽:
public boolean isRect (RectF rect)
判斷path是否是一個(gè)矩形,如果是一個(gè)矩形的話,會(huì)將矩形的信息存放進(jìn)參數(shù)rect中。
path.lineTo(0,400);
path.lineTo(400,400);
path.lineTo(400,0);
path.lineTo(0,0);
RectF rect = new RectF();
boolean b = path.isRect(rect);
Log.e("Rect","isRect:"+b+"| left:"+rect.left+"| top:"+rect.top+"| right:"+rect.right+"| bottom:"+rect.bottom);
log 輸出結(jié)果:
com.sloop.canvas E/Rect: isRect:true| left:0.0| top:0.0| right:400.0| bottom:400.0
set
方法預(yù)覽:
public void set (Path src)
將新的path賦值到現(xiàn)有path。
canvas.translate(mWidth / 2, mHeight / 2); // 移動(dòng)坐標(biāo)系到屏幕中心
canvas.scale(1,-1); // <-- 注意 翻轉(zhuǎn)y坐標(biāo)軸
Path path = new Path(); // path添加一個(gè)矩形
path.addRect(-200,-200,200,200, Path.Direction.CW);
Path src = new Path(); // src添加一個(gè)圓
src.addCircle(0,0,100, Path.Direction.CW);
path.set(src); // 大致相當(dāng)于 path = src;
canvas.drawPath(path,mPaint);
offset
方法預(yù)覽:
public void offset (float dx, float dy)
public void offset (float dx, float dy, Path dst)
這個(gè)的作用也很簡單,就是對(duì)path進(jìn)行一段平移,它和Canvas中的translate作用很像,但Canvas作用于整個(gè)畫布,而path的offset只作用于當(dāng)前path。
但是第二個(gè)方法最后怎么會(huì)有一個(gè)path作為參數(shù)?
其實(shí)第二個(gè)方法中最后的參數(shù)das是存儲(chǔ)平移后的path的。
canvas.translate(mWidth / 2, mHeight / 2); // 移動(dòng)坐標(biāo)系到屏幕中心
canvas.scale(1,-1); // <-- 注意 翻轉(zhuǎn)y坐標(biāo)軸
Path path = new Path(); // path中添加一個(gè)圓形(圓心在坐標(biāo)原點(diǎn))
path.addCircle(0,0,100, Path.Direction.CW);
Path dst = new Path(); // dst中添加一個(gè)矩形
dst.addRect(-200,-200,200,200, Path.Direction.CW);
path.offset(300,0,dst); // 平移
canvas.drawPath(path,mPaint); // 繪制path
mPaint.setColor(Color.BLUE); // 更改畫筆顏色
canvas.drawPath(dst,mPaint); // 繪制dst
從運(yùn)行效果圖可以看出,雖然我們在dst中添加了一個(gè)矩形,但是并沒有表現(xiàn)出來,所以,當(dāng)dst中存在內(nèi)容時(shí),dst中原有的內(nèi)容會(huì)被清空,而存放平移后的path。
三、總結(jié)
本想一篇把path寫完,但是萬萬沒想到居然扯了這么多。本篇中講解的是直線部分和一些常用方法,下一篇將著重講解貝塞爾曲線和自相交圖形渲染等相關(guān)問題,敬請(qǐng)期待哦。
學(xué)完本篇之后又解鎖了新的境界,可以看看這位大神的文章 Android雷達(dá)圖(蜘蛛網(wǎng)圖)繪制