天天看點

iOS開發學習之YYKit中YYText的深入解析,YYTextShadow的代碼解析

iOS開發學習之YYKit中YYText的深入解析,YYTextShadow的代碼解析

上面的陰影效果是用這樣的代碼實作的:

iOS開發學習之YYKit中YYText的深入解析,YYTextShadow的代碼解析
iOS開發學習之YYKit中YYText的深入解析,YYTextShadow的代碼解析

可以看到先生成了 YYTextShadow, 然後指派給了 attributedString 的 yy_textShadow,然後再把 attributedString 指派到 YYLabel 裡面,接着把 YYLabel 加入到 UIView 裡來顯示。跟蹤 yy_textshadow 發現,主要是把 textShadow 綁定到了 NSAttributedString 的 attribute 裡,key 是 

YYTextShadowAttributeName

,值是 textShadow,也就是先把 shadow 存起來,後來再使用。用 Shift + Command + J 快速跳轉到定義處:

iOS開發學習之YYKit中YYText的深入解析,YYTextShadow的代碼解析
iOS開發學習之YYKit中YYText的深入解析,YYTextShadow的代碼解析

這裡有個 addAttribute,它在 NSAttributedString.h 裡

- (void)addAttribute:(NSString *)name value:(id)value range:(NSRange)range;      

說你可以指派任意的鍵值對給它。而 YYTextShadowAttributeName 的定義是:

iOS開發學習之YYKit中YYText的深入解析,YYTextShadow的代碼解析

一個普通的字元串,說明先是把 shadow 資訊存起來,然後後面再使用。我們全局搜尋一下

YYTextShadowAttributeName

然後我們來到 YYTextLayout 裡的 YYTextDrawShadow 函數,

iOS開發學習之YYKit中YYText的深入解析,YYTextShadow的代碼解析

CGContextTranslateCTM

 是說改變一個 Context 裡的原點坐标,是以

CGContextTranslateCTM(context, point.x, point.y);      

是說要把繪制的上下文移動到 point 點。我們還是先搞清楚哪裡調用了 YYTextDrawShadow 吧,發現是在:

iOS開發學習之YYKit中YYText的深入解析,YYTextShadow的代碼解析
iOS開發學習之YYKit中YYText的深入解析,YYTextShadow的代碼解析

這裡可看到,在 drawInContext 裡,依次去繪制方塊的邊框,然後繪制背景邊框、陰影、下劃線、文字、附屬物、内陰影、删除線、文字邊框、調試線。

那到底那裡用了上面的 drawInContext 呢?我們可以看到裡面有個參數 YYTextDebugOption,是以這個函數一定不是系統的回調,而是 YYText 裡面自己調用的。

我們按住 Ctrl + 1 彈出快捷鍵,發現有四個地方調用了它。

iOS開發學習之YYKit中YYText的深入解析,YYTextShadow的代碼解析

上面的 drawInContext:size:debug 可見還是 YYText 自己的調用,因為 debug 的類型是 

YYTextDebugOption *

, 是 YY 自身的,newAsyncTask 不像是系統的調用,addAttachmentToView:layer: 同理,是以極有可能是 drawRect:。

iOS開發學習之YYKit中YYText的深入解析,YYTextShadow的代碼解析

果然是,看右邊的快速幫助,有詳盡的解釋,幫助的下面也說明了是在 UIView 裡定義的。再看 YYTextContainerView,它是繼承了 UIView 的。

iOS開發學習之YYKit中YYText的深入解析,YYTextShadow的代碼解析

是以 YYLabel 是用了 YYTextContainerView 咯?然後讓系統調用 YYTextContainerView 裡的 drawRect: 畫出來?

iOS開發學習之YYKit中YYText的深入解析,YYTextShadow的代碼解析

奇怪,YYLabel 可繼承了 UIView。是以,YYText 裡應該有兩套東西!一套 YYLabel,一套 YYTextView,像 UILabel 和 UITextView 一樣。接着我們再回去看之前的 YYLabel 的 newAsyncDispalyTask,

iOS開發學習之YYKit中YYText的深入解析,YYTextShadow的代碼解析

