本文新版本已轉移到開源中國,歡迎前往指導。
Inkpad是一款非常優秀的iPad向量繪圖軟體,保管你一看見就忘不了。我的感覺是“一覽眾山小”、“相見甚晚”,以至於我寫的TouchVG就是“小巫見大巫”。必須好好學習這款軟體的程式碼,破解其高效能繪圖奧祕。
另外,在寫這篇日誌前本想使用Markdown語言寫乾淨的部落格,在 http://rhcad.github.io/ 基於Jekyll配置了日誌專案,在本地配置了釋出平臺,無奈要做的事和要學的知識太多,半途停下來了,看來我不是當極客的料。
如果你閱讀本文覺得哪裡寫得糟糕,可以提出來交流,如果本文能幫助你一點點就OK了,我也是在學習,本意不是想寫漂亮的文章。
一、觸控互動繪圖
互動式繪圖當然得先看觸控響應機制,先看一張序列圖:
WDCanvas是代表繪圖畫布的主檢視,直接響應原始觸控事件(touchesBegan、touchesMoved等),沒有使用雙擊、旋轉、長按等很流行的手勢識別器,奇怪吧?
我猜想Inkpad不使用手勢識別器有兩個原因:(1)手勢識別器採用延遲識別技術進行手勢二義性判斷,有幾百毫秒的延遲,會影響繪圖快速響應的感覺;(2)Inkpad是獨立的程式,所有介面都是自己的,不需要與各種帶有手勢識別器的介面元件共存(例如在滾動檢視、電子書頁面中繪圖)。
在WDCanvas中通過“[[WDToolManager sharedInstance].activeTool touchesBegan:touches withEvent:event inCanvas:self]”向當前命令傳遞觸控事件,當然用的是單例項模式和命令模式了。
在WDCanvas的touchesBegan函式中不立即向互動命令WDTool傳送觸控開始事件,而是延遲到touchesMoved才去傳送。我猜想作者本想區分普通輕擊(Tap)和拖動(Pan),但在實現時並不徹底:在下面的touchesEnded函式片段中,沒有移動也是要傳送touchesBegan的。我認為應該在touchesMoved中檢查移動距離來判斷是否算是已移動,而不是簡單的直接切換到已移動狀態。
if (!controlGesture_ && [self canSendTouchToActiveTool]) { if (!moved_) { [[WDToolManager sharedInstance].activeTool touchesBegan:touches withEvent:event inCanvas:self]; } [[WDToolManager sharedInstance].activeTool touchesEnded:touches withEvent:event inCanvas:self]; }
在開始觸控、當前不是鋼筆命令(WDPenTool)時,設定WDCanvas的activePath為空(self.drawingController.activePath = nil)。當前路徑(activePath)用於記憶鋼筆命令的當前圖形路徑,將activePath定義在WDDrawingController中而不是定義在WDPenTool中,是方便於在多個類中檢查使用。
互動命令類的觸控響應函式使用瞭如下所示的模板方法模式,具體的命令類重寫beginWithEvent、moveWithEvent、endWithEvent函式實現具體的繪圖邏輯。
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event inCanvas:(WDCanvas *)canvas { //..... WDEvent *genericEvent = [self genericEventForTouch:primaryTouch_ inCanvas:canvas]; [self beginWithEvent:genericEvent inCanvas:canvas]; self.previousEvent = genericEvent; //...... } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event inCanvas:(WDCanvas *)canvas { //...... [self moveWithEvent:genericEvent inCanvas:canvas]; self.previousEvent = genericEvent; //...... } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event inCanvas:(WDCanvas *)canvas { [self endWithEvent:genericEvent inCanvas:canvas]; } - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event inCanvas:(WDCanvas *)canvas { [self touchesEnded:touches withEvent:event inCanvas:canvas]; }
下面就要說說Inkpad的一大亮點:在動態拖曳繪圖中使用OpenGL ES高速繪圖了!
在繪圖命令(例如可繪製多種圖形的WDShapeTool)中,設定WDCanvas的shapeUnderConstruction屬性,將當前的臨時圖形(WDPath)傳遞給WDCanvas。WDCanvas的setShapeUnderConstruction函式將呼叫WDSelectionView成員的立即繪製函式,WDSelectionView使用OpenGL ES 1.1繪製各種動態圖形。用了OpenGL ES,拖動上千圖形也回顯毫無延遲感覺,這是Inkpad的亮點。
Inkpad在使用OpenGL ES 1.1繪製動態圖形時,採用的優化顯示方法有:(1)單點線寬,(2)忽略虛線樣式,(3)不填充,(4)快取圖形輪廓路徑。
觸控操作完成後就要向文件提交靜態圖形了:(1)繪圖命令在endWithEvent中向當前層提交新的圖形物件([canvas.drawing addObject:path]);(2)WDDrawingController 觸發WDCanvas檢視的重繪訊息;(3)在WDCanvas的drawRect:函式中,遍歷各個圖形,呼叫各個物件的renderInContext函式顯示所有圖形,其實質是將圖形的path顯示到當前CGContext上。
與動態圖形的繪製相比,提交靜態圖形並沒有採用OpenGL ES,而是使用最簡單的CGContext顯示方式,而且是重畫全部圖形,沒有使用CALayer等高階渲染方式。這就產生一個疑問,大量圖形會不會太慢?我還沒去做效能分析實驗,初步估計Inkpad採用的顯示優化方法是:(1)快取CGPath輪廓路徑和填充路徑等物件;(2)避免幾何計算和物件重構。
Inkpad已經這麼好了,就需要重新評估TouchVG最近做的一些顯示優化實驗,看看這些計算是否有必要:(1)在GCD中非同步繪製,使用前臺圖形列表和後臺圖形列表避免多執行緒衝突;(2)在CALayer上提前繪製,在主檢視中只貼圖(使用renderInContext,利用GPU貼圖);(3)每一層圖形一個CALayer,避免重繪所有圖形;(4)是否有必要使用OpenGL ES 2.0繪製靜態圖形?
二、相關核心類
1、WDCanvas:繪圖主檢視,包含標尺檢視、選擇集渲染檢視、刪除提示線檢視,包含互動命令類要用的臨時圖形物件。
2、WDCanvasController:繪圖介面操作類,負責多種UI控制元件的管理和操作分發。
3、WDDrawingController:負責各種圖形的增刪改查邏輯。WDCanvasController是外殼功能,WDDrawingController是核心功能。
4、WDDrawing:圖形文件類,容納所有繪圖內容。
5、WDLayer:一個層,包含多個圖形元素WDElement。
6、WDStylable:可描邊、填充的圖形的基類。
7、WDAbstractPath:具有向量路徑的圖形的基類。
8、WDPath:可指定線端箭頭形狀的向量路徑的圖形類。
9、WDCompoundPath:複合路徑的圖形類。
10、WDTool:命令基類。
11、WDShapeTool:新增幾何形狀的命令,可繪製矩形、橢圓、星形、多邊形、直線段、螺旋線。這些不同圖形的繪圖工具是在WDToolManager中構造的。
12、WDPenTool:貝塞爾曲線繪圖工具。
13、WDFreehandTool:自由畫光滑曲線工具。
對於色部分的Core.Model類,主要基於向量路徑設計了幾何圖形模型類,好像不包含圖形的特徵資料(即後續編輯保持形狀特徵)吧。
我覺得我們可以基於Inkpad對圖形種類和互動命令工具進行擴充,做點行業相關的軟體來,如果你感興趣就加入討論吧。
這兩個UML圖是用EA畫的,InkpadUml.xmi可以匯入到其他UML建模工具。
寫了半天有點累了,先寫到這吧。期望目標是把Inkpad吃透,結合TouchVG生出一個新孩子:)
本文新版本已轉移到開源中國,歡迎前往指導。