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