# Path之完結篇(偽)
### 作者微博: [@GcsSloop](http://weibo.com/GcsSloop)
### [【本系列相關文章】](https://github.com/GcsSloop/AndroidNote/tree/master/CustomView/README.md)
經歷過前兩篇 [Path之基本操作](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B05%5DPath_Basic.md) 和 [Path之貝塞爾曲線](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B06%5DPath_Bezier.md) 的講解,本篇終于進入Path的收尾篇,本篇結束后Path的大部分相關方法都已經講解完了,但Path還有一些更有意思的玩法,應該會在后續的文章中出現吧,嗯,應該會的ˊ_>ˋ
******
## 一.Path常用方法表
> 為了兼容性(_偷懶_) 本表格中去除了在API21(即安卓版本5.0)以上才添加的方法。忍不住吐槽一下,為啥看起來有些順手就能寫的重載方法要等到API21才添加上啊。寶寶此刻內心也是崩潰的。
| 作用 | 相關方法 | 備注 |
| ----------- | ---------------------------------------- | ---------------------------------------- |
| 移動起點 | 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中的內容<br/> **reset不保留內部數據結構,但會保留FillType.**<br/> **rewind會保留內部的數據結構,但不保留FillType** |
| 矩陣操作 | transform | 矩陣變換 |
## 二、Path方法詳解
### rXxx方法
此類方法可以看到和前面的一些方法看起來很像,只是在前面多了一個r,那么這個rXxx和前面的一些方法有什么區別呢?
> **rXxx方法的坐標使用的是相對位置(基于當前點的位移),而之前方法的坐標是絕對位置(基于當前坐標系的坐標)。**
**舉個例子:**
``` java
Path path = new Path();
path.moveTo(100,100);
path.lineTo(100,200);
canvas.drawPath(path,mDeafultPaint);
```

在這個例子中,先移動點到坐標(100,100)處,之后再連接 _點(100,100)_ 到 _(100,200)_ 之間點直線,非常簡單,畫出來就是一條豎直的線,那接下來看下一個例子:
``` java
Path path = new Path();
path.moveTo(100,100);
path.rLineTo(100,200);
canvas.drawPath(path,mDeafultPaint);
```

這個例子中,將 lineTo 換成了 rLineTo 可以看到在屏幕上原本是豎直的線變成了傾斜的線。這是因為最終我們連接的是 _(100,100)_ 和 _(200, 300)_ 之間的線段。
在使用rLineTo之前,當前點的位置在 (100,100) , 使用了 rLineTo(100,200) 之后,下一個點的位置是在當前點的基礎上加上偏移量得到的,即 (100+100, 100+200) 這個位置,故最終結果如上所示。
**PS: 此處僅以 rLineTo 為例,只要理解 “絕對坐標” 和 “相對坐標” 的區別,其他方法類比即可。**
### 填充模式
我們在之前的文章中了解到,Paint有三種樣式,“描邊” “填充” 以及 “描邊加填充”,我們這里所了解到就是在Paint設置為后兩種樣式時**不同的填充模式對圖形渲染效果的影響**。
**我們要給一個圖形內部填充顏色,首先需要分清哪一部分是外部,哪一部分是內部,機器不像我們人那么聰明,機器是如何判斷內外呢?**
機器判斷圖形內外,一般有以下兩種方法:
> PS:此處所有的圖形均為封閉圖形,不包括圖形不封閉這種情況。
| 方法 | 判定條件 | 解釋 |
| ------- | --------------------- | ---------------------------------------- |
| 奇偶規則 | 奇數表示在圖形內,偶數表示在圖形外 | 從任意位置p作一條射線, 若與該射線相交的圖形邊的數目為奇數,則p是圖形內部點,否則是外部點。 |
| 非零環繞數規則 | 若環繞數為0表示在圖形外,非零表示在圖形內 | 首先使圖形的邊變為矢量。將環繞數初始化為零。再從任意位置p作一條射線。當從p點沿射線方向移動時,對在每個方向上穿過射線的邊計數,每當圖形的邊從右到左穿過射線時,環繞數加1,從左到右時,環繞數減1。處理完圖形的所有相關邊之后,若環繞數為非零,則p為內部點,否則,p是外部點。 |
接下來我們先了解一下兩種判斷方法是如何工作的。
#### 奇偶規則(Even-Odd Rule)
這一個比較簡單,也容易理解,直接用一個簡單示例來說明。