很長,在中間的位置調用了 YYTextLayout 裡的 drawInContext。newAsyncDispalyTask,它又是在哪裡調用的呢?

iOS開發學習之YYKit中YYText的深入解析,YYTextShadow的代碼解析

在第二行被調用了。是以可以簡單地了解為 YYLabel 用了異步來繪制文本。而 _displayAsync 被上面的 display 調用了。看 display 的文檔,說是系統會在恰當的時間來調用來更新 layer 的内容,你不要直接去調用它。我們也可以給它打個斷點。

iOS開發學習之YYKit中YYText的深入解析,YYTextShadow的代碼解析

說明這是 display 是在 CALayer 的一次事務中調用的。為何用事務,大概是因為想批量更新,效率高點吧?不像是資料庫裡的復原需求。

display 的系統文檔還說,如果你想你的 layer 繪制不一樣,那你可以複寫這個方法,來實作你自己的繪制。

是以,我們簡單的有了一點思路。YYLabel 通過複寫 UIView 的 display 方法,來異步繪制自己的陰影等各種效果,陰影效果先儲存在了 YYLabel 的 attributedText 裡的 attribute 中,在 display 中繪制的時候再取出來,繪制的時候用了系統的 CoreGraphics 架構。

是以理清了一些思路後,會發現,真正強大的是什麼?一邊是把這麼多效果、異步調用等組織起來,一邊是對底層 CoreGraphics 架構熟練運用。是以對前面的代碼組織有了些了解後,接着我們深入到 CoreGraphics 架構上去。看看是怎麼繪制上去的。

讓我們重新回到 YYTextDrawShadow。

iOS開發學習之YYKit中YYText的深入解析,YYTextShadow的代碼解析
iOS開發學習之YYKit中YYText的深入解析,YYTextShadow的代碼解析

這裡,CGContextSaveGState 和 CGContextRestoreGState 包圍起了一段繪制的代碼。CGContextSaveGState 的意思是說,把目前的繪圖狀态拷貝一份,放到繪制棧裡。每個繪制的 Context 都維護着一個繪制棧。我也不清楚,裡面棧到底是怎麼操作的。先暫且了解為繪制 Context 前要調用 CGContextSaveGState,繪制 Context 後要調用 CGContextRestoreGState,之後中間的繪制就能有效地出現在 Context 裡。CGContextTranslateCTM 是移動到 Context 移動到相應的位置。先是移動到 point.x 和 point.y ,繪制的相應位置,至于後面移動到 0 和 size.height,倒不清楚了,後續再看看。接着取出了 lines,執行了 for 循環。

lines 是什麼?發現在 YYTextLayout 裡的 

(YYTextLayout *)layoutWithContainer:(YYTextContainer *)container text:(NSAttributedString *)text range:(NSRange)range

 指派的。

iOS開發學習之YYKit中YYText的深入解析,YYTextShadow的代碼解析

接着翻到這個函數的定義處:

iOS開發學習之YYKit中YYText的深入解析,YYTextShadow的代碼解析

這個函數非常長,367 到 861 行,500 行代碼!看了頭尾,可見它的用處就是得到這些變量。lines 是怎麼得到的呢?

iOS開發學習之YYKit中YYText的深入解析,YYTextShadow的代碼解析
iOS開發學習之YYKit中YYText的深入解析,YYTextShadow的代碼解析

可以見到在一個大的 for 循環裡把一條一條 line 加入到 lines 裡。那 lineCount 是怎麼得到的呢?

iOS開發學習之YYKit中YYText的深入解析,YYTextShadow的代碼解析

第 472 行建立了一個 framesetter 對象,text 參數是 NSAttributedString,接着在 frameSetter 對象中建立了一個 CTFrameRef,接着從 CTFrameRef 得到了 lines。 line 到底是什麼呢?我們給它打個斷點。

發現,shadow 這個字的 lineCount = 2,并不是我們想象中的字母個數。

iOS開發學習之YYKit中YYText的深入解析,YYTextShadow的代碼解析

