Android自定義控件學(xué)習(xí)筆記(四)

自定義控件系列的讀書(shū)筆記,整理自下列資料,不代表博主個(gè)人觀點(diǎn) :GcsSloop/AndroidNote


六、Path基本操作

6.1 Path常用方法表

不包括 API21以上才添加的方法

作用 相關(guān)方法 備注
移動(dòng)起點(diǎn) moveTo 移動(dòng)下一次操作的起點(diǎn)位置
設(shè)置終點(diǎn) setLastPoint 重置當(dāng)前path中最后一個(gè)點(diǎn)位置,如果在繪制之前調(diào)用,效果和moveTo相同
連接直線 lineTo 添加上一個(gè)點(diǎn)到當(dāng)前點(diǎn)之間的直線到Path
閉合路徑 close 連接第一個(gè)點(diǎn)連接到最后一個(gè)點(diǎn),形成一個(gè)閉合區(qū)域
添加內(nèi)容 addRect, addRoundRect, addOval, addCircle, addPath, addArc, arcTo 添加(矩形, 圓角矩形, 橢圓, 圓, 路徑, 圓弧) 到當(dāng)前Path (注意addArc和arcTo的區(qū)別)
是否為空 isEmpty 判斷Path是否為空
是否為矩形 isRect 判斷path是否是一個(gè)矩形
替換路徑 set 用新的路徑替換到當(dāng)前路徑所有內(nèi)容
偏移路徑 offset 對(duì)當(dāng)前路徑之前的操作進(jìn)行偏移(不會(huì)影響之后的操作)
貝塞爾曲線 quadTo, cubicTo 分別為二次和三次貝塞爾曲線的方法
rXxx方法 rMoveTo, rLineTo, rQuadTo, rCubicTo 不帶r的方法是基于原點(diǎn)的坐標(biāo)系, rXxx方法是基于當(dāng)前點(diǎn)坐標(biāo)系
填充模式 setFillType, getFillType, isInverseFillType, toggleInverseFillType 設(shè)置,獲取,判斷和切換填充模式
提示方法 incReserve 提示Path還有多少個(gè)點(diǎn)等待加入(這個(gè)方法貌似會(huì)讓Path優(yōu)化存儲(chǔ)結(jié)構(gòu))
布爾操作(API19) op 對(duì)兩個(gè)Path進(jìn)行布爾運(yùn)算(即取交集、并集等操作)
計(jì)算邊界 computeBounds 計(jì)算Path的邊界
重置路徑 reset, rewind 清除Path中的內(nèi)容,reset不保留內(nèi)部數(shù)據(jù)結(jié)構(gòu),但會(huì)保留FillType;rewind會(huì)保留內(nèi)部的數(shù)據(jù)結(jié)構(gòu),但不保留FillType
矩陣操作 transform 矩陣變換

6.2 Path詳解

請(qǐng)關(guān)閉硬件加速,以免引起不必要的問(wèn)題!

6.2.1 Path作用

Path在2D繪圖中是一個(gè)很重要的東西,使用Path不僅能夠繪制簡(jiǎn)單圖形,也可以繪制這些比較復(fù)雜的圖形。另外,根據(jù)路徑繪制文本和剪裁畫(huà)布都會(huì)用到Path。

6.2.2 Path含義

Path封裝了由直線和曲線(二次,三次貝塞爾曲線)構(gòu)成的幾何路徑。你能用Canvas中的drawPath來(lái)把這條路徑畫(huà)出來(lái)(同樣支持Paint的不同繪制模式),也可以用于剪裁畫(huà)布和根據(jù)路徑繪制文字。我們有時(shí)會(huì)用Path來(lái)描述一個(gè)圖像的輪廓,所以也會(huì)稱(chēng)為輪廓線(輪廓線僅是Path的一種使用方法,兩者并不等價(jià))

另外路徑有開(kāi)放和封閉的區(qū)別。

圖像 名稱(chēng) 備注
封閉路徑 首尾相接形成了一個(gè)封閉區(qū)域
開(kāi)放路徑 沒(méi)有首位相接形成封閉區(qū)域

6.2.3 Path使用方法詳解

(1)第1組:moveTo、 setLastPoint、 lineTo 和 close

先創(chuàng)建一個(gè)通用的畫(huà)筆:

Paint mPaint = new Paint();             // 創(chuàng)建畫(huà)筆
mPaint.setColor(Color.BLACK);           // 畫(huà)筆顏色 - 黑色
mPaint.setStyle(Paint.Style.STROKE);    // 填充模式 - 描邊
mPaint.setStrokeWidth(10);              // 邊框?qū)挾?- 10