在上圖中有一個四邊形,我們選取了三個點來判斷這些點是否在圖形內部。
>
>P1: 從P1發出一條射線,發現圖形與該射線相交邊數為0,偶數,故P1點在圖形外部。<br/>
>P2: 從P2發出一條射線,發現圖形與該射線相交邊數為1,奇數,故P2點在圖形內部。<br/>
>P3: 從P3發出一條射線,發現圖形與該射線相交邊數為2,偶數,故P3點在圖形外部。<br/>
#### 非零環繞數規則(Non-Zero Winding Number Rule)
非零環繞數規則相對來說比較難以理解一點。
我們在之前的文章 [Path之基本操作](https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/%5B05%5DPath_Basic.md) 中我們了解到,在給Path中添加圖形時需要指定圖形的添加方式,是用順時針還是逆時針,另外我們不論是使用lineTo,quadTo,cubicTo還是其他連接線的方法,都是從一個點連接到另一個點,換言之,**Path中任何線段都是有方向性的**,這也是使用非零環繞數規則的基礎。
我們依舊用一個簡單的例子來說明非零環繞數規則的用法:
> **PS: 注意圖形中線段的方向性!**

>
>P1: 從P1點發出一條射線,沿射線方向移動,并沒有與邊相交點部分,環繞數為0,故P1在圖形外邊。<br/>
>P2: 從P2點發出一條射線,沿射線方向移動,與圖形點左側邊相交,該邊從左到右穿過射線,環繞數-1,最終環繞數為-1,故P2在圖形內部。<br/>
>P3: 從P3點發出一條射線,沿射線方向移動,在第一個交點處,底邊從右到左穿過射線,環繞數+1,在第二個交點處,右側邊從左到右穿過射線,環繞數-1,最終環繞數為0,故P3在圖形外部。<br/>
通常,這兩種方法的判斷結果是相同的,但也存在兩種方法判斷結果不同的情況,如下面這種情況:
> 注意圖形線段的方向,就不詳細解釋了,用上面的方法進行判斷即可。

#### 自相交圖形
**自相交圖形定義:多邊形在平面內除頂點外還有其他公共點。**
簡單的提一下自相交圖形,了解概念即可,下圖就是一個簡單的自相交圖形:

#### Android中的填充模式
Android中的填充模式有四種,是封裝在Path中的一個枚舉。
| 模式 | 簡介 |
| ---------------- | -------- |
| EVEN_ODD | 奇偶規則 |
| INVERSE_EVEN_ODD | 反奇偶規則 |
| WINDING | 非零環繞數規則 |
| INVERSE_WINDING | 反非零環繞數規則 |
我們可以看到上面有四種模式,分成兩對,例如 "奇偶規則" 與 "反奇偶規則" 是一對,它們之間有什么關系呢?
Inverse 的含義是“相反,對立”,說明反奇偶規則剛好與奇偶規則相反,例如對于一個矩形而言,使用奇偶規則會填充矩形內部,而使用反奇偶規則會填充矩形外部,這個會在后面示例中代碼展示兩者的區別。
#### Android與填充模式相關的方法
> 這些都是Path中的方法。
| 方法 | 作用 |
| --------------------- | ------------------------ |
| setFillType | 設置填充規則 |
| getFillType | 獲取當前填充規則 |
| isInverseFillType | 判斷是否是反向(INVERSE)規則 |
| toggleInverseFillType | 切換填充規則(即原有規則與反向規則之間相互切換) |
#### 示例演示:
本演示著重于幫助理解填充模式中的一些難點和易混淆的問題,對于一些比較簡單的問題,讀者可自行驗證,本文中不會過多贅述。
##### 奇偶規則與反奇偶規則
``` java
mDeafultPaint.setStyle(Paint.Style.FILL); // 設置畫布模式為填充
canvas.translate(mViewWidth / 2, mViewHeight / 2); // 移動畫布(坐標系)
Path path = new Path(); // 創建Path
//path.setFillType(Path.FillType.EVEN_ODD); // 設置Path填充模式為 奇偶規則
path.setFillType(Path.FillType.INVERSE_EVEN_ODD); // 反奇偶規則
path.addRect(-200,-200,200,200, Path.Direction.CW); // 給Path中添加一個矩形
```
下面兩張圖片分別是在奇偶規則與反奇偶規則的情況下繪制的結果,可以看出其填充的區域剛好相反:
> PS: 白色為背景色,黑色為填充色。


