原始碼筆記 — MBProgressHUD

發表於2016-10-19

前言

作為初學者,想要快速提高自己的水平,閱讀一些優秀的第三方原始碼是一個非常好的途徑.通過看別人的程式碼,可以學習不一樣的程式設計思路,瞭解一些沒有接觸過的類和方法. MBProgressHUD是一個非常受歡迎的第三方庫,其用法簡單,程式碼樸實易懂,涉及的知識點廣而不深奧,是非常適合初學者閱讀的一份原始碼.

一. 模式

首先, MBProgressHUD有以下幾種檢視模式.


mode屬性指定顯示模式

11651640-575d141795e6f121
預設使用的系統自帶指示器


12651640-94e6564c7f2557dc
餅圖


13651640-b8534cad90e948e5
進度條

14651640-f416b2d48cbfabee
圓環


15651640-58e2e7cf41ba4a95
只顯示文字


二. 結構

MBProgressHUD由指示器,文字框,詳情文字框,背景框4個部分組成.

16651640-56072c84e31d6acd
結構組成

三. 初始化方法

至於opaque這個屬性,著實讓我糾結了好一陣子,不過暫時先不糾結那麼多,以蘋果官方文件為參考:

This property provides a hint to the drawing system as to how it should treat the view. If set to YES, the drawing system treats the view as fully opaque, which allows the drawing system to optimize some drawing operations and improve performance. If set to NO, the drawing system composites the view normally with other content. The default value of this property is YES.

An opaque view is expected to fill its bounds with entirely opaque content—that is, the content should have an alpha value of 1.0. If the view is opaque and either does not fill its bounds or contains wholly or partially transparent content, the results are unpredictable. You should always set the value of this property to NO if the view is fully or partially transparent.

四. 動畫效果

在HUDshow或者hide的時候會顯示的動畫效果,預設的是MBProgressHUDAnimationFade.

動畫效果MBProgressHUDAnimation是一個列舉.

動畫效果是在這兩個方法中實現的:

接下來-initWithFrame:中又呼叫[self setupLabels]設定了兩個label的相關初始化設定(除了frame的設定–這應該是在layoutSubviews裡面做的事情).然後開始設定指示器.


五. KVO

初始化時,設定完指示器就開始註冊KVO和通知.

具體程式碼實現:

六. 佈局與繪製

佈局

子控制元件的佈局計算沒什麼複雜的地方,為了方便理解,我畫了兩幅圖

17651640-b7ea70f9b040e30f

上圖藍色虛線部分代表子控制元件們能夠展示的區域,其中寬度是被限制的,其中定義了maxWidth讓3個子控制元件中的最大寬度都不得超過它.值得注意的是,原始碼並沒設定最大高度,如果我們使用自定義的檢視,高度夠大就會使藍色虛線部分的上下底超出螢幕範圍.某種程度上來講也是設計上的一種bug,但我認為作者肯定意識到了這點—-label\detailLabel中有很多文字導致換行是很常見的情況,因此需要限制它的最大寬度,但沒人會使用一個非常大的指示器,所以通過額外的計算來考慮因為這種情況超出螢幕上下邊界是毫無必要的.

此外,綠色的label被限制為只能顯示一行,黃色的detailLabel通過下面的程式碼來限制它不能超出螢幕上下.

18651640-8d8eb68f7c57332f

上圖是另一種沒達到maxSize的情況.

繪製

下面看繪製部分,這是MBProgreeHUD中比較重要的內容.

indicator的繪製

MBRoundProgressView

當我們繪製路徑時,描述的路徑如果寬度大於1,描邊的時候是向路徑寬度是以路徑為中點的.

舉個例子,如果從(0,0)(100,0)畫一條寬度為X的線,那麼顯示的寬度實際只有X/2,因為還有一半因為超出了繪圖區域而沒有被繪製.

為了防止繪製內容的丟失,半徑radius的計算是(self.bounds.size.width - lineWidth)/2,而並不是self.bounds.size.width/2.更不是(self.bounds.size.width -2*lineWidth)/2,藉助下圖理解:

19651640-e7ac82ae84ad4e37

在圓餅的繪製過程中,圓餅外層的圓環是通過CGContextStrokeEllipseInRect(CGContextRef, CGRect)進行描邊的,根據上面的結論,圓餅繪製區域(circleRect)和上下文提供的繪製區域(allRect)應該寬高都相差1.f就夠圓餅外層的圓環的正確繪製.作者在這裡用了2.f,實際上1.f就夠了.

接下來是MBBarProgressView的繪製.

MBBarProgressView