lineTo:

public void lineTo (float x, float y)

lineTo是指從某個(gè)點(diǎn)到參數(shù)坐標(biāo)點(diǎn)之間連一條線,這里的某個(gè)點(diǎn)就是上次操作結(jié)束的點(diǎn),如果沒(méi)有進(jìn)行過(guò)操作則默認(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,第一次由于之前沒(méi)有過(guò)操作,所以默認(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:

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

// setLastPoint
public void setLastPoint (float dx, float dy)
方法名 簡(jiǎn)介 是否影響之前的操作 是否影響之后操作
moveTo 移動(dòng)下一次操作的起點(diǎn)位置
setLastPoint 設(shè)置之前操作的最后一個(gè)點(diǎn)位置

moveTo示例代碼:

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

moveTo只改變下次操作的起點(diǎn),在執(zhí)行完第一次LineTo的時(shí)候,本來(lái)的默認(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:

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)仍然無(wú)法形成封閉圖形,則close什么也不做。

(2)第2組:addXxx與arcTo

這次內(nèi)容主要是在Path中添加基本圖形,重點(diǎn)區(qū)分addArc與arcTo。

第一類(lèi)(基本形狀)

// 圓形
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)

這一類(lèi)就是在path中添加一個(gè)基本形狀,基本形狀部分和前面所講的繪制基本形狀并無(wú)太大差別。

仔細(xì)觀察一下第一類(lèi)的方法,無(wú)一例外,在最后都有一個(gè)Path.Direction。Direction的意思是方向,趨勢(shì),是一個(gè)枚舉(Enum)類(lèi)型,里面只有兩個(gè)枚舉常量,如下:

類(lèi)型 解釋 翻譯
CW clockwise 順時(shí)針
CCW counter-clockwise 逆時(shí)針

它們的作用如下有:一是在添加圖形時(shí)確定閉合順序(各個(gè)點(diǎn)的記錄順序);二是對(duì)圖形的渲染結(jié)果有影響(是判斷圖形渲染的重要條件) 。

先研究確定閉合順序的問(wèn)題,添加一個(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)行一次,會(huì)發(fā)現(xiàn)兩次運(yùn)行結(jié)果一模一樣!

想要讓它現(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);

Path是使用四個(gè)點(diǎn)來(lái)記錄矩形,對(duì)于上面這個(gè)矩形來(lái)說(shuō),采用的是順時(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的位置,如下:

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

參數(shù)中點(diǎn)的順序很重要!

第二類(lèi)(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ì)比較簡(jiǎn)單,也很容易理解,就是將兩個(gè)Path合并成為一個(gè)。

第三個(gè)方法是將src添加到當(dāng)前path之前先使用Matrix進(jìn)行變換。

第二個(gè)方法比第一個(gè)方法多出來(lái)的兩個(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),我們?cè)趯瑘A形的path添加到包含矩形的path之前將其進(jìn)行移動(dòng)了一段距離,最終繪制出來(lái)的效果就如上面所示。

第三類(lèi)(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)

從名字就可以看出,這兩個(gè)方法都是與圓弧相關(guān)的,作用都是添加一個(gè)圓弧到path中,但既然存在兩個(gè)方法,兩者之間肯定是有區(qū)別的:

名稱(chēng) 作用 區(qū)別
addArc 添加一個(gè)圓弧到path 直接添加一個(gè)圓弧到path中
arcTo 添加一個(gè)圓弧到path 添加一個(gè)圓弧到path,如果圓弧的起點(diǎn)和上次最后一個(gè)坐標(biāo)點(diǎn)不相同,就連接兩個(gè)點(diǎn)

可以看到addArc有1個(gè)方法(實(shí)際上是兩個(gè)的,但另一個(gè)重載方法是API21添加的), 而arcTo有2個(gè)方法,其中一個(gè)最后多了一個(gè)布爾類(lèi)型的變量forceMoveTo。

forceMoveTo是什么作用呢?

這個(gè)變量意思為“是否強(qiáng)制使用moveTo”,也就是說(shuō),是否使用moveTo將變量移動(dòng)到圓弧的起點(diǎn)位移,也就意味著:

forceMoveTo 含義 等價(jià)方法
true 將最后一個(gè)點(diǎn)移動(dòng)到圓弧起點(diǎn),再開(kāi)始繪制圓弧,即不連接最后一個(gè)點(diǎn)與圓弧起點(diǎn) public void addArc (RectF oval, float startAngle, float sweepAngle)
false 直接連接最后一個(gè)點(diǎn)與圓弧起點(diǎn),然后開(kāi)始繪制圓弧 public void arcTo (RectF oval, float startAngle, float sweepAngle)

示例(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)第3組:isEmpty、 isRect、isConvex、 set 和 offset

isEmpty:

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é)果:

03-02 14:22:54.770 12379-12379/com.sloop.canvas E/1: true
03-02 14:22:54.770 12379-12379/com.sloop.canvas E/2: false

isRect:

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é)果:

03-02 16:48:39.669 24179-24179/com.sloop.canvas E/Rect: isRect:true| left:0.0| top:0.0| right:400.0| bottom:400.0

set:

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:

public void offset (float dx, float dy)
public void offset (float dx, float dy, Path dst)

對(duì)path進(jìn)行一段平移,和Canvas中的translate作用很像,但Canvas作用于整個(gè)畫(huà)布,而path的offset只作用于當(dāng)前path。

第二個(gè)方法中最后的參數(shù)dst是存儲(chǔ)平移后的path的。

dst狀態(tài) 效果
dst不為空 將當(dāng)前path平移后的狀態(tài)存入dst中,不會(huì)影響當(dāng)前path
dst為空(null) 平移將作用于當(dāng)前path,相當(dāng)于第一種方法

示例:

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);                // 更改畫(huà)筆顏色

canvas.drawPath(dst,mPaint);                // 繪制dst

從運(yùn)行效果圖可以看出,雖然我們?cè)赿st中添加了一個(gè)矩形,但是并沒(méi)有表現(xiàn)出來(lái),所以,當(dāng)dst中存在內(nèi)容時(shí),dst中原有的內(nèi)容會(huì)被清空,而存放平移后的path。

(4)rXxx方法

此類(lèi)方法可以看到和前面的方法看起來(lái)很像,只是在前面多了一個(gè)r,r代表的是相對(duì)坐標(biāo)。rXxx方法的坐標(biāo)使用的是相對(duì)位置(基于當(dāng)前點(diǎn)的位移),而之前方法的坐標(biāo)是絕對(duì)位置(基于當(dāng)前坐標(biāo)系的坐標(biāo))。

舉個(gè)例子:

Path path = new Path();
path.moveTo(100,100);
path.lineTo(100,200);
canvas.drawPath(path,mDeafultPaint);

改成r方法后:

Path path = new Path();
path.moveTo(100,100);
path.rLineTo(100,200);
canvas.drawPath(path,mDeafultPaint);

6.3 PathMeasure

常用方法如下:

返回值 方法名 釋義
void setPath(Path path, boolean forceClosed) 關(guān)聯(lián)一個(gè)Path
boolean isClosed() 是否閉合
float getLength() 獲取Path的長(zhǎng)度
boolean nextContour() 跳轉(zhuǎn)到下一個(gè)輪廓
boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo) 截取片段
boolean getPosTan(float distance, float[] pos, float[] tan) 獲取指定長(zhǎng)度的位置坐標(biāo)及該點(diǎn)切線值
boolean getMatrix(float distance, Matrix matrix, int flags) 獲取指定長(zhǎng)度的位置坐標(biāo)及該點(diǎn)Matrix

6.3.1 構(gòu)造函數(shù)

(1)無(wú)參構(gòu)造函數(shù)
PathMeasure ()

用這個(gè)構(gòu)造函數(shù)可創(chuàng)建一個(gè)空的 PathMeasure,但是使用之前需要先調(diào)用 setPath 方法來(lái)與 Path 進(jìn)行關(guān)聯(lián)。被關(guān)聯(lián)的 Path 必須是已經(jīng)創(chuàng)建好的,如果關(guān)聯(lián)之后 Path 內(nèi)容進(jìn)行了更改,則需要使用 setPath 方法重新關(guān)聯(lián)

(2)有參構(gòu)造函數(shù)
PathMeasure (Path path, boolean forceClosed)

用這個(gè)構(gòu)造函數(shù)是創(chuàng)建一個(gè) PathMeasure 并關(guān)聯(lián)一個(gè) Path, 其實(shí)和創(chuàng)建一個(gè)空的 PathMeasure 后調(diào)用 setPath 進(jìn)行關(guān)聯(lián)效果是一樣的,同樣,被關(guān)聯(lián)的 Path 也必須是已經(jīng)創(chuàng)建好的,如果關(guān)聯(lián)之后 Path 內(nèi)容進(jìn)行了更改,則需要使用 setPath 方法重新關(guān)聯(lián)

該方法有兩個(gè)參數(shù),第二個(gè)參數(shù)是用來(lái)確保 Path 閉合,如果設(shè)置為 true, 則不論之前Path是否閉合,都會(huì)自動(dòng)閉合該 Path(如果Path可以閉合的話)。