##### 圖形邊的方向對非零奇偶環繞數規則填充結果的影響
我們之前討論過給Path添加圖形時順時針與逆時針的作用,除了上次講述的方便記錄外,就是本文所涉及的另外一個重要作用了: **"作為非零環繞數規則的判斷依據。"**
通過前面我們已經大致了解了在圖形邊的方向會如何影響到填充效果,我們這里驗證一下:
``` java
mDeafultPaint.setStyle(Paint.Style.FILL); // 設置畫筆模式為填充
canvas.translate(mViewWidth / 2, mViewHeight / 2); // 移動畫布(坐系)
Path path = new Path(); // 創建Path
// 添加小正方形 (通過這兩行代碼來控制小正方形邊的方向,從而演示不同的效果)
// path.addRect(-200, -200, 200, 200, Path.Direction.CW);
path.addRect(-200, -200, 200, 200, Path.Direction.CCW);
// 添加大正方形
path.addRect(-400, -400, 400, 400, Path.Direction.CCW);
path.setFillType(Path.FillType.WINDING); // 設置Path填充模式為非零環繞規則
canvas.drawPath(path, mDeafultPaint); // 繪制Path
```


### 布爾操作(API19)
布爾操作與我們中學所學的集合操作非常像,只要知道集合操作中的交集,并集,差集等操作,那么理解布爾操作也是很容易的。
**布爾操作是兩個Path之間的運算,主要作用是用一些簡單的圖形通過一些規則合成一些相對比較復雜,或難以直接得到的圖形**。
如太極中的陰陽魚,如果用貝塞爾曲線制作的話,可能需要六段貝塞爾曲線才行,而在這里我們可以用四個Path通過布爾運算得到,而且會相對來說更容易理解一點。

``` java
canvas.translate(mViewWidth / 2, mViewHeight / 2);
Path path1 = new Path();
Path path2 = new Path();
Path path3 = new Path();
Path path4 = new Path();
path1.addCircle(0, 0, 200, Path.Direction.CW);
path2.addRect(0, -200, 200, 200, Path.Direction.CW);
path3.addCircle(0, -100, 100, Path.Direction.CW);
path4.addCircle(0, 100, 100, Path.Direction.CCW);
path1.op(path2, Path.Op.DIFFERENCE);
path1.op(path3, Path.Op.UNION);
path1.op(path4, Path.Op.DIFFERENCE);
canvas.drawPath(path1, mDeafultPaint);
```
前面演示了布爾運算的作用,接下來我們了解一下布爾運算的核心:布爾邏輯。
Path的布爾運算有五種邏輯,如下:
| 邏輯名稱 | 類比 | 說明 | 示意圖 |
| ------------------ | ---- | ------------------------ | ---------------------------------------- |
| DIFFERENCE | 差集 | Path1中減去Path2后剩下的部分 |  |
| REVERSE_DIFFERENCE | 差集 | Path2中減去Path1后剩下的部分 |  |
| INTERSECT | 交集 | Path1與Path2相交的部分 |  |
| UNION | 并集 | 包含全部Path1和Path2 |  |
| XOR | 異或 | 包含Path1與Path2但不包括兩者相交的部分 |  |
#### 布爾運算方法
通過前面的理論知識鋪墊,相信大家對布爾運算已經有了基本的認識和理解,下面我們用代碼演示一下布爾運算:
在Path中的布爾運算有兩個方法
``` java
boolean op (Path path, Path.Op op)
boolean op (Path path1, Path path2, Path.Op op)
```
兩個方法中的返回值用于判斷布爾運算是否成功,它們使用方法如下:
``` java
// 對 path1 和 path2 執行布爾運算,運算方式由第二個參數指定,運算結果存入到path1中。
path1.op(path2, Path.Op.DIFFERENCE);
// 對 path1 和 path2 執行布爾運算,運算方式由第三個參數指定,運算結果存入到path3中。
path3.op(path1, path2, Path.Op.DIFFERENCE)
```
#### 布爾運算示例

