源码来源: https://github.com/jdg/MBProgressHUD
版本:0.9.1
MBProgressHUD是一个显示HUD窗口的第三方类库,用于在执行一些后台任务时,在程序中显示一个表示进度的loading视图和两 个可选的文本提示的HUD窗口。我想最多是应用在加载网络数据的时候。其实苹果官方自己有一个带有此功能的类UIProgressHUD,只不过它是私有 的,现在不让用。至于实际的效果,可以看看github上工程给出的几张图例(貌似我这经常无法单独打开图片,所以就不在这贴图片了),也可以运行一下 Demo。
具体用法我们就不多说了,参考github上的说明就能用得很顺的。本文主要还是从源码的角度来分析一下它的具体实现。
在分析实现代码之前,我们先来看看MBProgressHUD中定义的MBProgressHUDMode枚举。它用来表示HUD窗口的模式,即我们从效果图中看到的几种显示样式。其具体定义如下:
1 typedef enum { 2 // 使用UIActivityIndicatorView来显示进度,这是默认值 3 MBProgressHUDModeIndeterminate, 4 // 使用一个圆形饼图来作为进度视图 5 MBProgressHUDModeDeterminate, 6 // 使用一个水平进度条 7 MBProgressHUDModeDeterminateHorizontalBar, 8 // 使用圆环作为进度条 9 MBProgressHUDModeAnnularDeterminate, 10 // 显示一个自定义视图,通过这种方式,可以显示一个正确或错误的提示图 11 MBProgressHUDModeCustomView, 12 // 只显示文本 13 MBProgressHUDModeText 14 } MBProgressHUDMode;
我们先来了解一下MBProgressHUD的基本组成。一个MBProgressHUD视图主要由四个部分组成:
为了让我们更好地自定义这几个部分,MBProgressHUD还提供了一些属性,我们简单了解一下:
1 // 背景框的透明度,默认值是0.8 2 @property (assign) float opacity; 3 // 背景框的颜色 4 // 需要注意的是如果设置了这个属性,则opacity属性会失效,即不会有半透明效果 5 @property (MB_STRONG) UIColor *color; 6 // 背景框的圆角半径。默认值是10.0 7 @property (assign) float cornerRadius; 8 // 标题文本的字体及颜色 9 @property (MB_STRONG) UIFont* labelFont; 10 @property (MB_STRONG) UIColor* labelColor; 11 // 详情文本的字体及颜色 12 @property (MB_STRONG) UIFont* detailsLabelFont; 13 @property (MB_STRONG) UIColor* detailsLabelColor; 14 // 菊花的颜色,默认是白色 15 @property (MB_STRONG) UIColor *activityIndicatorColor;
另外还有一个比较有意思的属性是dimBackground,用于为HUD窗口的视图区域覆盖上一层径向渐变(radial gradient)层,其定义如下:
@property (assign) BOOL dimBackground;
让我们来看看通过它,MBProgressHUD都做了些什么。代码如下:
1 - (void)drawRect:(CGRect)rect { 2 ... 3 if (self.dimBackground) { 4 //Gradient colours 5 size_t gradLocationsNum = 2; 6 CGFloat gradLocations[2] = {0.0f, 1.0f}; 7 CGFloat gradColors[8] = {0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.75f}; 8 CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); 9 CGGradientRef gradient = CGGradientCreateWithColorComponents(colorSpace, gradColors, gradLocations, gradLocationsNum); 10 CGColorSpaceRelease(colorSpace); 11 //Gradient center 12 CGPoint gradCenter= CGPointMake(self.bounds.size.width/2, self.bounds.size.height/2); 13 //Gradient radius 14 float gradRadius = MIN(self.bounds.size.width , self.bounds.size.height) ; 15 // 由中心向四周绘制渐变 16 CGContextDrawRadialGradient (context, gradient, gradCenter, 17 0, gradCenter, gradRadius, 18 kCGGradientDrawsAfterEndLocation); 19 CGGradientRelease(gradient); 20 } 21 ... 22 }
除了继承自UIView的-initWithFrame:初始化方法,MBProgressHUD还为我们提供了两个初始化方法,如下所示:
1 - (id)initWithWindow:(UIWindow *)window; 2 - (id)initWithView:(UIView *)view;
MBProgressHUD提供了几个属性,可以让我们控制HUD的布局,这些属性主要有以下几个:
1 // HUD相对于父视图中心点的x轴偏移量和y轴偏移量 2 @property (assign) float xOffset; 3 @property (assign) float yOffset; 4 // HUD各元素与HUD边缘的间距 5 @property (assign) float margin; 6 // HUD背景框的最小大小 7 @property (assign) CGSize minSize; 8 // HUD的实际大小 9 @property (atomic, assign, readonly) CGSize size; 10 // 是否强制HUD背景框宽高相等 11 @property (assign, getter = isSquare) BOOL square;
需要注意的是,MBProgressHUD视图会充满其父视图的frame内,为此,在MBProgressHUD的layoutSubviews方法中,还专门做了处理,如下代码所示:
1 - (void)layoutSubviews { 2 [super layoutSubviews]; 3 // Entirely cover the parent view 4 UIView *parent = self.superview; 5 if (parent) { 6 self.frame = parent.bounds; 7 } 8 ... 9 }
在布局的过程中,会先根据我们要显示的视图计算出容纳这些视图所需要的总的宽度和高度。当然,会设置一个最大值。我们截取其中一段来看看:
1 CGRect bounds = self.bounds; 2 ... 3 CGFloat remainingHeight = bounds.size.height - totalSize.height - kPadding - 4 * margin; 4 CGSize maxSize = CGSizeMake(maxWidth, remainingHeight); 5 CGSize detailsLabelSize = MB_MULTILINE_TEXTSIZE(detailsLabel.text, detailsLabel.font, maxSize, detailsLabel.lineBreakMode); 6 totalSize.width = MAX(totalSize.width, detailsLabelSize.width); 7 totalSize.height += detailsLabelSize.height; 8 if (detailsLabelSize.height > 0.f && (indicatorF.size.height > 0.f || labelSize.height > 0.f)) { 9 totalSize.height += kPadding; 10 } 11 totalSize.width += 2 * margin; 12 totalSize.height += 2 * margin;
在上面的布局代码中,主要是处理了loading动画视图、标题文本框和详情文本框,而HUD背景框主要是在drawRect:中来绘制的。背景框的绘制代码如下:
1 // Center HUD 2 CGRect allRect = self.bounds; 3 // Draw rounded HUD backgroud rect 4 CGRect boxRect = CGRectMake(round((allRect.size.width - size.width) / 2) + self.xOffset, 5 round((allRect.size.height - size.height) / 2) + self.yOffset, size.width, size.height); 6 float radius = self.cornerRadius; 7 CGContextBeginPath(context); 8 CGContextMoveToPoint(context, CGRectGetMinX(boxRect) + radius, CGRectGetMinY(boxRect)); 9 CGContextAddArc(context, CGRectGetMaxX(boxRect) - radius, CGRectGetMinY(boxRect) + radius, radius, 3 * (float)M_PI / 2, 0, 0); 10 CGContextAddArc(context, CGRectGetMaxX(boxRect) - radius, CGRectGetMaxY(boxRect) - radius, radius, 0, (float)M_PI / 2, 0); 11 CGContextAddArc(context, CGRectGetMinX(boxRect) + radius, CGRectGetMaxY(boxRect) - radius, radius, (float)M_PI / 2, (float)M_PI, 0); 12 CGContextAddArc(context, CGRectGetMinX(boxRect) + radius, CGRectGetMinY(boxRect) + radius, radius, (float)M_PI, 3 * (float)M_PI / 2, 0); 13 CGContextClosePath(context); 14 CGContextFillPath(context);
我们上面讲过MBProgressHUD提供了几种窗口模式,这几种模式的主要区别在于loading动画视图的展示。默认情况下,使用的是菊 花(MBProgressHUDModeIndeterminate)。我们可以通过设置以下属性,来改变loading动画视图:
1 @property (assign) MBProgressHUDMode mode;
对于其它几种模式,MBProgressHUD专门我们提供了几个视图类。如果是进度条模式 (MBProgressHUDModeDeterminateHorizontalBar),则使用的是MBBarProgressView类;如果是饼 图模式(MBProgressHUDModeDeterminate)或环形模式 (MBProgressHUDModeAnnularDeterminate),则使用的是MBRoundProgressView类。上面这两个类的主 要操作就是在drawRect:中根据一些进度参数来绘制形状,大家可以自己详细看一下。
当然,我们还可以自定义loading动画视图,此时选择的模式是MBProgressHUDModeCustomView。或者不显示loading动画视图,而只显示文本框(MBProgressHUDModeText)。
具体显示哪一种loading动画视图,是在-updateIndicators方法中来处理的,其实现如下所示:
1 - (void)updateIndicators { 2 BOOL isActivityIndicator = [indicator isKindOfClass:[UIActivityIndicatorView class]]; 3 BOOL isRoundIndicator = [indicator isKindOfClass:[MBRoundProgressView class]]; 4 if (mode == MBProgressHUDModeIndeterminate) { 5 ... 6 } 7 else if (mode == MBProgressHUDModeDeterminateHorizontalBar) { 8 // Update to bar determinate indicator 9 [indicator removeFromSuperview]; 10 self.indicator = MB_AUTORELEASE([[MBBarProgressView alloc] init]); 11 [self addSubview:indicator]; 12 } 13 else if (mode == MBProgressHUDModeDeterminate || mode == MBProgressHUDModeAnnularDeterminate) { 14 if (!isRoundIndicator) { 15 ... 16 } 17 if (mode == MBProgressHUDModeAnnularDeterminate) { 18 [(MBRoundProgressView *)indicator setAnnular:YES]; 19 } 20 } 21 else if (mode == MBProgressHUDModeCustomView && customView != indicator) { 22 ... 23 } else if (mode == MBProgressHUDModeText) { 24 ... 25 } 26 }
MBRoundProgressView为我们提供了丰富的显示与隐藏HUD窗口的。在分析这些方法之前,我们先来看看MBProgressHUD为显示与隐藏提供的一些属性:
1 // HUD显示和隐藏的动画类型 2 @property (assign) MBProgressHUDAnimation animationType; 3 // HUD显示的最短时间。设置这个值是为了避免HUD显示后立即被隐藏。默认值为0 4 @property (assign) float minShowTime; 5 // 这个属性设置了一个宽限期,它是在没有显示HUD窗口前被调用方法可能运行的时间。 6 // 如果被调用方法在宽限期内执行完,则HUD不会被显示。 7 // 这主要是为了避免在执行很短的任务时,去显示一个HUD窗口。 8 // 默认值是0。只有当任务状态是已知时,才支持宽限期。具体我们看实现代码。 9 @property (assign) float graceTime; 10 // 这是一个标识位,标明执行的操作正在处理中。这个属性是配合graceTime使用的。 11 // 如果没有设置graceTime,则这个标识是没有太大意义的。在使用showWhileExecuting:onTarget:withObject:animated:方法时, 12 // 会自动去设置这个属性为YES,其它情况下都需要我们自己手动设置。 13 @property (assign) BOOL taskInProgress; 14 // 隐藏时是否将HUD从父视图中移除,默认是NO。 15 @property (assign) BOOL removeFromSuperViewOnHide; 16 // 进度指示器,从0.0到1.0,默认值为0.0 17 @property (assign) float progress; 18 // 在HUD被隐藏后的回调 19 @property (copy) MBProgressHUDCompletionBlock completionBlock;
以上这些属性都还好理解,可能需要注意的就是graceTime和taskInProgress的配合使用。在下面我们将会看看这两个属性的用法。
对于显示操作,最基本的就是-show:方法(其它几个显示方法都会调用该方法来显示HUD窗口),我们先来看看它的实现:
1 - (void)show:(BOOL)animated { 2 useAnimation = animated; 3 // If the grace time is set postpone the HUD display 4 if (self.graceTime > 0.0) { 5 self.graceTimer = [NSTimer scheduledTimerWithTimeInterval:self.graceTime target:self 6 selector:@selector(handleGraceTimer:) userInfo:nil repeats:NO]; 7 } 8 // ... otherwise show the HUD imediately 9 else { 10 [self showUsingAnimation:useAnimation]; 11 } 12 }
可以看到,如果我们没有设置graceTime属性,则会立即显示HUD;而如果设置了graceTime,则会创建一个定时器,并让显示操作延迟到graceTime所设定的时间再执行,而-handleGraceTimer:实现如下:
1 - (void)handleGraceTimer:(NSTimer *)theTimer { 2 // Show the HUD only if the task is still running 3 if (taskInProgress) { 4 [self showUsingAnimation:useAnimation]; 5 } 6 }
除了-show:方法以外,MBProgressHUD还为我们提供了一组显示方法,可以让我们在显示HUD的同时,执行一些后台任务,我们在 此主要介绍两个。其中一个是-showWhileExecuting:onTarget:withObject:animated:,它是基于 target-action方式的调用,在执行一个后台任务时显示HUD,等后台任务执行完成后再隐藏HUD,具体实现如下所示:
1 - (void)showWhileExecuting:(SEL)method onTarget:(id)target withObject:(id)object animated:(BOOL)animated { 2 methodForExecution = method; 3 targetForExecution = MB_RETAIN(target); 4 objectForExecution = MB_RETAIN(object); 5 // Launch execution in new thread 6 self.taskInProgress = YES; 7 [NSThread detachNewThreadSelector:@selector(launchExecution) toTarget:self withObject:nil]; 8 // Show HUD view 9 [self show:animated]; 10 } 11 - (void)launchExecution { 12 @autoreleasepool { 13 #pragma clang diagnostic push 14 #pragma clang diagnostic ignored "-Warc-performSelector-leaks" 15 // Start executing the requested task 16 [targetForExecution performSelector:methodForExecution withObject:objectForExecution]; 17 #pragma clang diagnostic pop 18 // Task completed, update view in main thread (note: view operations should 19 // be done only in the main thread) 20 [self performSelectorOnMainThread:@selector(cleanUp) withObject:nil waitUntilDone:NO]; 21 } 22 }
而在异步调用方法-launchExecution中,线程首先是维护了自己的一个@autoreleasepool,所以在我们自己的方法 中,就不需要再去维护一个@autoreleasepool了。之后是去执行我们的任务,在任务完成之后,再回去主线程去执行清理操作,并隐藏HUD窗 口。
另一个显示方法是-showAnimated:whileExecutingBlock:onQueue:completionBlock:, 它是基于GCD的调用,当block中的任务在指定的队列中执行时,显示HUD窗口,任务完成之后执行completionBlock操作,最后隐藏 HUD窗口。我们来看看它的具体实现:
1 - (void)showAnimated:(BOOL)animated whileExecutingBlock:(dispatch_block_t)block onQueue:(dispatch_queue_t)queue 2 completionBlock:(MBProgressHUDCompletionBlock)completion { 3 self.taskInProgress = YES; 4 self.completionBlock = completion; 5 dispatch_async(queue, ^(void) { 6 block(); 7 dispatch_async(dispatch_get_main_queue(), ^(void) { 8 [self cleanUp]; 9 }); 10 }); 11 [self show:animated]; 12 }
对于HUD的隐藏,MBProgressHUD提供了两个方法,一个是-hide:,另一个是-hide:afterDelay:,后者基于前者,所以我们主要来看看-hide:的实现:
1 - (void)hide:(BOOL)animated { 2 useAnimation = animated; 3 // If the minShow time is set, calculate how long the hud was shown, 4 // and pospone the hiding operation if necessary 5 if (self.minShowTime > 0.0 && showStarted) { 6 NSTimeInterval interv = [[NSDate date] timeIntervalSinceDate:showStarted]; 7 if (interv < self.minShowTime) { 8 self.minShowTimer = [NSTimer scheduledTimerWithTimeInterval:(self.minShowTime - interv) target:self 9 selector:@selector(handleMinShowTimer:) userInfo:nil repeats:NO]; 10 return; 11 } 12 } 13 // ... otherwise hide the HUD immediately 14 [self hideUsingAnimation:useAnimation]; 15 }
隐藏的实际操作主要是去做了些清理操作,包括根据设定的removeFromSuperViewOnHide值来执行是否从父视图移除HUD窗 口,以及执行completionBlock操作,还有就是执行代理的hudWasHidden:方法。这些操作是在私有方法-done里面执行的,实现如下:
1 - (void)done { 2 [NSObject cancelPreviousPerformRequestsWithTarget:self]; 3 isFinished = YES; 4 self.alpha = 0.0f; 5 if (removeFromSuperViewOnHide) { 6 [self removeFromSuperview]; 7 } 8 #if NS_BLOCKS_AVAILABLE 9 if (self.completionBlock) { 10 self.completionBlock(); 11 self.completionBlock = NULL; 12 } 13 #endif 14 if ([delegate respondsToSelector:@selector(hudWasHidden:)]) { 15 [delegate performSelector:@selector(hudWasHidden:) withObject:self]; 16 } 17 }
MBProgressHUD的一些主要的代码差不多已经分析完了,最后还有些边边角角的地方,一起来看看。
除了上面描述的实例方法之外,MBProgressHUD还为我们提供了几个便捷显示和隐藏HUD窗口的方法,如下所示:
1 + (MB_INSTANCETYPE)showHUDAddedTo:(UIView *)view animated:(BOOL)animated; 2 + (BOOL)hideHUDForView:(UIView *)view animated:(BOOL)animated; 3 + (NSUInteger)hideAllHUDsForView:(UIView *)view animated:(BOOL)animated;
对于部分属性(主要是”外观”一节中针对菊花、标题文本框和详情文本框的几个属性值),为了在设置将这些属性时修改对应视图的属性,并没有直接为每个属性生成一个setter,而是通过KVO来监听这些属性值的变化,再将这些值赋值给视图的对应属性,如下所示:
1 // 监听的属性数组 2 - (NSArray *)observableKeypaths { 3 return [NSArray arrayWithObjects:@"mode", @"customView", @"labelText", @"labelFont", @"labelColor", ..., nil]; 4 } 5 // 注册KVO 6 - (void)registerForKVO { 7 for (NSString *keyPath in [self observableKeypaths]) { 8 [self addObserver:self forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:NULL]; 9 } 10 } 11 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { 12 if (![NSThread isMainThread]) { 13 [self performSelectorOnMainThread:@selector(updateUIForKeypath:) withObject:keyPath waitUntilDone:NO]; 14 } else { 15 [self updateUIForKeypath:keyPath]; 16 } 17 } 18 - (void)updateUIForKeypath:(NSString *)keyPath { 19 if ([keyPath isEqualToString:@"mode"] || [keyPath isEqualToString:@"customView"] || 20 [keyPath isEqualToString:@"activityIndicatorColor"]) { 21 [self updateIndicators]; 22 } else if ([keyPath isEqualToString:@"labelText"]) { 23 label.text = self.labelText; 24 } 25 ... 26 [self setNeedsLayout]; 27 [self setNeedsDisplay]; 28 }
MBProgressHUD还为我们提供了一个代理MBProgressHUDDelegate,这个代理中只提供了一个方法,即:
1 - (void)hudWasHidden:(MBProgressHUD *)hud;
这个代理方法是在隐藏HUD窗口后调用,如果此时我们需要在我们自己的实现中执行某些操作,则可以实现这个方法。
MBProgressHUD为我们提供了一个HUD窗口的很好的实现,不过个人在使用过程中,觉得它给我们提供的交互功能太少。其代理只提供了 一个-hudWasHidden:方法,而且我们也无法通过点击HUD来执行一些操作。在现实的需求中,可能存在这种情况:比如一个网络操作,在发送请求 等待响应的过程中,我们会显示一个HUD窗口以显示一个loading框。但如果我们想在等待响应的过程中,在当前视图中取消这个网络请求,就没有相应的 处理方式,MBProgressHUD没有为我们提供这样的交互操作。当然这时候,我们可以根据自己的需求来修改源码。
与MBProgressHUD类似,SVProgressHUD类库也为我们提供了在视图中显示一个HUD窗口的功能。两者的基本思路是差不多的,差别更多的是在实现细节上。相对于MBProgressHUD来说,SVProgressHUD的实现有以下几点不同:
SVProgressHUD的实现细节还未详细去看,有兴趣的读者可以去研究一下。这两个HUD类库各有优点,大家在使用时,可根据自己的需要和喜好来选择。
总体来说,MBProgressHUD的代码相对朴实,简单易懂,没有什么花哨难懂的东西。就技术点而言,也没有太多复杂的技术,都是我们常用的一些东西。就使用而言,也是挺方便的,参考一下github上的使用指南就能很快上手。