在這里有兩點(diǎn)需要明確:

  • 1、不論 forceClosed 設(shè)置為何種狀態(tài)(true 或者 false), 都不會(huì)影響原有Path的狀態(tài),即 Path 與 PathMeasure 關(guān)聯(lián)之后,之前的的 Path 不會(huì)有任何改變。
  • 2、forceClosed 的設(shè)置狀態(tài)可能會(huì)影響測(cè)量結(jié)果,如果 Path 未閉合但在與 PathMeasure 關(guān)聯(lián)的時(shí)候設(shè)置 forceClosed 為 true 時(shí),測(cè)量結(jié)果可能會(huì)比 Path 實(shí)際長(zhǎng)度稍長(zhǎng)一點(diǎn),獲取到到是該 Path 閉合時(shí)的狀態(tài)。

用一個(gè)例子來(lái)驗(yàn)證一下:

canvas.translate(mViewWidth/2,mViewHeight/2);

Path path = new Path();
path.lineTo(0,200);
path.lineTo(200,200);
path.lineTo(200,0);

PathMeasure measure1 = new PathMeasure(path,false);
PathMeasure measure2 = new PathMeasure(path,true);

Log.e("TAG", "forceClosed=false---->"+measure1.getLength());
Log.e("TAG", "forceClosed=true----->"+measure2.getLength());

canvas.drawPath(path,mDeafultPaint);

log如下:

forceClosed=false---->600.0
forceClosed=true----->800.0

繪制在界面上的效果如下:

通過(guò)上面的示例能驗(yàn)證以上兩個(gè)問(wèn)題,另外還有:

  • 1、將 Path 與兩個(gè)的 PathMeasure 進(jìn)行關(guān)聯(lián),并給 forceClosed 設(shè)置了不同的狀態(tài),之后繪制再繪制出來(lái)的 Path 沒(méi)有任何變化,所以與 Path 與 PathMeasure進(jìn)行關(guān)聯(lián)并不會(huì)影響 Path 狀態(tài)。
  • 2、可以看到,設(shè)置 forceClosed 為 true 的方法比設(shè)置為 false 的方法測(cè)量出來(lái)的長(zhǎng)度要長(zhǎng)一點(diǎn),這是由于 Path 沒(méi)有閉合的緣故,多出來(lái)的距離正是 Path 最后一個(gè)點(diǎn)與最開(kāi)始一個(gè)點(diǎn)之間點(diǎn)距離。forceClosed 為 false 測(cè)量的是當(dāng)前 Path 狀態(tài)的長(zhǎng)度, forceClosed 為 true,則不論P(yáng)ath是否閉合測(cè)量的都是 Path 的閉合長(zhǎng)度。

6.3.2 setPath、 isClosed 和 getLength

setPath 是 PathMeasure 與 Path 關(guān)聯(lián)的重要方法,效果和 構(gòu)造函數(shù) 中兩個(gè)參數(shù)的作用是一樣的。

isClosed 用于判斷 Path 是否閉合,但是如果你在關(guān)聯(lián) Path 的時(shí)候設(shè)置 forceClosed 為 true 的話,這個(gè)方法的返回值則一定為true。

getLength 用于獲取 Path 的總長(zhǎng)度,在之前的測(cè)試中已經(jīng)用過(guò)了。

6.3.3 getSegment

getSegment 用于獲取Path的一個(gè)片段,方法如下:

boolean getSegment (float startD, float stopD, Path dst, boolean startWithMoveTo)

方法各參數(shù)釋義:

參數(shù) 作用 備注
返回值(boolean) 判斷截取是否成功 true 表示截取成功,結(jié)果存入dst中,false 截取失敗,不會(huì)改變dst中內(nèi)容
startD 開(kāi)始截取位置距離 Path 起點(diǎn)的長(zhǎng)度 取值范圍: 0 <= startD < stopD <= Path總長(zhǎng)度
stopD 結(jié)束截取位置距離 Path 起點(diǎn)的長(zhǎng)度 取值范圍: 0 <= startD < stopD <= Path總長(zhǎng)度
dst 截取的 Path 將會(huì)添加到 dst 中 注意: 是添加,而不是替換
startWithMoveTo 起始點(diǎn)是否使用 moveTo 用于保證截取的 Path 第一個(gè)點(diǎn)位置不變
  • 如果 startD、stopD 的數(shù)值不在取值范圍 [0, getLength] 內(nèi),或者 startD == stopD 則返回值為 false,不會(huì)改變 dst 內(nèi)容。
  • 如果在安卓4.4或者之前的版本,在默認(rèn)開(kāi)啟硬件加速的情況下,更改 dst 的內(nèi)容后可能繪制會(huì)出現(xiàn)問(wèn)題,請(qǐng)關(guān)閉硬件加速或者給 dst 添加一個(gè)單個(gè)操作,例如: dst.rLineTo(0, 0)