代碼:
``` java
int x = 80;
int r = 100;
canvas.translate(250,0);
Path path1 = new Path();
Path path2 = new Path();
Path pathOpResult = new Path();
path1.addCircle(-x, 0, r, Path.Direction.CW);
path2.addCircle(x, 0, r, Path.Direction.CW);
pathOpResult.op(path1,path2, Path.Op.DIFFERENCE);
canvas.translate(0, 200);
canvas.drawText("DIFFERENCE", 240,0,mDeafultPaint);
canvas.drawPath(pathOpResult,mDeafultPaint);
pathOpResult.op(path1,path2, Path.Op.REVERSE_DIFFERENCE);
canvas.translate(0, 300);
canvas.drawText("REVERSE_DIFFERENCE", 240,0,mDeafultPaint);
canvas.drawPath(pathOpResult,mDeafultPaint);
pathOpResult.op(path1,path2, Path.Op.INTERSECT);
canvas.translate(0, 300);
canvas.drawText("INTERSECT", 240,0,mDeafultPaint);
canvas.drawPath(pathOpResult,mDeafultPaint);
pathOpResult.op(path1,path2, Path.Op.UNION);
canvas.translate(0, 300);
canvas.drawText("UNION", 240,0,mDeafultPaint);
canvas.drawPath(pathOpResult,mDeafultPaint);
pathOpResult.op(path1,path2, Path.Op.XOR);
canvas.translate(0, 300);
canvas.drawText("XOR", 240,0,mDeafultPaint);
canvas.drawPath(pathOpResult,mDeafultPaint);
```
### 計算邊界
這個方法主要作用是計算Path所占用的空間以及所在位置,方法如下:
``` java
void computeBounds (RectF bounds, boolean exact)
```
它有兩個參數:
| 參數 | 作用 |
| ------ | ------------------------------- |
| bounds | 測量結果會放入這個矩形 |
| exact | 是否精確測量,目前這一個參數作用已經廢棄,一般寫true即可 |
關于exact如有疑問可參見Google官方的提交記錄[Path.computeBounds()](https://code.google.com/p/android/issues/detail?id=4070)
#### 計算邊界示例
計算path邊界的一個簡單示例.

代碼:
``` java
// 移動canvas,mViewWidth與mViewHeight在 onSizeChanged 方法中獲得
canvas.translate(mViewWidth/2,mViewHeight/2);
RectF rect1 = new RectF(); // 存放測量結果的矩形
Path path = new Path(); // 創建Path并添加一些內容
path.lineTo(100,-50);
path.lineTo(100,50);
path.close();
path.addCircle(-100,0,100, Path.Direction.CW);
path.computeBounds(rect1,true); // 測量Path
canvas.drawPath(path,mDeafultPaint); // 繪制Path
mDeafultPaint.setStyle(Paint.Style.STROKE);
mDeafultPaint.setColor(Color.RED);
canvas.drawRect(rect1,mDeafultPaint); // 繪制邊界
```
### 重置路徑
重置Path有兩個方法,分別是reset和rewind,兩者區別主要有以下兩點:
| 方法 | 是否保留FillType設置 | 是否保留原有數據結構 |
| ------ | :------------: | :--------: |
| reset | 是 | 否 |
| rewind | 否 | 是 |
**這個兩個方法應該何時選擇呢?**
選擇權重: FillType > 數據結構
_因為“FillType”影響的是顯示效果,而“數據結構”影響的是重建速度。_
## 總結
Path中常用的方法到此已經結束,希望能夠幫助大家加深對Path的理解運用,讓大家能夠用Path愉快的玩耍。( ̄▽ ̄)
(,,? ? ?,,)
#### PS: 由于本人水平有限,某些地方可能存在誤解或不準確,如果你對此有疑問可以提交Issues進行反饋。
## About Me
### 作者微博: [@GcsSloop](http://weibo.com/GcsSloop)
<a href="https://github.com/GcsSloop/AndroidNote/blob/magic-world/FINDME.md" target="_blank"> <img src="http://ww4.sinaimg.cn/large/005Xtdi2gw1f1qn89ihu3j315o0dwwjc.jpg" width=300 height=100 /> </a>
## 參考資料
[Path](https://developer.android.com/reference/android/graphics/Path.html)<br/>
[維基百科-Nonzero-rule](https://en.wikipedia.org/wiki/Nonzero-rule)<br/>
[android繪圖之Path總結](http://ghui.me/post/2015/10/android-graphics-path/)<br/>
[布爾邏輯](https://zh.wikipedia.org/wiki/%E5%B8%83%E5%B0%94%E9%80%BB%E8%BE%91)<br/>
[GoogleCode-Path.computeBounds()](https://code.google.com/p/android/issues/detail?id=4070)<br/>
[Path.reset vs Path.rewind](http://stackoverflow.com/questions/11505617/path-reset-vs-path-rewind)<br/>
[]()<br/>
[]()<br/>
- 0-發現
- AndroidInterview-Q-A
- Android能讓你少走彎路的干貨整理
- LearningNotes
- temp
- temp11
- 部分地址
- 0-待辦任務
- 待補充列表
- 0-未分類
- AndroidView事件分發與滑動沖突處理
- Spannable
- 事件分發機制詳解
- 1-Java
- 1-Java-01基礎
- 未歸檔
- 你應該知道的JDK知識
- 集合框架
- 1-Java-04合集
- Java之旅0
- Java之旅
- JAVA之旅01
- JAVA之旅02
- JAVA之旅03
- JAVA之旅04
- JAVA之旅05
- JAVA之旅06
- JAVA之旅07
- JAVA之旅08
- JAVA之旅09
- java之旅1
- JAVA之旅10
- JAVA之旅11
- JAVA之旅12
- JAVA之旅13
- JAVA之旅14
- JAVA之旅15
- JAVA之旅16
- JAVA之旅17
- JAVA之旅18
- JAVA之旅19
- java之旅2
- JAVA之旅20
- JAVA之旅21
- JAVA之旅22
- JAVA之旅23
- JAVA之旅24
- JAVA之旅25
- JAVA之旅26
- JAVA之旅27
- JAVA之旅28
- JAVA之旅29
- java之旅3
- JAVA之旅30
- JAVA之旅31
- JAVA之旅32
- JAVA之旅33
- JAVA之旅34
- JAVA之旅35
- 1-Java-05辨析
- HashMapArrayMap
- Java8新特性
- Java8接口默認方法
- 圖解HashMap(1)
- 圖解HashMap(2)
- 2-Android
- 2-Android-1-基礎
- View繪制流程
- 事件分發
- AndroidView的事件分發機制和滑動沖突解決
- 自定義View基礎
- 1-安卓自定義View基礎-坐標系
- 2-安卓自定義View基礎-角度弧度
- 3-安卓自定義View基礎-顏色
- 自定義View進階
- 1-安卓自定義View進階-分類和流程
- 10-安卓自定義View進階-Matrix詳解
- 11-安卓自定義View進階-MatrixCamera
- 12-安卓自定義View進階-事件分發機制原理
- 13-安卓自定義View進階-事件分發機制詳解
- 14-安卓自定義View進階-MotionEvent詳解
- 15-安卓自定義View進階-特殊形狀控件事件處理方案
- 16-安卓自定義View進階-多點觸控詳解
- 17-安卓自定義View進階-手勢檢測GestureDetector
- 2-安卓自定義View進階-繪制基本圖形
- 3-安卓自定義View進階-畫布操作
- 4-安卓自定義View進階-圖片文字
- 5-安卓自定義View進階-Path基本操作
- 6-安卓自定義View進階-貝塞爾曲線
- 7-安卓自定義View進階-Path完結篇偽
- 8-安卓自定義View進階-Path玩出花樣PathMeasure
- 9-安卓自定義View進階-Matrix原理
- 通用類介紹
- Application
- 2-Android-2-使用
- 2-Android-02控件
- ViewGroup
- ConstraintLayout
- CoordinatorLayout
- 2-Android-03三方使用
- Dagger2
- Dagger2圖文完全教程
- Dagger2最清晰的使用教程
- Dagger2讓你愛不釋手-終結篇
- Dagger2讓你愛不釋手-重點概念講解、融合篇
- dagger2讓你愛不釋手:基礎依賴注入框架篇
- 閱讀筆記
- Glide
- Google推薦的圖片加載庫Glide:最新版使用指南(含新特性)
- rxjava
- 這可能是最好的RxJava2.x入門教程完結版
- 這可能是最好的RxJava2.x入門教程(一)
- 這可能是最好的RxJava2.x入門教程(三)
- 這可能是最好的RxJava2.x入門教程(二)
- 這可能是最好的RxJava2.x入門教程(五)
- 這可能是最好的RxJava2.x入門教程(四)
- 2-Android-3-優化
- 優化概況
- 各種優化
- Android端秒開優化
- apk大小優化
- 內存分析
- 混淆
- 2-Android-4-工具
- adb命令
- 一鍵分析Android的BugReport
- 版本控制
- git
- git章節簡述
- 2-Android-5-源碼
- HandlerThread 源碼分析
- IntentService的使用和源碼分析
- 2-Android-9-辨析
- LRU算法
- 什么是Bitmap
- 常見圖片壓縮方式
- 3-Kotlin
- Kotlin使用筆記1-草稿
- Kotlin使用筆記2
- kotlin特性草稿
- Kotlin草稿-Delegation
- Kotlin草稿-Field
- Kotlin草稿-object
- 4-JavaScript
- 5-Python
- 6-Other
- Git
- Gradle
- Android中ProGuard配置和總結
- gradle使用筆記
- Nexus私服搭建
- 編譯提速最佳實踐
- 7-設計模式與架構
- 組件化
- 組件化探索(OKR)
- 1-參考列表
- 2-1-組件化概述
- 2-2-gradle配置
- 2-3-代碼編寫
- 2-4-常見問題
- 2-9-值得一讀
- 8-數據結構與算法
- 0臨時文件
- 漢諾塔
- 8-數據-1數據結構
- HashMap
- HashMap、Hashtable、HashSet 和 ConcurrentHashMap 的比較
- 遲到一年HashMap解讀
- 8-數據-2算法
- 1個就夠了
- Java常用排序算法(必須掌握的8大排序算法)
- 常用排序算法總結(性能+代碼)
- 必須知道的八大種排序算法(java實現)
- 9-職業
- 閱讀
- 書單
- 面試
- 面試-01-java
- Java面試題全集駱昊(上)
- Java面試題全集駱昊(下)
- Java面試題全集駱昊(中)
- 面試-02-android
- 40道Android面試題
- 面試-03-開源源碼
- Android圖片加載框架最全解析(二),從源碼的角度理解Glide的執行流程
- 面試-07-設計模式
- 面試-08-算法
- 面試-09-其他
- SUMMARY
- 版權說明
- temp111