前言
作為初學者,想要快速提高自己的水平,閱讀一些優秀的第三方原始碼是一個非常好的途徑.通過看別人的程式碼,可以學習不一樣的程式設計思路,瞭解一些沒有接觸過的類和方法. MBProgressHUD是一個非常受歡迎的第三方庫,其用法簡單,程式碼樸實易懂,涉及的知識點廣而不深奧,是非常適合初學者閱讀的一份原始碼.
一. 模式
首先, MBProgressHUD
有以下幾種檢視模式.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
typedef enum { /** 預設模式,使用系統自帶的指示器 ,不能顯示進度,只能不停地轉呀轉*/ MBProgressHUDModeIndeterminate, /** 用餅圖顯示進度 */ MBProgressHUDModeDeterminate, /** 進度條 */ MBProgressHUDModeDeterminateHorizontalBar, /** 圓環 */ MBProgressHUDModeAnnularDeterminate, /** 自定義檢視 */ MBProgressHUDModeCustomView, /** 只顯示文字 */ MBProgressHUDModeText } MBProgressHUDMode; |
mode
屬性指定顯示模式
1 |
hud.mode = MBProgressHUDModeIndeterminate; |
1 |
hud.mode = MBProgressHUDModeDeterminate; |
1 |
hud.mode = MBProgressHUDModeDeterminateHorizontalBar; |
1 |
hud.mode = MBProgressHUDModeAnnularDeterminate; |
1 2 |
hud.mode = MBProgressHUDModeText; hud.labelText = @"MBProgressHUDModeText"; |
二. 結構
MBProgressHUD
由指示器,文字框,詳情文字框,背景框4個部分組成.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// 文字框和其相關屬性 @property (copy) NSString *labelText; @property (MB_STRONG) UIFont* labelFont; @property (MB_STRONG) UIColor* labelColor; //詳情文字框和其相關屬性 @property (copy) NSString *detailsLabelText; @property (MB_STRONG) UIFont* detailsLabelFont; @property (MB_STRONG) UIColor* detailsLabelColor; // 背景框的透明度,預設值是0.8 @property (assign) float opacity; // 背景框的顏色, 如果設定了這個屬性,則opacity屬性會失效,即不會有半透明效果 @property (MB_STRONG) UIColor *color; // 背景框的圓角半徑。預設值是10.0 @property (assign) float cornerRadius; // 菊花的顏色,預設是白色 @property (MB_STRONG) UIColor *activityIndicatorColor; |
三. 初始化方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
- (id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { // 顯示隱藏時的動畫模式 self.animationType = MBProgressHUDAnimationFade; // 預設指示器是菊花 self.mode = MBProgressHUDModeIndeterminate; ...... // 關閉繪製的"效能開關",如果alpha不為1,最好將opaque設為NO,讓繪圖系統優化效能 self.opaque = NO; // 使背景顏色為透明 self.backgroundColor = [UIColor clearColor]; // 即使使用者建立了一個hud,並呼叫了addSubview方法 // 沒有呼叫show也是不能顯示的.在這之前要使hud隱藏並且不能接受觸控事件 // 透明度為0(小於等於0.01),相當於hidden,無法響應觸控事件 self.alpha = 0.0f; rotationTransform = CGAffineTransformIdentity; // 設定label和detailLabel [self setupLabels]; // 設定指示器 [self updateIndicators]; } return self; } |
至於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
.
1 |
self.animationType = MBProgressHUDAnimationFade; |
動畫效果MBProgressHUDAnimation
是一個列舉.
1 2 3 4 5 6 7 8 9 |
typedef NS_ENUM(NSInteger, MBProgressHUDAnimation) { // 預設效果,只有透明度變化的動畫效果 MBProgressHUDAnimationFade, // 透明度變化+形變效果,其中MBProgressHUDAnimationZoom和 // MBProgressHUDAnimationZoomOut的列舉值都為1 MBProgressHUDAnimationZoom, MBProgressHUDAnimationZoomOut = MBProgressHUDAnimationZoom, MBProgressHUDAnimationZoomIn }; |
動畫效果是在這兩個方法中實現的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
// 顯示HUD - (void)showUsingAnimation:(BOOL)animated { // Cancel any scheduled hideDelayed: calls [NSObject cancelPreviousPerformRequestsWithTarget:self]; [self setNeedsDisplay]; // ZoomIn,ZoomOut分別理解為`拉近鏡頭`,`拉遠鏡頭` // 因此MBProgressHUDAnimationZoomIn先把形變縮小到0.5倍,再恢復到原狀,產生放大效果 // 反之MBProgressHUDAnimationZoomOut先把形變放大到1.5倍,再恢復原狀,產生縮小效果 // 要注意的是,形變的是整個`MBProgressHUD`,而不是中間可視部分 if (animated && animationType == MBProgressHUDAnimationZoomIn) { // 在初始化方法中, 已經定義了rotationTransform = CGAffineTransformIdentity. // CGAffineTransformIdentity也就是對view不進行變形,對view進行仿射變化總是原樣 // CGAffineTransformConcat是兩個矩陣相乘,與之等價的設定方式是: // self.transform = CGAffineTransformScale(rotationTransform, 0.5f, 0.5f); self.transform = CGAffineTransformConcat(rotationTransform, CGAffineTransformMakeScale(0.5f, 0.5f)); } else if (animated && animationType == MBProgressHUDAnimationZoomOut) { // self.transform = CGAffineTransformScale(rotationTransform, 1.5f, 1.5f); self.transform = CGAffineTransformConcat(rotationTransform, CGAffineTransformMakeScale(1.5f, 1.5f)); } self.showStarted = [NSDate date]; // 開始做動畫 if (animated) { // 在初始化方法或者`hideUsingAnimation:`方法中,alpha被設定為0.f,在該方法中完成0.f~1.f的動畫效果 [UIView beginAnimations:nil context:NULL]; [UIView setAnimationDuration:0.30]; self.alpha = 1.0f; // 從形變狀態回到初始狀態 if (animationType == MBProgressHUDAnimationZoomIn || animationType == MBProgressHUDAnimationZoomOut) { self.transform = rotationTransform; } [UIView commitAnimations]; } else { self.alpha = 1.0f; } } // 隱藏HUD - (void)hideUsingAnimation:(BOOL)animated { // Fade out if (animated && showStarted) { [UIView beginAnimations:nil context:NULL]; [UIView setAnimationDuration:0.30]; [UIView setAnimationDelegate:self]; [UIView setAnimationDidStopSelector:@selector(animationFinished:finished:context:)]; // 當alpha小於0.01時,就會被當做全透明對待,全透明是接收不了觸控事件的. // 所以設定0.02防止hud在還沒結束動畫並呼叫done方法之前傳遞觸控事件. // 在完成的回撥animationFinished:finished:context:才設為0 if (animationType == MBProgressHUDAnimationZoomIn) { self.transform = CGAffineTransformConcat(rotationTransform, CGAffineTransformMakeScale(1.5f, 1.5f)); } else if (animationType == MBProgressHUDAnimationZoomOut) { self.transform = CGAffineTransformConcat(rotationTransform, CGAffineTransformMakeScale(0.5f, 0.5f)); } self.alpha = 0.02f; [UIView commitAnimations]; } else { self.alpha = 0.0f; [self done]; } self.showStarted = nil; } |
接下來-initWithFrame:
中又呼叫[self setupLabels]
設定了兩個label
的相關初始化設定(除了frame
的設定–這應該是在layoutSubviews
裡面做的事情).然後開始設定指示器.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
- (void)updateIndicators { // 讀原始碼的時候,類似這種區域性變數直接忽略,等程式碼用到它,我們再"懶載入" BOOL isActivityIndicator = [indicator isKindOfClass:[UIActivityIndicatorView class]]; BOOL isRoundIndicator = [indicator isKindOfClass:[MBRoundProgressView class]]; // 如果模式是MBProgressHUDModeIndeterminate,將使用系統自帶的菊花系列指示器 if (mode == MBProgressHUDModeIndeterminate) { // 再看回最上面的兩條語句 // 初始化的時候進來,indicator是空的,對空物件傳送訊息返回的布林值是NO // 因為在初始化完畢後,使用者可能會設定mode屬性,那時還會進入這個方法,所以這兩個布林變數除了第一次以外是有用的 if (!isActivityIndicator) { // 預設第一次會進入到這裡,對nil傳送訊息不會發生什麼事 // 為什麼要removeFromSuperview呢,因為這方法並不會只進入一次 // 不排除有些情況下先改變了mode到其他模式,之後又改回來了,這時候如果不移除 // MBProgressHUD就會殘留子控制元件在subviews裡,雖然介面並不會顯示它 [indicator removeFromSuperview]; // 使用系統自帶的巨大白色菊花 // 系統菊花有三種 //typedef NS_ENUM(NSInteger, UIActivityIndicatorViewStyle) { // UIActivityIndicatorViewStyleWhiteLarge, // 大又白 // UIActivityIndicatorViewStyleWhite, // 小白 // UIActivityIndicatorViewStyleGray, // 小灰 //}; self.indicator = MB_AUTORELEASE([[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge]); [(UIActivityIndicatorView *)indicator startAnimating]; [self addSubview:indicator]; } // 系統菊花能設定顏色是從iOS5開始(NS_AVAILABLE_IOS(5_0)),這裡用巨集對手機版本進行了判斷 #if __IPHONE_OS_VERSION_MIN_REQUIRED >= 50000 [(UIActivityIndicatorView *)indicator setColor:self.activityIndicatorColor]; #endif } // 原始碼實現了兩種自定義檢視 // 一種是MBBarProgressView(進度條),另一種是MBRoundProgressView(圓餅or圓環) else if (mode == MBProgressHUDModeDeterminateHorizontalBar) { // 進度條樣式 [indicator removeFromSuperview]; self.indicator = MB_AUTORELEASE([[MBBarProgressView alloc] init]); [self addSubview:indicator]; } else if (mode == MBProgressHUDModeDeterminate || mode == MBProgressHUDModeAnnularDeterminate) { // 這兩種mode都產生MBRoundProgressView檢視,MBRoundProgressView又分兩種樣式 // 如果你設定了mode為MBProgressHUDModeDeterminate,那麼流程是這樣子的 // 1)alloc init先生成系統的MBProgressHUDModeIndeterminate模式-> // 2)設定了mode為餅圖,觸發KVO,又進入了updateIndicators方法-> // 3)由於isRoundIndicator是No,產生餅狀圖 // 如果設定了MBProgressHUDModeAnnularDeterminate,那麼步驟比它多了一步, // 1)alloc init先生成系統的MBProgressHUDModeIndeterminate模式-> // 2)設定了mode為圓環,觸發KVO,又進入了updateIndicators方法-> // 3)由於isRoundIndicator是No,產生餅狀圖-> // 4)設定[(MBRoundProgressView *)indicator setAnnular:YES]觸發MBRoundProgressView的 // KVO進行重繪檢視產生圓環圖 if (!isRoundIndicator) { // 個人認為這個isRoundIndicator變數純屬多餘 // isRoundIndicator為Yes的情況只有從MBProgressHUDModeDeterminate換成MBProgressHUDModeAnnularDeterminate // 或者MBProgressHUDModeAnnularDeterminate換成MBProgressHUDModeDeterminate // 而實際上這兩種切換方式產生的檢視都是圓環,這是由於沒有讓annular設定成No [indicator removeFromSuperview]; self.indicator = MB_AUTORELEASE([[MBRoundProgressView alloc] init]); [self addSubview:indicator]; } if (mode == MBProgressHUDModeAnnularDeterminate) { [(MBRoundProgressView *)indicator setAnnular:YES]; } } else if (mode == MBProgressHUDModeCustomView && customView != indicator) { // 自定義檢視 [indicator removeFromSuperview]; self.indicator = customView; [self addSubview:indicator]; } else if (mode == MBProgressHUDModeText) { // 只有文字的模式 [indicator removeFromSuperview]; self.indicator = nil; } } |
五. KVO
初始化時,設定完指示器就開始註冊KVO和通知.
1 2 3 4 |
..... [self registerForKVO]; [self registerForNotifications]; ..... |
具體程式碼實現:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
// 註冊KVO,遍歷從[self observableKeypaths]返回的字串,觀察這些屬性的變化 - (void)registerForKVO { for (NSString *keyPath in [self observableKeypaths]) { [self addObserver:self forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:NULL]; } } - (NSArray *)observableKeypaths { return [NSArray arrayWithObjects:@"mode", @"customView", @"labelText", @"labelFont", @"labelColor", @"detailsLabelText", @"detailsLabelFont", @"detailsLabelColor", @"progress", @"activityIndicatorColor", nil]; } // 在delloc的時候,需要將觀察解除 - (void)unregisterFromKVO { for (NSString *keyPath in [self observableKeypaths]) { [self removeObserver:self forKeyPath:keyPath]; } } // 觸發KVO - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (![NSThread isMainThread]) { // 當前是子執行緒,那麼切換到主執行緒進行UI更新 [self performSelectorOnMainThread:@selector(updateUIForKeypath:) withObject:keyPath waitUntilDone:NO]; } else { // 當前執行緒為主執行緒,直接更新 [self updateUIForKeypath:keyPath]; } } - (void)updateUIForKeypath:(NSString *)keyPath { ........ // 以上省略一萬行 else if ([keyPath isEqualToString:@"progress"]) { // 除了系統指示器和自定義檢視,MB給我們提供的三種形狀的指示器都帶有progress屬性 if ([indicator respondsToSelector:@selector(setProgress:)]) { // 觸發該檢視的KVO更新指示器檢視 [(id)indicator setValue:@(progress) forKey:@"progress"]; } // 繪製交給檢視內部處理 return; } // 如果更改了label的字型,需要重新呼叫layoutSubviews [self setNeedsLayout]; // 設定標記,在下一個週期呼叫drawRect:方法重繪 [self setNeedsDisplay]; } |
六. 佈局與繪製
佈局
子控制元件的佈局計算沒什麼複雜的地方,為了方便理解,我畫了兩幅圖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
- (void)layoutSubviews { [super layoutSubviews]; // MBProgressHUD是一個充滿整個父控制元件的控制元件 // 使得父控制元件的互動完全被遮蔽 UIView *parent = self.superview; if (parent) { self.frame = parent.bounds; } CGRect bounds = self.bounds; ....... // 如果使用者設定了square屬性,就會盡量讓它顯示成正方形 if (square) { // totalSize為下圖藍色框框的size CGFloat max = MAX(totalSize.width, totalSize.height); if (max <= bounds.size.width - 2 * margin) { totalSize.width = max; } if (max <= bounds.size.height - 2 * margin) { totalSize.height = max; } } if (totalSize.width < minSize.width) { totalSize.width = minSize.width; } if (totalSize.height < minSize.height) { totalSize.height = minSize.height; } size = totalSize; } |
上圖藍色虛線部分
代表子控制元件們能夠展示的區域,其中寬度是被限制的,其中定義了maxWidth
讓3個子控制元件中的最大寬度都不得超過它.值得注意的是,原始碼並沒設定最大高度,如果我們使用自定義的檢視,高度夠大就會使藍色虛線部分
的上下底超出螢幕範圍.某種程度上來講也是設計上的一種bug,但我認為作者肯定意識到了這點—-label\detailLabel
中有很多文字導致換行是很常見的情況,因此需要限制它的最大寬度,但沒人會使用一個非常大的指示器,所以通過額外的計算來考慮因為這種情況超出螢幕上下邊界是毫無必要的.
此外,綠色的label
被限制為只能顯示一行,黃色的detailLabel
通過下面的程式碼來限制它不能超出螢幕上下.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// 計算出螢幕剩下的高度 // 其中減去了4個margin大小,保證了子空間和HUD的邊距,HUD和螢幕的距離 CGFloat remainingHeight = bounds.size.height - totalSize.height - kPadding - 4 * margin; // 將文字內容限制在這個size中,超出部分省略號 CGSize maxSize = CGSizeMake(maxWidth, remainingHeight); CCGSize detailsLabelSize = MB_MULTILINE_TEXTSIZE(detailsLabel.text, detailsLabel.font, maxSize, detailsLabel.lineBreakMode); // 7.0開始使用boundingRectWithSize:options:attributes:context:方法計算 // 7.0以前使用sizeWithFont:constrainedToSize:lineBreakMode:計算 #if __IPHONE_OS_VERSION_MIN_REQUIRED >= 70000 #define MB_MULTILINE_TEXTSIZE(text, font, maxSize, mode) [text length] > 0 ? [text \ boundingRectWithSize:maxSize options:(NSStringDrawingUsesLineFragmentOrigin) \ attributes:@{NSFontAttributeName:font} context:nil].size : CGSizeZero; #else #define MB_MULTILINE_TEXTSIZE(text, font, maxSize, mode) [text length] > 0 ? [text \ sizeWithFont:font constrainedToSize:maxSize lineBreakMode:mode] : CGSizeZero; #endif |
上圖是另一種沒達到maxSize
的情況.
繪製
下面看繪製部分,這是MBProgreeHUD中比較重要的內容.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
- (void)drawRect:(CGRect)rect { // 拿到當前的繪圖上下文 CGContextRef context = UIGraphicsGetCurrentContext(); UIGraphicsPushContext(context); // 預設中間的HUD外是透明的,可以看到父控制元件,設定了dimBackground這個屬性可以讓HUD周圍是一個漸變色的背景. // 這裡用了一個漸變層,顏色是寫死的 if (self.dimBackground) { //Gradient colours size_t gradLocationsNum = 2; CGFloat gradLocations[2] = {0.0f, 1.0f}; CGFloat gradColors[8] = {0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.75f}; CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); CGGradientRef gradient = CGGradientCreateWithColorComponents(colorSpace, gradColors, gradLocations, gradLocationsNum); CGColorSpaceRelease(colorSpace); //Gradient center CGPoint gradCenter= CGPointMake(self.bounds.size.width/2, self.bounds.size.height/2); //Gradient radius float gradRadius = MIN(self.bounds.size.width , self.bounds.size.height) ; //Gradient draw CGContextDrawRadialGradient (context, gradient, gradCenter, 0, gradCenter, gradRadius, kCGGradientDrawsAfterEndLocation); CGGradientRelease(gradient); } // 使用者有設定顏色就使用設定的顏色,沒有的話預設灰色 // 從下面程式碼可以看出,自定義HUD背景顏色是沒有透明度的 if (self.color) { CGContextSetFillColorWithColor(context, self.color.CGColor); } else { CGContextSetGrayFillColor(context, 0.0f, self.opacity); } CGRect allRect = self.bounds; // 畫出一個圓角的HUD // size在layoutSubviews中被計算出來,是HUD的真實size CGRect boxRect = CGRectMake(round((allRect.size.width - size.width) / 2) + self.xOffset, round((allRect.size.height - size.height) / 2) + self.yOffset, size.width, size.height); float radius = self.cornerRadius; //開始繪製路徑 CGContextBeginPath(context); // 起始點 CGContextMoveToPoint(context, CGRectGetMinX(boxRect) + radius, CGRectGetMinY(boxRect)); // 依次畫出右上角、右下角,左下角,左上角的四分之一圓弧 // 注意,雖然沒有顯式地呼叫CGContextAddLineToPoint函式 // 但繪製圓弧時每一次的起點都會和上一次的終點連線,生成線段 CGContextAddArc(context, CGRectGetMaxX(boxRect) - radius, CGRectGetMinY(boxRect) + radius, radius, 3 * (float)M_PI / 2, 0, 0); CGContextAddArc(context, CGRectGetMaxX(boxRect) - radius, CGRectGetMaxY(boxRect) - radius, radius, 0, (float)M_PI / 2, 0); CGContextAddArc(context, CGRectGetMinX(boxRect) + radius, CGRectGetMaxY(boxRect) - radius, radius, (float)M_PI / 2, (float)M_PI, 0); CGContextAddArc(context, CGRectGetMinX(boxRect) + radius, CGRectGetMinY(boxRect) + radius, radius, (float)M_PI, 3 * (float)M_PI / 2, 0); CGContextClosePath(context); CGContextFillPath(context); // UIGraphicsPopContext(); } |
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
,藉助下圖理解:
1 2 3 4 5 |
// 圓環繪製 if (_annular) { // iOS7.0以後的圓環描邊風格變了,變成了2.f // 7.0之前的還是5.f.主要是為了迎合扁平的風格我覺得 BOOL isPreiOS7 = kCFCoreFoundationVersionNumber |
在圓餅的繪製過程中,圓餅外層的圓環是通過CGContextStrokeEllipseInRect(CGContextRef, CGRect)
進行描邊的,根據上面的結論,圓餅繪製區域(circleRect)和上下文提供的繪製區域(allRect)應該寬高都相差1.f
就夠圓餅外層的圓環的正確繪製.作者在這裡用了2.f
,實際上1.f
就夠了.
1 2 |
CGRect allRect = self.bounds; CGRect circleRect = CGRectInset(allRect, 2.0f, 2.0f); |
接下來是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度.
兩個畫弧的函式差別有點大,CGContextAddArcToPoint
分為兩步:
- 從當前點
P1
開始,沿著(x1,y1)
方向畫線段. - 線段一直畫到
圓
與虛線相切的地方. - 這是圓被分成了兩段弧線,繪製短的那條(即圓心對著的那段弧).
我們還可以得到其他的結論:
(x2,y2)
的作用只是為了確定與另一條射線形成的角度,只要(x2,y2)
是在(x1,y1)
->(x2,y2)
射線方向上的任意一點就可以了.- 當
P1
點剛好為切點時,畫出來的僅僅是一條弧線而不是線段加弧線. CGContextAddArcToPoint
功能比CGContextAddArc
強大,後者需要起始角度和終止角度.有些情況下,是很難算出這兩個角度的.
當利用上面的結論2時,畫出來的弧和使用CGContextAddArc
函式畫出的弧效果相當.如果三個點形成的角度為直角,那麼剛好是1/4圓弧.
遺憾的是,原始碼並沒有發揮該函式強大的一面,使用了CGContextAddLineToPoint
來畫蛇添足.將它們註釋掉,結果並沒有什麼不同,讀者可以繼續註釋後三條CGContextAddArcToPoint
,可以驗證該函式已經幫我們畫好線段了.
1 2 3 4 5 6 7 8 9 10 11 |
..... // Draw background float radius = (rect.size.height / 2) - 2; CGContextMoveToPoint(context, 2, rect.size.height/2); CGContextAddArcToPoint(context, 2, 2, radius + 2, 2, radius); //CGContextAddLineToPoint(context, rect.size.width - radius - 2, 2); CGContextAddArcToPoint(context, rect.size.width - 2, 2, rect.size.width - 2, rect.size.height / 2, radius); CGContextAddArcToPoint(context, rect.size.width - 2, rect.size.height - 2, rect.size.width - radius - 2, rect.size.height - 2, radius); //CGContextAddLineToPoint(context, radius + 2, rect.size.height - 2); CGContextAddArcToPoint(context, 2, rect.size.height - 2, 2, rect.size.height/2, radius); CGContextFillPath(context); |
畫完背景後,繼續進行了描邊,描邊的程式碼和上面幾乎一模一樣,作者之所以這樣做,是因為一個子路徑的fill
和stroke
效果是不能同時產生的,哪個先呼叫,就只會出現它產生的效果.如果原始碼是這樣寫的:
1 2 3 4 5 6 7 8 |
// Draw background ..... //CGContextAddLineToPoint(context, radius + 2, rect.size.height - 2); CGContextAddArcToPoint(context, 2, rect.size.height - 2, 2, rect.size.height/2, radius); // 先呼叫fill,就只有填充效果,如果調換CGContextFillPath和CGContextStrokePath的呼叫順序呢 // 那麼就只有描邊效果 CGContextFillPath(context); CGContextStrokePath(context); |
所以作者的做法是——又畫了一個路徑.
1 2 3 4 5 6 7 8 9 |
// Draw border CGContextMoveToPoint(context, 2, rect.size.height/2); CGContextAddArcToPoint(context, 2, 2, radius + 2, 2, radius); CGContextAddLineToPoint(context, rect.size.width - radius - 2, 2); CGContextAddArcToPoint(context, rect.size.width - 2, 2, rect.size.width - 2, rect.size.height / 2, radius); CGContextAddArcToPoint(context, rect.size.width - 2, rect.size.height - 2, rect.size.width - radius - 2, rect.size.height - 2, radius); CGContextAddLineToPoint(context, radius + 2, rect.size.height - 2); CGContextAddArcToPoint(context, 2, rect.size.height - 2, 2, rect.size.height/2, radius); CGContextStrokePath(context); |
事實上,可以使用CGContextDrawPath(CGContextRef c, CGPathDrawingMode mode)
函式解決這個問題.這樣就能省略很多的重複程式碼.
1 2 3 4 5 6 7 8 9 |
// Draw background ..... //CGContextAddLineToPoint(context, radius + 2, rect.size.height - 2); CGContextAddArcToPoint(context, 2, rect.size.height - 2, 2, rect.size.height/2, radius); // 這兩句被替換 // CGContextFillPath(context); // CGContextStrokePath(context); // kCGPathFillStroke引數告訴函式進行描邊和填充 CGContextDrawPath(context, kCGPathFillStroke); |
progress進度的更新
1.使用者更新progress
屬性
2.由於progress
被監聽,觸發KVO
,呼叫- observeValueForKeyPath:ofObject:change:context:
3.observeValueForKeyPath:ofObject:change:context:
中呼叫了setNeedsDisplay
,標識檢視為需要重新繪製.
4.呼叫drawRect:
重繪,進度條更新
七. 顯示與隱藏
顯示
顯示過程中,原始碼提供了給hud
“繫結”後臺任務的方法.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
- (void)showWhileExecuting:(SEL)method onTarget:(id)target withObject:(id)object animated:(BOOL)animated { methodForExecution = method; // 對於MRC來說,要保留target和object物件 // ARC會自動保留這兩個物件 // 不管是ARC還是MRC,都要注意引用迴圈的問題,因此下面有個-cleanUp方法用來釋放強引用 targetForExecution = MB_RETAIN(target); objectForExecution = MB_RETAIN(object); self.taskInProgress = YES; // detachNewThreadSelector是NSThread的類方法,開啟一個子執行緒執行任務,執行緒預設start [NSThread detachNewThreadSelector:@selector(launchExecution) toTarget:self withObject:nil]; // Show HUD view [self show:animated]; } - (void)showAnimated:(BOOL)animated whileExecutingBlock:(dispatch_block_t)block onQueue:(dispatch_queue_t)queue completionBlock:(MBProgressHUDCompletionBlock)completion { // 標記任務標識 self.taskInProgress = YES; // 將block先引用起來,在隱藏完之後執行block self.completionBlock = completion; // 在佇列上非同步執行,更新UI在主執行緒進行 dispatch_async(queue, ^(void) { block(); dispatch_async(dispatch_get_main_queue(), ^(void) { // 方法中有隱藏HUD這一更新UI的操作 [self cleanUp]; }); }); // 在任務執行的過程中進行動畫 [self show:animated]; } - (void)launchExecution { // 對於多執行緒操作建議把執行緒操作放到@autoreleasepool中 @autoreleasepool { // 忽略警告的編譯器指令 #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" // 究其原因,編譯期時編譯器並不知道methodForExecution是什麼 // ARC的記憶體管理是建立在規範的命名規則之上的,不知道方法名是什麼就不知道如何處理返回值 // 如果該方法有返回值,就不知道返回值是加入了自動釋放池的還是需要ARC釋放的物件 // 因此ARC不對返回值執行任何操作,如果返回值並不是加入自動釋放池的物件,這時就記憶體洩露了 [targetForExecution performSelector:methodForExecution withObject:objectForExecution]; #pragma clang diagnostic pop [self performSelectorOnMainThread:@selector(cleanUp) withObject:nil waitUntilDone:NO]; } } - (void)cleanUp { // 任務標識重置 taskInProgress = NO; #if !__has_feature(objc_arc) [targetForExecution release]; [objectForExecution release]; #else targetForExecution = nil; objectForExecution = nil; #endif [self hide:useAnimation]; } |
taskInProgress
的意思要結合graceTime
來看.graceTime
是為了防止hud只顯示很短時間(一閃而過)的情況,給使用者設定的一個屬性,如果任務在graceTime
內完成,將不會show
hud.所以graceTime
這個屬性離開了賦給hud的任務就沒意義了.因此,taskInProgress
用來標識是否帶有執行的任務.
1 2 3 4 5 6 |
- (void)handleGraceTimer:(NSTimer *)theTimer { // 如果沒有任務,設定了graceTime也沒有意義 if (taskInProgress) { [self showUsingAnimation:useAnimation]; } } |
值得注意的是,通過showWhileExecuting:onTarget:withObject:animated:
等方法時,會自動將taskInProgress
置為yes
,其他情況(任務所在的執行緒不是由hud內部所建立的)需手動設定這個屬性.
1 2 3 4 5 6 7 8 9 10 11 |
- (void)show:(BOOL)animated { ...... // 進行self.graceTime的延時之後,才呼叫handleGraceTimer:顯示hud // 如果沒到時間就執行完了,那麼完成任務呼叫的done方法會把taskInProgress設為NO,那麼就不會顯示hud了 if (self.graceTime > 0.0) { NSTimer *newGraceTimer = [NSTimer timerWithTimeInterval:self.graceTime target:self selector:@selector(handleGraceTimer:) userInfo:nil repeats:NO]; [[NSRunLoop currentRunLoop] addTimer:newGraceTimer forMode:NSRunLoopCommonModes]; self.graceTimer = newGraceTimer; } ...... } |
隱藏
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
- (void)hide:(BOOL)animated afterDelay:(NSTimeInterval)delay { [self performSelector:@selector(hideDelayed:) withObject:[NSNumber numberWithBool:animated] afterDelay:delay]; } - (void)hideDelayed:(NSNumber *)animated { [self hide:[animated boolValue]]; } - (void)hide:(BOOL)animated { NSAssert([NSThread isMainThread], @"MBProgressHUD needs to be accessed on the main thread."); useAnimation = animated; // 設定一個最短的顯示時間 // showStarted在顯示的時候被設定了,用當前的時間算出距離showStarted過了多少時間 // 得出interv.如果沒有達到minShowTimer所要求的時間,就開啟定時器等待到指定的最短時間 if (self.minShowTime > 0.0 && showStarted) { NSTimeInterval interv = [[NSDate date] timeIntervalSinceDate:showStarted]; if (interv |
八. 用法
用法示例程式碼來自該原始碼的github上.
1 2 3 4 5 6 7 8 9 |
// 使用MBProgressHUD最重要的準則是當要執行一個耗時任務時,不能放在主執行緒上影響UI的重新整理 // 正確地使用方式是在主執行緒上建立MBProgressHUD,然後在子執行緒上執行耗時操作,執行完再在主執行緒上重新整理UI [MBProgressHUD showHUDAddedTo:self.view animated:YES]; dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{ // Do something... dispatch_async(dispatch_get_main_queue(), ^{ [MBProgressHUD hideHUDForView:self.view animated:YES]; }); }); |
如果你想要對MBProgressHUD
進行額外的配置,需要將showHUDAddedTo:animated:
的返回的例項進行設定.
1 2 3 4 5 6 7 8 9 |
// 通過這個類方法生成的hud是加在傳進去的view上的 MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES]; hud.mode = MBProgressHUDModeAnnularDeterminate; hud.labelText = @"Loading"; [self doSomethingInBackgroundWithProgressCallback:^(float progress) { hud.progress = progress; } completionCallback:^{ [hud hide:YES]; }]; |
UI的更新應當總是在主執行緒上完成的,一些MBProgressHUD
上的屬性的setter方法考慮到了執行緒安全,可以被後臺執行緒安全地呼叫.這些setter包括setMode:, setCustomView:, setLabelText:, setLabelFont:, setDetailsLabelText:, setDetailsLabelFont: 和 setProgress:.
如果你需要在主執行緒上執行一個耗時的操作,你需要在執行前稍微延時一下,以使得在阻塞主執行緒之前,UIKit有足夠的時間去更新UI(即繪製HUD).
1 2 3 4 5 6 7 8 |
[MBProgressHUD showHUDAddedTo:self.view animated:YES]; // 如果上面那句話之後就要在主執行緒執行一個長時間操作,那麼要先延時一下讓HUD先畫好 // 不然在執行任務前沒畫出來就顯示不出來了 dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, 0.01 * NSEC_PER_SEC); dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ // Do something... [MBProgressHUD hideHUDForView:self.view animated:YES]; }); |