看看這個(gè)方法如何使用:

創(chuàng)建了一個(gè) Path, 并在其中添加了一個(gè)矩形,現(xiàn)在想截取矩形中的一部分,就是下圖中紅色的部分。

代碼如下:

canvas.translate(mViewWidth / 2, mViewHeight / 2);          // 平移坐標(biāo)系

Path path = new Path();                                     // 創(chuàng)建Path并添加了一個(gè)矩形
path.addRect(-200, -200, 200, 200, Path.Direction.CW);
Path dst = new Path();                                      // 創(chuàng)建用于存儲(chǔ)截取后內(nèi)容的 Path

PathMeasure measure = new PathMeasure(path, false);         // 將 Path 與 PathMeasure 關(guān)聯(lián)
// 截取一部分存入dst中,并使用 moveTo 保持截取得到的 Path 第一個(gè)點(diǎn)的位置不變
measure.getSegment(200, 600, dst, true);                    

canvas.drawPath(dst, mDeafultPaint);                        // 繪制 dst

結(jié)果如下:

從上圖可以看到我們成功到將需要到片段截取了出來(lái),然而當(dāng) dst 中有內(nèi)容時(shí)會(huì)怎樣呢?

canvas.translate(mViewWidth / 2, mViewHeight / 2);          // 平移坐標(biāo)系

Path path = new Path();                                     // 創(chuàng)建Path并添加了一個(gè)矩形
path.addRect(-200, -200, 200, 200, Path.Direction.CW);
Path dst = new Path();                                      // 創(chuàng)建用于存儲(chǔ)截取后內(nèi)容的 Path
dst.lineTo(-300, -300);                                     // <--- 在 dst 中添加一條線段

PathMeasure measure = new PathMeasure(path, false);         // 將 Path 與 PathMeasure 關(guān)聯(lián)
measure.getSegment(200, 600, dst, true);                   // 截取一部分 并使用 moveTo 保持截取得到的 Path 第一個(gè)點(diǎn)的位置不變

canvas.drawPath(dst, mDeafultPaint);                        // 繪制 Path

結(jié)果如下:

從上面的示例可以看到 dst 中的線段保留了下來(lái),可以得到結(jié)論:被截取的 Path 片段會(huì)添加到 dst 中,而不是替換 dst 中到內(nèi)容。

前面兩個(gè)例子中 startWithMoveTo 均為 true, 如果設(shè)置為false會(huì)怎樣呢?

canvas.translate(mViewWidth / 2, mViewHeight / 2);          // 平移坐標(biāo)系

Path path = new Path();                                     // 創(chuàng)建Path并添加了一個(gè)矩形
path.addRect(-200, -200, 200, 200, Path.Direction.CW);
Path dst = new Path();                                      // 創(chuàng)建用于存儲(chǔ)截取后內(nèi)容的 Path
dst.lineTo(-300, -300);                                     // 在 dst 中添加一條線段

PathMeasure measure = new PathMeasure(path, false);         // 將 Path 與 PathMeasure 關(guān)聯(lián)
measure.getSegment(200, 600, dst, false);                   // <--- 截取一部分 不使用 startMoveTo, 保持 dst 的連續(xù)性

canvas.drawPath(dst, mDeafultPaint);                        // 繪制 Path

結(jié)果如下:

從該示例我們又可以得到一條結(jié)論:如果 startWithMoveTo 為 true, 則被截取出來(lái)到Path片段保持原狀,如果 startWithMoveTo 為 false,則會(huì)將截取出來(lái)的 Path 片段的起始點(diǎn)移動(dòng)到 dst 的最后一個(gè)點(diǎn),以保證 dst 的連續(xù)性。

6.3.4 nextContour

我們知道 Path 可以由多條曲線構(gòu)成,但不論是 getLength , getSegment 或者是其它方法,都只會(huì)在其中第一條線段上運(yùn)行,而這個(gè) nextContour 就是用于跳轉(zhuǎn)到下一條曲線的方法,如果跳轉(zhuǎn)成功,則返回 true, 如果跳轉(zhuǎn)失敗,則返回 false。