MBBarProgressView與MBRoundProgressView的繪製類似,都是使用Quartz2D進行繪圖.使用的都是很基礎很常用的API,所以閱讀難度並不大.唯一讓人困惑的可能是這個CGContextAddArcToPoint(CGContextRef c, CGFloat x1, CGFloat y1,CGFloat x2, CGFloat y2, CGFloat radius)了,另一個畫弧的函式則簡單很多:CGContextAddArc(CGContextRef c, CGFloat x, CGFloat y, CGFloat radius, CGFloat startAngle, CGFloat endAngle, int clockwise).

結合下圖,我的理解方式是:P1為繪圖的當前點,x1 ,y1, x2, y2表示了兩個定點.通過當前點P1,點(x1,y1)(x2,y2),可以表示一個確定的角度,這時一個任意半徑的圓都能與圖中的兩條射線相切.不同半徑的圓,圓心角都不同,兩個切點之間的弧也不相同.舉個例子,我們拿不同半徑的球體去貼到兩面牆的相交處,兩個切點之間有段弧線,球越大弧越長,但是圓心角大小都是一樣的.控制圓心角大小由這三個點決定,能夠獲得的最大圓心角是90度.

20651640-200ff3c3a846229d
函式示意圖

兩個畫弧的函式差別有點大,CGContextAddArcToPoint分為兩步:

  1. 從當前點P1開始,沿著(x1,y1)方向畫線段.
  2. 線段一直畫到與虛線相切的地方.
  3. 這是圓被分成了兩段弧線,繪製短的那條(即圓心對著的那段弧).

我們還可以得到其他的結論:

  1. (x2,y2)的作用只是為了確定與另一條射線形成的角度,只要(x2,y2)是在(x1,y1)->(x2,y2)射線方向上的任意一點就可以了.
  2. P1點剛好為切點時,畫出來的僅僅是一條弧線而不是線段加弧線.
  3. CGContextAddArcToPoint功能比CGContextAddArc強大,後者需要起始角度和終止角度.有些情況下,是很難算出這兩個角度的.

當利用上面的結論2時,畫出來的弧和使用CGContextAddArc函式畫出的弧效果相當.如果三個點形成的角度為直角,那麼剛好是1/4圓弧.

遺憾的是,原始碼並沒有發揮該函式強大的一面,使用了CGContextAddLineToPoint來畫蛇添足.將它們註釋掉,結果並沒有什麼不同,讀者可以繼續註釋後三條CGContextAddArcToPoint,可以驗證該函式已經幫我們畫好線段了.

畫完背景後,繼續進行了描邊,描邊的程式碼和上面幾乎一模一樣,作者之所以這樣做,是因為一個子路徑的fillstroke效果是不能同時產生的,哪個先呼叫,就只會出現它產生的效果.如果原始碼是這樣寫的:

所以作者的做法是——又畫了一個路徑.

事實上,可以使用CGContextDrawPath(CGContextRef c, CGPathDrawingMode mode)函式解決這個問題.這樣就能省略很多的重複程式碼.

progress進度的更新

1.使用者更新progress屬性

2.由於progress被監聽,觸發KVO,呼叫- observeValueForKeyPath:ofObject:change:context:

3.observeValueForKeyPath:ofObject:change:context:中呼叫了setNeedsDisplay,標識檢視為需要重新繪製.

4.呼叫drawRect:重繪,進度條更新

七. 顯示與隱藏

顯示

顯示過程中,原始碼提供了給hud“繫結”後臺任務的方法.

taskInProgress的意思要結合graceTime來看.graceTime是為了防止hud只顯示很短時間(一閃而過)的情況,給使用者設定的一個屬性,如果任務在graceTime內完成,將不會showhud.所以graceTime這個屬性離開了賦給hud的任務就沒意義了.因此,taskInProgress用來標識是否帶有執行的任務.

值得注意的是,通過showWhileExecuting:onTarget:withObject:animated:等方法時,會自動將taskInProgress置為yes,其他情況(任務所在的執行緒不是由hud內部所建立的)需手動設定這個屬性.

隱藏

八. 用法

用法示例程式碼來自該原始碼的github上.

如果你想要對MBProgressHUD 進行額外的配置,需要將showHUDAddedTo:animated:的返回的例項進行設定.

UI的更新應當總是在主執行緒上完成的,一些MBProgressHUD 上的屬性的setter方法考慮到了執行緒安全,可以被後臺執行緒安全地呼叫.這些setter包括setMode:, setCustomView:, setLabelText:, setLabelFont:, setDetailsLabelText:, setDetailsLabelFont: 和 setProgress:.

如果你需要在主執行緒上執行一個耗時的操作,你需要在執行前稍微延時一下,以使得在阻塞主執行緒之前,UIKit有足夠的時間去更新UI(即繪製HUD).

相關文章