是以猜測,白色的 Shadow 整個是一條 line,陰影也是一條 line?

YYText 裡有好幾個例子,

iOS開發學習之YYKit中YYText的深入解析,YYTextShadow的代碼解析

隻顯示其中一種效果,把其它的代碼注釋掉。發現很奇怪,Shadow 的 lineCount = 2,Multiple Shadows 的 lineCount 也是 2,可 Multiple Shadows 還有内陰影啊,應該是 3 條啊?

去找 CTLine 的蘋果文檔,說 CTLine 代表着一行的文本,一個 CTLine 對象包含着一組的 glyph runs。是以就是簡單的行數而已!看上面的斷點截圖,剛剛 shadow 之是以為 2 ,是因為它的文本是 

shadow\n\n

,看剛剛,\n\n 是故意加的,為了顯示美觀:

iOS開發學習之YYKit中YYText的深入解析,YYTextShadow的代碼解析
iOS開發學習之YYKit中YYText的深入解析,YYTextShadow的代碼解析

是以 

shadow\n\n

 就是兩行文本。CTLine 就是我們平時說的行。接着回去看我們的 lineCount:

iOS開發學習之YYKit中YYText的深入解析,YYTextShadow的代碼解析

這裡得到 CTLines 數組,從裡面的個數,然後如果 lineCount 大于 0 的話,得到每行的坐标原點。好了,有了 lineCount,我們接着看 for 循環。

iOS開發學習之YYKit中YYText的深入解析,YYTextShadow的代碼解析

從 ctLines 數組裡得到 CTLine,接着得到 YYTextLine 對象,然後加入到 lines 數組中。然後做一些 line 的 frame 計算。YYTextLine 的構造函數很簡單,先儲存着位置、是否垂直排版、CTLine 對象:

iOS開發學習之YYKit中YYText的深入解析,YYTextShadow的代碼解析

lines 搞清楚之後,我們再回去之前的 YYTextDrawShadow 中去:

iOS開發學習之YYKit中YYText的深入解析,YYTextShadow的代碼解析

這下代碼簡單了。先擷取到行數,周遊它,然後取得 GlyphRuns 數組,再周遊它,GlyphRun 可以了解為一個圖元,或者繪制單元。然後從中得到 attributes 數組,用我們之前的 YYTextShadowAttributeName,擷取我們一開始指派的 shadow,接着開始繪制陰影:

iOS開發學習之YYKit中YYText的深入解析,YYTextShadow的代碼解析

一個 while 循環,來不斷繪制子陰影。調用 CGContextSetShadowWithColor 設好陰影的位移、半徑、顔色。接着調用 YYTextDrawRun 來真正的繪制。YYTextDrawRun 被三個地方調用了:

iOS開發學習之YYKit中YYText的深入解析,YYTextShadow的代碼解析

來繪制内陰影和文本陰影以及文本。說明它是個通用方法,來畫 Run 這個對象。

iOS開發學習之YYKit中YYText的深入解析,YYTextShadow的代碼解析

一開始擷取文字的變換矩陣,用 runTextMatrixIsID 來看看它是否原地不變,如果不是垂直排版或沒有設定圖元轉換的話,就直接上來畫。調用 CTRunDraw 來畫 run 對象。接着斷點發現,繪制一開始那個陰影時隻進入了 if 裡面,沒有進入 else 裡面。

iOS開發學習之YYKit中YYText的深入解析,YYTextShadow的代碼解析

是以我們的陰影繪制就到此結束了!

總結一下,YYLabel 先把陰影等效果儲存在 attribtutedText 裡的 attrributes,複寫了 UIView 的 display 方法,在 display 中進行異步繪制,用 CoreText 架構得到 CTLine、CTRun 對象,從 CTRun 擷取到 attributes,之後再根據 attributes 裡的各屬性,用 CoreGraphics 架構把 CTRun 對象繪制到 Context 中。

了解還是不夠,等後續再來品讀。不覺感歎 YY 實在太強了!今天理了理思路,讓自己邊寫邊讀代碼,不至于枯燥,同時供大家參考。得去睡覺了。。

繼續閱讀