如下,我們創(chuàng)建了一個(gè) Path 并使其中包含了兩個(gè)閉合的曲線,內(nèi)部的邊長(zhǎng)是200,外面的邊長(zhǎng)是400,現(xiàn)在我們使用 PathMeasure 分別測(cè)量?jī)蓷l曲線的總長(zhǎng)度。

代碼:

canvas.translate(mViewWidth / 2, mViewHeight / 2);      // 平移坐標(biāo)系

Path path = new Path();
path.addRect(-100, -100, 100, 100, Path.Direction.CW);  // 添加小矩形
path.addRect(-200, -200, 200, 200, Path.Direction.CW);  // 添加大矩形

canvas.drawPath(path,mDeafultPaint);                    // 繪制 Path

PathMeasure measure = new PathMeasure(path, false);     // 將Path與PathMeasure關(guān)聯(lián)
float len1 = measure.getLength();                       // 獲得第一條路徑的長(zhǎng)度
measure.nextContour();                                  // 跳轉(zhuǎn)到下一條路徑
float len2 = measure.getLength();                       // 獲得第二條路徑的長(zhǎng)度

Log.i("LEN","len1="+len1);                              // 輸出兩條路徑的長(zhǎng)度
Log.i("LEN","len2="+len2);

log輸出結(jié)果:

len1=800.0
len2=1600.0

通過(guò)測(cè)試,我們可以得到以下內(nèi)容:

  • 1、曲線的順序與 Path 中添加的順序有關(guān)。
  • 2、getLength 獲取到到是當(dāng)前一條曲線分長(zhǎng)度,而不是整個(gè) Path 的長(zhǎng)度。
  • 3、getLength 等方法是針對(duì)當(dāng)前的曲線(其它方法請(qǐng)自行驗(yàn)證)。

6.3.5 getPosTan

這個(gè)方法是用于得到路徑上某一長(zhǎng)度的位置以及該位置的正切值:

boolean getPosTan (float distance, float[] pos, float[] tan)

各參數(shù)釋義:

參數(shù) 作用 備注
返回值(boolean) 判斷獲取是否成功 true表示成功,數(shù)據(jù)會(huì)存入 pos 和 tan 中,
false 表示失敗,pos 和 tan 不會(huì)改變
distance 距離 Path 起點(diǎn)的長(zhǎng)度 取值范圍: 0 <= distance <= getLength
pos 該點(diǎn)的坐標(biāo)值 當(dāng)前點(diǎn)在畫(huà)布上的位置,有兩個(gè)數(shù)值,分別為x,y坐標(biāo)。
tan 該點(diǎn)的正切值 當(dāng)前點(diǎn)在曲線上的方向,使用 Math.atan2(tan[1], tan[0]) 獲取到正切角的弧度值。

tan 是用來(lái)判斷 Path 上趨勢(shì)的,即在這個(gè)位置上曲線的走向,請(qǐng)看下圖示例,注意箭頭的方向:

點(diǎn)擊這里下載箭頭圖片

可以看到 上圖中箭頭在沿著 Path 運(yùn)動(dòng)時(shí),方向始終與 Path 走向保持一致,保持方向主要就是依靠 tan

下面來(lái)看看代碼是如何實(shí)現(xiàn)的,首先需要定義幾個(gè)必要的變量:

private float currentValue = 0;     // 用于紀(jì)錄當(dāng)前的位置,取值范圍[0,1]映射Path的整個(gè)長(zhǎng)度
private float[] pos;                // 當(dāng)前點(diǎn)的實(shí)際位置
private float[] tan;                // 當(dāng)前點(diǎn)的tangent值,用于計(jì)算圖片所需旋轉(zhuǎn)的角度
private Bitmap mBitmap;             // 箭頭圖片
private Matrix mMatrix;             // 矩陣,用于對(duì)圖片進(jìn)行一些操作

初始化這些變量(在構(gòu)造函數(shù)中調(diào)用):

private void init(Context context) {
      pos = new float[2];
      tan = new float[2];
      BitmapFactory.Options options = new BitmapFactory.Options();
      options.inSampleSize = 2;       // 縮放圖片
      mBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.arrow, options);
      mMatrix = new Matrix();
}

具體繪制:

canvas.translate(mViewWidth / 2, mViewHeight / 2);      // 平移坐標(biāo)系

Path path = new Path();                                 // 創(chuàng)建 Path
path.addCircle(0, 0, 200, Path.Direction.CW);           // 添加一個(gè)圓形
PathMeasure measure = new PathMeasure(path, false);     // 創(chuàng)建 PathMeasure

currentValue += 0.005;                                  // 計(jì)算當(dāng)前的位置在總長(zhǎng)度上的比例[0,1]
if (currentValue >= 1) {
      currentValue = 0;
}

