前言
作為初學(xué)者,想要快速提高自己的水平,閱讀一些優(yōu)秀的第三方源代碼是一個非常好的途徑.通過看別人的代碼,可以學(xué)習(xí)不一樣的編程思路,了解一些沒有接觸過的類和方法. MBProgressHUD是一個非常受歡迎的第三方庫,其用法簡單,代碼樸實易懂,涉及的知識點廣而不深奧,是非常適合初學(xué)者閱讀的一份源碼.
一. 模式
首先, MBProgressHUD
有以下幾種視圖模式.
typedef enum {
/** 默認模式,使用系統(tǒng)自帶的指示器 ,不能顯示進度,只能不停地轉(zhuǎn)呀轉(zhuǎn)*/
MBProgressHUDModeIndeterminate,
/** 用餅圖顯示進度 */
MBProgressHUDModeDeterminate,
/** 進度條 */
MBProgressHUDModeDeterminateHorizontalBar,
/** 圓環(huán) */
MBProgressHUDModeAnnularDeterminate,
/** 自定義視圖 */
MBProgressHUDModeCustomView,
/** 只顯示文字 */
MBProgressHUDModeText
} MBProgressHUDMode;
mode
屬性指定顯示模式
hud.mode = MBProgressHUDModeIndeterminate;
hud.mode = MBProgressHUDModeDeterminate;
hud.mode = MBProgressHUDModeDeterminateHorizontalBar;
hud.mode = MBProgressHUDModeAnnularDeterminate;
hud.mode = MBProgressHUDModeText;
hud.labelText = @"MBProgressHUDModeText";
二. 結(jié)構(gòu)
MBProgressHUD
由指示器,文本框,詳情文本框,背景框4個部分組成.
// 文本框和其相關(guān)屬性
@property (copy) NSString *labelText;
@property (MB_STRONG) UIFont* labelFont;
@property (MB_STRONG) UIColor* labelColor;
//詳情文本框和其相關(guān)屬性
@property (copy) NSString *detailsLabelText;
@property (MB_STRONG) UIFont* detailsLabelFont;
@property (MB_STRONG) UIColor* detailsLabelColor;
// 背景框的透明度,默認值是0.8
@property (assign) float opacity;
// 背景框的顏色, 如果設(shè)置了這個屬性,則opacity屬性會失效,即不會有半透明效果
@property (MB_STRONG) UIColor *color;
// 背景框的圓角半徑江解。默認值是10.0
@property (assign) float cornerRadius;
// 菊花的顏色,默認是白色
@property (MB_STRONG) UIColor *activityIndicatorColor;
三. 初始化方法
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
// 顯示隱藏時的動畫模式
self.animationType = MBProgressHUDAnimationFade;
// 默認指示器是菊花
self.mode = MBProgressHUDModeIndeterminate;
......
// 關(guān)閉繪制的"性能開關(guān)",如果alpha不為1,最好將opaque設(shè)為NO,讓繪圖系統(tǒng)優(yōu)化性能
self.opaque = NO;
// 使背景顏色為透明
self.backgroundColor = [UIColor clearColor];
// 即使用戶創(chuàng)建了一個hud,并調(diào)用了addSubview方法
// 沒有調(diào)用show也是不能顯示的.在這之前要使hud隱藏并且不能接受觸摸事件
// 透明度為0(小于等于0.01),相當于hidden,無法響應(yīng)觸摸事件
self.alpha = 0.0f;
rotationTransform = CGAffineTransformIdentity;
// 設(shè)置label和detailLabel
[self setupLabels];
// 設(shè)置指示器
[self updateIndicators];
}
return self;
}
至于opaque
這個屬性,著實讓我糾結(jié)了好一陣子,不過暫時先不糾結(jié)那么多,以蘋果官方文檔為參考:
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
.
self.animationType = MBProgressHUDAnimationFade;
動畫效果MBProgressHUDAnimation
是一個枚舉.
typedef NS_ENUM(NSInteger, MBProgressHUDAnimation) {
// 默認效果,只有透明度變化的動畫效果
MBProgressHUDAnimationFade,
// 透明度變化+形變效果,其中MBProgressHUDAnimationZoom和
// MBProgressHUDAnimationZoomOut的枚舉值都為1
MBProgressHUDAnimationZoom,
MBProgressHUDAnimationZoomOut = MBProgressHUDAnimationZoom,
MBProgressHUDAnimationZoomIn
};
動畫效果是在這兩個方法中實現(xiàn)的:
// 顯示HUD
- (void)showUsingAnimation:(BOOL)animated {
// Cancel any scheduled hideDelayed: calls
[NSObject cancelPreviousPerformRequestsWithTarget:self];
[self setNeedsDisplay];
// ZoomIn,ZoomOut分別理解為`拉近鏡頭`,`拉遠鏡頭`
// 因此MBProgressHUDAnimationZoomIn先把形變縮小到0.5倍,再恢復(fù)到原狀,產(chǎn)生放大效果
// 反之MBProgressHUDAnimationZoomOut先把形變放大到1.5倍,再恢復(fù)原狀,產(chǎn)生縮小效果
// 要注意的是,形變的是整個`MBProgressHUD`,而不是中間可視部分
if (animated && animationType == MBProgressHUDAnimationZoomIn) {
// 在初始化方法中, 已經(jīng)定義了rotationTransform = CGAffineTransformIdentity.
// CGAffineTransformIdentity也就是對view不進行變形,對view進行仿射變化總是原樣
// CGAffineTransformConcat是兩個矩陣相乘,與之等價的設(shè)置方式是:
// 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被設(shè)置為0.f,在該方法中完成0.f~1.f的動畫效果
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:0.30];
self.alpha = 1.0f;
// 從形變狀態(tài)回到初始狀態(tài)
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時,就會被當做全透明對待,全透明是接收不了觸摸事件的.
// 所以設(shè)置0.02防止hud在還沒結(jié)束動畫并調(diào)用done方法之前傳遞觸摸事件.
// 在完成的回調(diào)animationFinished:finished:context:才設(shè)為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:
中又調(diào)用[self setupLabels]
設(shè)置了兩個label
的相關(guān)初始化設(shè)置(除了frame
的設(shè)置--這應(yīng)該是在layoutSubviews
里面做的事情).然后開始設(shè)置指示器.
- (void)updateIndicators {
// 讀源碼的時候,類似這種局部變量直接忽略,等代碼用到它,我們再"懶加載"
BOOL isActivityIndicator = [indicator isKindOfClass:[UIActivityIndicatorView class]];
BOOL isRoundIndicator = [indicator isKindOfClass:[MBRoundProgressView class]];
// 如果模式是MBProgressHUDModeIndeterminate,將使用系統(tǒng)自帶的菊花系列指示器
if (mode == MBProgressHUDModeIndeterminate) {
// 再看回最上面的兩條語句
// 初始化的時候進來,indicator是空的,對空對象發(fā)送消息返回的布爾值是NO
// 因為在初始化完畢后,用戶可能會設(shè)置mode屬性,那時還會進入這個方法,所以這兩個布爾變量除了第一次以外是有用的
if (!isActivityIndicator) {
// 默認第一次會進入到這里,對nil發(fā)送消息不會發(fā)生什么事
// 為什么要removeFromSuperview呢,因為這方法并不會只進入一次
// 不排除有些情況下先改變了mode到其他模式,之后又改回來了,這時候如果不移除
// MBProgressHUD就會殘留子控件在subviews里,雖然界面并不會顯示它
[indicator removeFromSuperview];
// 使用系統(tǒng)自帶的巨大白色菊花
// 系統(tǒng)菊花有三種
//typedef NS_ENUM(NSInteger, UIActivityIndicatorViewStyle) {
// UIActivityIndicatorViewStyleWhiteLarge, // 大又白
// UIActivityIndicatorViewStyleWhite, // 小白
// UIActivityIndicatorViewStyleGray, // 小灰
//};
self.indicator = MB_AUTORELEASE([[UIActivityIndicatorView alloc]
initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge]);
[(UIActivityIndicatorView *)indicator startAnimating];
[self addSubview:indicator];
}
// 系統(tǒng)菊花能設(shè)置顏色是從iOS5開始(NS_AVAILABLE_IOS(5_0)),這里用宏對手機版本進行了判斷
#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 50000
[(UIActivityIndicatorView *)indicator setColor:self.activityIndicatorColor];
#endif
}
// 源碼實現(xiàn)了兩種自定義視圖
// 一種是MBBarProgressView(進度條),另一種是MBRoundProgressView(圓餅or圓環(huán))
else if (mode == MBProgressHUDModeDeterminateHorizontalBar) {
// 進度條樣式
[indicator removeFromSuperview];
self.indicator = MB_AUTORELEASE([[MBBarProgressView alloc] init]);
[self addSubview:indicator];
}
else if (mode == MBProgressHUDModeDeterminate || mode == MBProgressHUDModeAnnularDeterminate) {
// 這兩種mode都產(chǎn)生MBRoundProgressView視圖,MBRoundProgressView又分兩種樣式
// 如果你設(shè)置了mode為MBProgressHUDModeDeterminate,那么流程是這樣子的
// 1)alloc init先生成系統(tǒng)的MBProgressHUDModeIndeterminate模式->
// 2)設(shè)置了mode為餅圖,觸發(fā)KVO,又進入了updateIndicators方法->
// 3)由于isRoundIndicator是No,產(chǎn)生餅狀圖
// 如果設(shè)置了MBProgressHUDModeAnnularDeterminate,那么步驟比它多了一步,
// 1)alloc init先生成系統(tǒng)的MBProgressHUDModeIndeterminate模式->
// 2)設(shè)置了mode為圓環(huán),觸發(fā)KVO,又進入了updateIndicators方法->
// 3)由于isRoundIndicator是No,產(chǎn)生餅狀圖->
// 4)設(shè)置[(MBRoundProgressView *)indicator setAnnular:YES]觸發(fā)MBRoundProgressView的
// KVO進行重繪視圖產(chǎn)生圓環(huán)圖
if (!isRoundIndicator) {
// 個人認為這個isRoundIndicator變量純屬多余
// isRoundIndicator為Yes的情況只有從MBProgressHUDModeDeterminate換成MBProgressHUDModeAnnularDeterminate
// 或者MBProgressHUDModeAnnularDeterminate換成MBProgressHUDModeDeterminate
// 而實際上這兩種切換方式產(chǎn)生的視圖都是圓環(huán),這是由于沒有讓annular設(shè)置成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
初始化時,設(shè)置完指示器就開始注冊KVO和通知.
.....
[self registerForKVO];
[self registerForNotifications];
.....
具體代碼實現(xiàn):
// 注冊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];
}
}
// 觸發(fā)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"]) {
// 除了系統(tǒng)指示器和自定義視圖,MB給我們提供的三種形狀的指示器都帶有progress屬性
if ([indicator respondsToSelector:@selector(setProgress:)]) {
// 觸發(fā)該視圖的KVO更新指示器視圖
[(id)indicator setValue:@(progress) forKey:@"progress"];
}
// 繪制交給視圖內(nèi)部處理
return;
}
// 如果更改了label的字體,需要重新調(diào)用layoutSubviews
[self setNeedsLayout];
// 設(shè)置標記,在下一個周期調(diào)用drawRect:方法重繪
[self setNeedsDisplay];
}
六. 布局與繪制
布局
子控件的布局計算沒什么復(fù)雜的地方,為了方便理解,我畫了兩幅圖
- (void)layoutSubviews {
[super layoutSubviews];
// MBProgressHUD是一個充滿整個父控件的控件
// 使得父控件的交互完全被屏蔽
UIView *parent = self.superview;
if (parent) {
self.frame = parent.bounds;
}
CGRect bounds = self.bounds;
.......
// 如果用戶設(shè)置了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;
}
上圖藍色虛線部分
代表子控件們能夠展示的區(qū)域,其中寬度是被限制的,其中定義了maxWidth
讓3個子控件中的最大寬度都不得超過它.值得注意的是,源碼并沒設(shè)置最大高度,如果我們使用自定義的視圖,高度夠大就會使藍色虛線部分
的上下底超出屏幕范圍.某種程度上來講也是設(shè)計上的一種bug,但我認為作者肯定意識到了這點----label\detailLabel
中有很多文字導(dǎo)致?lián)Q行是很常見的情況,因此需要限制它的最大寬度,但沒人會使用一個非常大的指示器,所以通過額外的計算來考慮因為這種情況超出屏幕上下邊界是毫無必要的.
此外,綠色的label
被限制為只能顯示一行,黃色的detailLabel
通過下面的代碼來限制它不能超出屏幕上下.
// 計算出屏幕剩下的高度
// 其中減去了4個margin大小,保證了子空間和HUD的邊距,HUD和屏幕的距離
CGFloat remainingHeight = bounds.size.height - totalSize.height - kPadding - 4 * margin;
// 將文字內(nèi)容限制在這個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中比較重要的內(nèi)容.
- (void)drawRect:(CGRect)rect {
// 拿到當前的繪圖上下文
CGContextRef context = UIGraphicsGetCurrentContext();
UIGraphicsPushContext(context);
// 默認中間的HUD外是透明的,可以看到父控件,設(shè)置了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);
}
// 用戶有設(shè)置顏色就使用設(shè)置的顏色,沒有的話默認灰色
// 從下面代碼可以看出,自定義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));
// 依次畫出右上角、右下角,左下角,左上角的四分之一圓弧
// 注意,雖然沒有顯式地調(diào)用CGContextAddLineToPoint函數(shù)
// 但繪制圓弧時每一次的起點都會和上一次的終點連接,生成線段
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
,因為還有一半因為超出了繪圖區(qū)域而沒有被繪制.
為了防止繪制內(nèi)容的丟失,半徑radius
的計算是(self.bounds.size.width - lineWidth)/2
,而并不是self.bounds.size.width/2
.更不是(self.bounds.size.width -2*lineWidth)/2
,借助下圖理解:
// 圓環(huán)繪制
if (_annular) {
// iOS7.0以后的圓環(huán)描邊風(fēng)格變了,變成了2.f
// 7.0之前的還是5.f.主要是為了迎合扁平的風(fēng)格我覺得
BOOL isPreiOS7 = kCFCoreFoundationVersionNumber < kCFCoreFoundationVersionNumber_iOS_7_0;
CGFloat lineWidth = isPreiOS7 ? 5.f : 2.f;
......
CGFloat radius = (self.bounds.size.width - lineWidth)/2;
}
在圓餅的繪制過程中,圓餅外層的圓環(huán)是通過CGContextStrokeEllipseInRect(CGContextRef, CGRect)
進行描邊的,根據(jù)上面的結(jié)論,圓餅繪制區(qū)域(circleRect)和上下文提供的繪制區(qū)域(allRect)應(yīng)該寬高都相差1.f
就夠圓餅外層的圓環(huán)的正確繪制.作者在這里用了2.f
,實際上1.f
就夠了.
CGRect allRect = self.bounds;
CGRect circleRect = CGRectInset(allRect, 2.0f, 2.0f);
接下來是MBBarProgressView
的繪制.
MBBarProgressView
MBBarProgressView與MBRoundProgressView的繪制類似,都是使用Quartz2D進行繪圖.使用的都是很基礎(chǔ)很常用的API,所以閱讀難度并不大.唯一讓人困惑的可能是這個CGContextAddArcToPoint(CGContextRef c, CGFloat x1, CGFloat y1,CGFloat x2, CGFloat y2, CGFloat radius)
了,另一個畫弧的函數(shù)則簡單很多:CGContextAddArc(CGContextRef c, CGFloat x, CGFloat y, CGFloat radius, CGFloat startAngle, CGFloat endAngle, int clockwise)
.
結(jié)合下圖,我的理解方式是:P1
為繪圖的當前點,x1
,y1
, x2
, y2
表示了兩個定點.通過當前點P1
,點(x1,y1)
和(x2,y2)
,可以表示一個確定的角度,這時一個任意半徑的圓都能與圖中的兩條射線相切.不同半徑的圓,圓心角都不同,兩個切點之間的弧也不相同.舉個例子,我們拿不同半徑的球體去貼到兩面墻的相交處,兩個切點之間有段弧線,球越大弧越長,但是圓心角大小都是一樣的.控制圓心角大小由這三個點決定,能夠獲得的最大圓心角是90度.
兩個畫弧的函數(shù)差別有點大,CGContextAddArcToPoint
分為兩步:
- 從當前點
P1
開始,沿著(x1,y1)
方向畫線段. - 線段一直畫到
圓
與虛線相切的地方. - 這是圓被分成了兩段弧線,繪制短的那條(即圓心對著的那段弧).
我們還可以得到其他的結(jié)論:
-
(x2,y2)
的作用只是為了確定與另一條射線形成的角度,只要(x2,y2)
是在(x1,y1)
->(x2,y2)
射線方向上的任意一點就可以了. - 當
P1
點剛好為切點時,畫出來的僅僅是一條弧線而不是線段加弧線. -
CGContextAddArcToPoint
功能比CGContextAddArc
強大,后者需要起始角度和終止角度.有些情況下,是很難算出這兩個角度的.
當利用上面的結(jié)論2時,畫出來的弧和使用CGContextAddArc
函數(shù)畫出的弧效果相當.如果三個點形成的角度為直角,那么剛好是1/4圓弧.
遺憾的是,源碼并沒有發(fā)揮該函數(shù)強大的一面,使用了CGContextAddLineToPoint
來畫蛇添足.將它們注釋掉,結(jié)果并沒有什么不同,讀者可以繼續(xù)注釋后三條CGContextAddArcToPoint
,可以驗證該函數(shù)已經(jīng)幫我們畫好線段了.
.....
// 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);
畫完背景后,繼續(xù)進行了描邊,描邊的代碼和上面幾乎一模一樣,作者之所以這樣做,是因為一個子路徑的fill
和stroke
效果是不能同時產(chǎn)生的,哪個先調(diào)用,就只會出現(xiàn)它產(chǎn)生的效果.如果源碼是這樣寫的:
// Draw background
.....
//CGContextAddLineToPoint(context, radius + 2, rect.size.height - 2);
CGContextAddArcToPoint(context, 2, rect.size.height - 2, 2, rect.size.height/2, radius);
// 先調(diào)用fill,就只有填充效果,如果調(diào)換CGContextFillPath和CGContextStrokePath的調(diào)用順序呢
// 那么就只有描邊效果
CGContextFillPath(context);
CGContextStrokePath(context);
所以作者的做法是——又畫了一個路徑.
// 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)
函數(shù)解決這個問題.這樣就能省略很多的重復(fù)代碼.
// 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參數(shù)告訴函數(shù)進行描邊和填充
CGContextDrawPath(context, kCGPathFillStroke);
progress進度的更新
1.用戶更新progress
屬性
2.由于progress
被監(jiān)聽,觸發(fā)KVO
,調(diào)用- observeValueForKeyPath:ofObject:change:context:
3.observeValueForKeyPath:ofObject:change:context:
中調(diào)用了setNeedsDisplay
,標識視圖為需要重新繪制.
4.調(diào)用drawRect:
重繪,進度條更新
七. 顯示與隱藏
顯示
顯示過程中,源碼提供了給hud
"綁定"后臺任務(wù)的方法.
- (void)showWhileExecuting:(SEL)method onTarget:(id)target withObject:(id)object animated:(BOOL)animated {
methodForExecution = method;
// 對于MRC來說,要保留target和object對象
// ARC會自動保留這兩個對象
// 不管是ARC還是MRC,都要注意引用循環(huán)的問題,因此下面有個-cleanUp方法用來釋放強引用
targetForExecution = MB_RETAIN(target);
objectForExecution = MB_RETAIN(object);
self.taskInProgress = YES;
// detachNewThreadSelector是NSThread的類方法,開啟一個子線程執(zhí)行任務(wù),線程默認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 {
// 標記任務(wù)標識
self.taskInProgress = YES;
// 將block先引用起來,在隱藏完之后執(zhí)行block
self.completionBlock = completion;
// 在隊列上異步執(zhí)行,更新UI在主線程進行
dispatch_async(queue, ^(void) {
block();
dispatch_async(dispatch_get_main_queue(), ^(void) {
// 方法中有隱藏HUD這一更新UI的操作
[self cleanUp];
});
});
// 在任務(wù)執(zhí)行的過程中進行動畫
[self show:animated];
}
- (void)launchExecution {
// 對于多線程操作建議把線程操作放到@autoreleasepool中
@autoreleasepool {
// 忽略警告的編譯器指令
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
// 究其原因,編譯期時編譯器并不知道m(xù)ethodForExecution是什么
// ARC的內(nèi)存管理是建立在規(guī)范的命名規(guī)則之上的,不知道方法名是什么就不知道如何處理返回值
// 如果該方法有返回值,就不知道返回值是加入了自動釋放池的還是需要ARC釋放的對象
// 因此ARC不對返回值執(zhí)行任何操作,如果返回值并不是加入自動釋放池的對象,這時就內(nèi)存泄露了
[targetForExecution performSelector:methodForExecution withObject:objectForExecution];
#pragma clang diagnostic pop
[self performSelectorOnMainThread:@selector(cleanUp) withObject:nil waitUntilDone:NO];
}
}
- (void)cleanUp {
// 任務(wù)標識重置
taskInProgress = NO;
#if !__has_feature(objc_arc)
[targetForExecution release];
[objectForExecution release];
#else
targetForExecution = nil;
objectForExecution = nil;
#endif
[self hide:useAnimation];
}
taskInProgress
的意思要結(jié)合graceTime
來看.graceTime
是為了防止hud只顯示很短時間(一閃而過)的情況,給用戶設(shè)定的一個屬性,如果任務(wù)在graceTime
內(nèi)完成,將不會show
hud.所以graceTime
這個屬性離開了賦給hud的任務(wù)就沒意義了.因此,taskInProgress
用來標識是否帶有執(zhí)行的任務(wù).
- (void)handleGraceTimer:(NSTimer *)theTimer {
// 如果沒有任務(wù),設(shè)置了graceTime也沒有意義
if (taskInProgress) {
[self showUsingAnimation:useAnimation];
}
}
值得注意的是,通過showWhileExecuting:onTarget:withObject:animated:
等方法時,會自動將taskInProgress
置為yes
,其他情況(任務(wù)所在的線程不是由hud內(nèi)部所創(chuàng)建的)需手動設(shè)置這個屬性.
- (void)show:(BOOL)animated {
......
// 進行self.graceTime的延時之后,才調(diào)用handleGraceTimer:顯示hud
// 如果沒到時間就執(zhí)行完了,那么完成任務(wù)調(diào)用的done方法會把taskInProgress設(shè)為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;
}
......
}
隱藏
- (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;
// 設(shè)置一個最短的顯示時間
// showStarted在顯示的時候被設(shè)置了,用當前的時間算出距離showStarted過了多少時間
// 得出interv.如果沒有達到minShowTimer所要求的時間,就開啟定時器等待到指定的最短時間
if (self.minShowTime > 0.0 && showStarted) {
NSTimeInterval interv = [[NSDate date] timeIntervalSinceDate:showStarted];
if (interv < self.minShowTime) {
self.minShowTimer = [NSTimer scheduledTimerWithTimeInterval:(self.minShowTime - interv) target:self
selector:@selector(handleMinShowTimer:) userInfo:nil repeats:NO];
return;
}
}
// ... otherwise hide the HUD immediately
[self hideUsingAnimation:useAnimation];
}
八. 用法
用法示例代碼來自該源碼的github上.
// 使用MBProgressHUD最重要的準則是當要執(zhí)行一個耗時任務(wù)時,不能放在主線程上影響UI的刷新
// 正確地使用方式是在主線程上創(chuàng)建MBProgressHUD,然后在子線程上執(zhí)行耗時操作,執(zhí)行完再在主線程上刷新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:
的返回的實例進行設(shè)置.
// 通過這個類方法生成的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的更新應(yīng)當總是在主線程上完成的,一些MBProgressHUD
上的屬性的setter方法考慮到了線程安全,可以被后臺線程安全地調(diào)用.這些setter包括setMode:, setCustomView:, setLabelText:, setLabelFont:, setDetailsLabelText:, setDetailsLabelFont: 和 setProgress:.
如果你需要在主線程上執(zhí)行一個耗時的操作,你需要在執(zhí)行前稍微延時一下,以使得在阻塞主線程之前,UIKit有足夠的時間去更新UI(即繪制HUD).
[MBProgressHUD showHUDAddedTo:self.view animated:YES];
// 如果上面那句話之后就要在主線程執(zhí)行一個長時間操作,那么要先延時一下讓HUD先畫好
// 不然在執(zhí)行任務(wù)前沒畫出來就顯示不出來了
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];
});