measure.getPosTan(measure.getLength() * currentValue, pos, tan);        // 獲取當(dāng)前位置的坐標(biāo)以及趨勢(shì)

mMatrix.reset();                                                        // 重置Matrix
float degrees = (float) (Math.atan2(tan[1], tan[0]) * 180.0 / Math.PI); // 計(jì)算圖片旋轉(zhuǎn)角度

mMatrix.postRotate(degrees, mBitmap.getWidth() / 2, mBitmap.getHeight() / 2);   // 旋轉(zhuǎn)圖片
mMatrix.postTranslate(pos[0] - mBitmap.getWidth() / 2, pos[1] - mBitmap.getHeight() / 2);   // 將圖片繪制中心調(diào)整到與當(dāng)前點(diǎn)重合

canvas.drawPath(path, mDeafultPaint);                                   // 繪制 Path
canvas.drawBitmap(mBitmap, mMatrix, mDeafultPaint);                     // 繪制箭頭

invalidate();                                                           // 重繪頁(yè)面

核心要點(diǎn):

  • 1、圖片需要旋轉(zhuǎn)的角度應(yīng)該跟圓上改點(diǎn)的切線斜率有關(guān),而切線夾角的tan值可以通過(guò)getPosTan得到,而Math中 atan2 方法是根據(jù)正切是數(shù)值計(jì)算出該角度的大小,得到的單位是弧度(取值范圍是 -pi 到 pi),所以上面又將弧度轉(zhuǎn)為了角度。
  • 2、通過(guò) Matrix 來(lái)設(shè)置圖片對(duì)旋轉(zhuǎn)角度和位移,這里使用的方法與前面講解過(guò)對(duì) canvas操作有些類(lèi)似。
  • 3、頁(yè)面刷新,頁(yè)面刷新此處是在 onDraw 里面調(diào)用了 invalidate 方法來(lái)保持界面不斷循環(huán)刷新,但并不提倡這么做,正確對(duì)做法應(yīng)該是使用 線程 或者 ValueAnimator 來(lái)控制界面的刷新。

6.3.6 getMatrix

這個(gè)方法是用于得到路徑上某一長(zhǎng)度的位置以及該位置的正切值的矩陣:

boolean getMatrix (float distance, Matrix matrix, int flags)

各參數(shù)釋義:

參數(shù) 作用 備注
返回值(boolean) 判斷獲取是否成功 true表示成功,數(shù)據(jù)會(huì)存入matrix中,false 失敗,matrix內(nèi)容不會(huì)改變
distance 距離 Path 起點(diǎn)的長(zhǎng)度 取值范圍: 0 <= distance <= getLength
matrix 根據(jù) falgs 封裝好的matrix 會(huì)根據(jù) flags 的設(shè)置而存入不同的內(nèi)容
flags 規(guī)定哪些內(nèi)容會(huì)存入到matrix中 可選擇POSITION_MATRIX_FLAG(位置)、ANGENT_MATRIX_FLAG(正切)

其實(shí)這個(gè)方法就相當(dāng)于在前一個(gè)例子中封裝 matrix 的過(guò)程,上面的過(guò)程由 getMatrix 來(lái)做了,可以直接得到一個(gè)封裝好到 matrix

最后到 flags 選項(xiàng)可以選擇 位置 或者 正切,如果兩個(gè)選項(xiàng)都想選擇,可以將兩個(gè)選項(xiàng)之間用 | 連接起來(lái),如下:

measure.getMatrix(distance, matrix, PathMeasure.TANGENT_MATRIX_FLAG | PathMeasure.POSITION_MATRIX_FLAG);

試試將上面都例子中 getPosTan 替換為 getMatrix, 這樣一來(lái)就會(huì)顯得簡(jiǎn)單很多:

具體繪制:

Path path = new Path();                                 // 創(chuàng)建 Path
path.addCircle(0, 0, 200, Path.Direction.CW);           // 添加一個(gè)圓形

PathMeasure measure = new PathMeasure(path, false);     // 創(chuàng)建 PathMeasure

currentValue += 0.005;                                  // 計(jì)算當(dāng)前的位置在總長(zhǎng)度上的比例[0,1]
if (currentValue >= 1) {
      currentValue = 0;
}

// 獲取當(dāng)前位置的坐標(biāo)以及趨勢(shì)的矩陣
measure.getMatrix(measure.getLength() * currentValue, mMatrix, PathMeasure.TANGENT_MATRIX_FLAG | PathMeasure.POSITION_MATRIX_FLAG);
mMatrix.preTranslate(-mBitmap.getWidth() / 2, -mBitmap.getHeight() / 2);   // <-- 將圖片繪制中心調(diào)整到與當(dāng)前點(diǎn)重合(注意:此處是前乘pre)

canvas.drawPath(path, mDeafultPaint);                                   // 繪制 Path
canvas.drawBitmap(mBitmap, mMatrix, mDeafultPaint);                     // 繪制箭頭

invalidate();                                                           // 重繪頁(yè)面

使用 getMatrix 方法的確可以節(jié)省一些代碼,不過(guò)這里依舊需要注意一些內(nèi)容:

  • 1、對(duì) matrix 的操作必須要在 getMatrix 之后進(jìn)行,否則會(huì)被 getMatrix 重置而導(dǎo)致無(wú)效。
  • 2、矩陣對(duì)旋轉(zhuǎn)角度默認(rèn)為圖片的左上角,此處需要使用 preTranslate 調(diào)整為圖片中心。
  • 3、pre(矩陣前乘) 與 post(矩陣后乘) 的區(qū)別,后續(xù)文章講解。

6.4 Path & SVG

當(dāng)圖形過(guò)于復(fù)雜時(shí),用代碼寫(xiě)就不現(xiàn)實(shí)了,在繪制復(fù)雜的圖形時(shí)一般是將 SVG 圖像轉(zhuǎn)換為 Path。

SVG 是一種矢量圖,內(nèi)部用的是 xml 格式化存儲(chǔ)方式存儲(chǔ)這操作和數(shù)據(jù),可以將 SVG 看作是 Path 的各項(xiàng)操作簡(jiǎn)化書(shū)寫(xiě)后的存儲(chǔ)格式。

Path 和 SVG 結(jié)合通常能誕生出一些奇妙的東西,如下:

該圖片來(lái)自這個(gè)開(kāi)源庫(kù) :PathView
SVG 轉(zhuǎn) Path 的解析可以用這個(gè)庫(kù): AndroidSVG


6.5 Path使用技巧

先放一個(gè)效果圖,然后分析一下實(shí)現(xiàn)過(guò)程:

這是一個(gè)搜索的動(dòng)效圖,通過(guò)分析可以得到它應(yīng)該有四種狀態(tài),分別如下:

狀態(tài) 概述
初始狀態(tài) 初始狀態(tài),沒(méi)有任何動(dòng)效,只顯示一個(gè)搜索標(biāo)志 ??
準(zhǔn)備搜索 放大鏡圖標(biāo)逐漸變化為一個(gè)點(diǎn)
正在搜索 圍繞這一個(gè)圓環(huán)運(yùn)動(dòng),并且線段長(zhǎng)度會(huì)周期性變化
準(zhǔn)備結(jié)束 從一個(gè)點(diǎn)逐漸變化成為放大鏡圖標(biāo)

這些狀態(tài)是有序轉(zhuǎn)換的,轉(zhuǎn)換流程以及轉(zhuǎn)換條件如下:

6.5.1 Path 劃分

為了制作對(duì)方便,此處整個(gè)動(dòng)效用了兩個(gè) Path, 一個(gè)是中間對(duì)放大鏡, 另一個(gè)則是外側(cè)的圓環(huán),將兩者全部畫(huà)出來(lái)是這樣的。

其中 Path 的走向要把握好,如下(只是一個(gè)放大鏡,并不是♂):

其中圓形上面的點(diǎn)可以用 PathMeasure 測(cè)量,無(wú)需計(jì)算。

6.5.2 動(dòng)畫(huà)狀態(tài)與時(shí)間關(guān)聯(lián)

此處使用的是 ValueAnimator,它可以將一段時(shí)間映射到一段數(shù)值上,隨著時(shí)間變化不斷的更新數(shù)值,并且可以使用插值器來(lái)控制數(shù)值變化規(guī)律。

6.5.3 具體繪制

繪制部分是根據(jù) 當(dāng)前狀態(tài)以及從 ValueAnimator 獲得的數(shù)值來(lái)截取 Path 中合適的部分繪制出來(lái)。

6.5.4 最終效果

6.5.5 源碼

戳這里查看源碼

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,646評(píng)論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,595評(píng)論 3 418
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 176,560評(píng)論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 63,035評(píng)論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,814評(píng)論 6 410
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,224評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,301評(píng)論 3 442
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,444評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,988評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,804評(píng)論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,998評(píng)論 1 370
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,544評(píng)論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,237評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,665評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,927評(píng)論 1 287
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,706評(píng)論 3 393
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,993評(píng)論 2 374

推薦閱讀更多精彩內(nèi)容