本篇博文记录MBProgressHUD源码学习过程,从官方提供的Demo项目入手,一步步了解其代码结构,学习它使用的技术,体会做者的编程思想。编程
咱们先来看下MBProgressHUD的结构,查看其类的定义。
1.MBProgressHUD是UIView的子类。
2.属性:app
1. //代理,<MBProgressHUDDelegate>仅定义了一个方法:- (void)hudWasHidden:(MBProgressHUD *)hud;用于执行HUD隐藏以后的操做 @property (weak, nonatomic) id<MBProgressHUDDelegate> delegate; //执行HUD隐藏以后的操做的Block,目的同上 @property (copy, nullable) MBProgressHUDCompletionBlock completionBlock; 2. //延迟时间,若任务在graceTime到时以前就完成了,HUD再也不展现,即防止为短期任务显示HUD @property (assign, nonatomic) NSTimeInterval graceTime; //最短展现时间,防止HUD隐藏的过快 @property (assign, nonatomic) NSTimeInterval minShowTime; //配置HUD是否隐藏以后就从其superview上移除。默认NO @property (assign, nonatomic) BOOL removeFromSuperViewOnHide; 3. //指定进度条的样式,包括菊花、圆饼、环形、水平进度条、自定义样式和纯文本等 @property (assign, nonatomic) MBProgressHUDMode mode; //内容(label+indicator+customView)颜色 @property (strong, nonatomic, nullable) UIColor *contentColor; //显示和隐藏时的动画类型:Fade(淡入淡出)、Zoom(放大显示缩小隐藏)、ZoomIn、ZoomOut @property (assign, nonatomic) MBProgressHUDAnimation animationType; //内容框(bezelView)距离中心位置的偏移,例如CGPointMake(0.f, MBProgressMaxOffset),内容框会在底部居中 @property (assign, nonatomic) CGPoint offset; @property (assign, nonatomic) CGFloat margin;//各元素到HUD的边距 @property (assign, nonatomic) CGSize minSize;//内容框的最小尺寸 @property (assign, nonatomic, getter = isSquare) BOOL square;//强制HUD为方形 @property (assign, nonatomic, getter=areDefaultMotionEffectsEnabled) BOOL defaultMotionEffectsEnabled;//内容框(bezelView)是否受设备加速计的影响,默认YES 4. @property (assign, nonatomic) float progress;//进度 @property (strong, nonatomic, nullable) NSProgress *progressObject;//进度对象,用于更新进度条 5. //内容框,即展现实际内容(文本、indicator)的矩形框 @property (strong, nonatomic, readonly) MBBackgroundView *bezelView; //背景试图,会覆盖整个屏幕 @property (strong, nonatomic, readonly) MBBackgroundView *backgroundView; //自定义视图用于展现 @property (strong, nonatomic, nullable) UIView *customView; @property (strong, nonatomic, readonly) UILabel *label;//文本 @property (strong, nonatomic, readonly) UILabel *detailsLabel;//文本下面的详细文本 @property (strong, nonatomic, readonly) UIButton *button;//文本下面的action button
3.其余相关类async
(1) MBBackgroundViewide
UIVisualEffectView
和UIBlurEffect
实现的。(2) MBRoundProgressView函数
(3) MBBarProgressView布局
(4) MBProgressHUDRoundedButton学习
知识点:HUD中有个button属性以下:动画
/** * A button that is placed below the labels. Visible only if a target / action is added. */ @property (strong, nonatomic, readonly) UIButton *button;
注意它的注释Visible only if a target / action is added
。也就是说,只有给button添加事件以后,该按钮才会展现出来。这是如何作到的呢?那就是重写UIView的函数- (CGSize)intrinsicContentSize
:atom
- (CGSize)intrinsicContentSize { // Only show if we have associated control events if (self.allControlEvents == 0) return CGSizeZero; CGSize size = [super intrinsicContentSize]; // Add some side padding size.width += 20.f; return size; }
这个函数用来设置控件的内置尺寸。能够看到,经过判断allControlEvents
的个数来判断button上是否有事件,若是有事件,就在原来内置的尺寸上加20。spa
了解了MBProgressHUD的基本结构以后,接下来咱们就看看具体的功能是如何实现的。HUDDemo提供了15个样例,咱们选取纯文本、加载(菊花)、条状进度条和自定义视图进行分析,其余的样例与它们相似。
咱们先从最简单的纯文本开始。启动HUDDemo项目,点开MBHudDemoViewController.m
文件,找到函数- (void)textExample{…}
,这个函数就是显示纯文本的处理函数:
- (void)textExample { MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.navigationController.view animated:YES]; // Set the text mode to show only text. hud.mode = MBProgressHUDModeText; hud.label.text = NSLocalizedString(@"Message here!", @"HUD message title"); // Move to bottm center. hud.offset = CGPointMake(0.f, MBProgressMaxOffset); [hud hideAnimated:YES afterDelay:3.f]; }
① 进入到函数showHUDAddedTo:animated:
中查看MBProgressHUD实例的建立过程:
initWithView:
->initWithFrame:
->commonInit
使用self.navigationController.view
的bounds初始化HUD,而后在commonInit
里指定动画类型(Fade)、HUD模式(菊花)、间距(20)、内容颜色(黑色半透明)。除此以外,还设置HUD为彻底透明,背景色为clear,配置HUD的尺寸自动调整:
//保证上下间距比例不变、左右间距比例不变,即防止横竖屏切换时HUD位置错误 self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; //让HUD的各个子视图本身控制本身的透明度,使其不受HUD透明度的影响 self.layer.allowsGroupOpacity = NO;
[self setupViews]
在这个函数中真正执行子视图的建立工做。
背景视图(backgroundView)
为类MBBackgroundView
的实例。MBBackgroundView
实例默认会建立成白色半透明模糊效果,并覆盖全屏,但在本例中,建立完成以后会更改其style
为MBProgressHUDBackgroundStyleSolidColor
,并将背景色设置为透明(clear)。
内容框(bezelView)
同为类MBBackgroundView
实例,是实际展现内容的View(即中间的黑框),包含文本、indicator、进度条等。bezelView
会默认建立成白色半透明模糊效果,但frame为0。建立后会设置其边角半径为5。
知识点:做者为bezelView添加了MotionEffect,也就是说在bezelView显示的时候,它会根据手机的倾斜方向调整本身的位置!
- (void)updateBezelMotionEffects { #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000 || TARGET_OS_TV MBBackgroundView *bezelView = self.bezelView; if (![bezelView respondsToSelector:@selector(addMotionEffect:)]) return; if (self.defaultMotionEffectsEnabled) { CGFloat effectOffset = 10.f; UIInterpolatingMotionEffect *effectX = [[UIInterpolatingMotionEffect alloc] initWithKeyPath:@"center.x" type:UIInterpolatingMotionEffectTypeTiltAlongHorizontalAxis]; effectX.maximumRelativeValue = @(effectOffset); effectX.minimumRelativeValue = @(-effectOffset); UIInterpolatingMotionEffect *effectY = [[UIInterpolatingMotionEffect alloc] initWithKeyPath:@"center.y" type:UIInterpolatingMotionEffectTypeTiltAlongVerticalAxis]; effectY.maximumRelativeValue = @(effectOffset); effectY.minimumRelativeValue = @(-effectOffset); UIMotionEffectGroup *group = [[UIMotionEffectGroup alloc] init]; group.motionEffects = @[effectX, effectY]; [bezelView addMotionEffect:group]; } else { NSArray *effects = [bezelView motionEffects]; for (UIMotionEffect *effect in effects) { [bezelView removeMotionEffect:effect]; } } #endif }
label和detailsLabel
设置显示文字的label,其中detailsLabel容许多行。
button
为MBProgressHUDRoundedButton
的实例,做为HUD上的功能按钮,好比进度条下方能够显示一个"取消"按钮。
topSpacer和bottomSpacer
均为UIView
的实例,用于调节上下间距的辅助View。
设置label、detailsLabel及button的抗压系数,并添加到父视图上。
for (UIView *view in @[label, detailsLabel, button]) { view.translatesAutoresizingMaskIntoConstraints = NO;//本身手动管理约束 [view setContentCompressionResistancePriority:998.f forAxis:UILayoutConstraintAxisHorizontal];//设置水平抗压缩系数,值越大,越不容易被压缩 [view setContentCompressionResistancePriority:998.f forAxis:UILayoutConstraintAxisVertical];//设置垂直抗压缩系数,值越大,越不容易被压缩 [bezelView addSubview:view]; }
[self updateIndicators]
HUD的indicator
是UIView
的实例,用来记录HUD上显示的视图,进度条、加载图标(菊花)、自定义视图等都是用HUD的indicator
属性记录的。在函数- (void)updaetIndicators
中,根据HUD的mode值配置不一样的indicator。最后会调用[self setNeedsUpdateConstraints]
触发约束更新函数-(void)updateConstraints
来更新UI。
[self registerForNotifications]
注册通知,处理屏幕旋转的问题。
② 在建立完HUD以后,会调用[hud showAnimated:animated];
将HUD展现到屏幕上。事实上,虽然当前HUD已经在屏幕上了,但因为初始化HUD的时候bezelView的frame为0,用户看不到。
③ 配置HUD实例的属性
hud.mode = MBProgressHUDModeText;//设置hud只显示纯文本 hud.label.text = NSLocalizedString(@"Message here!", @"HUD message title");//设置文本内容 hud.offset = CGPointMake(0.f, MBProgressMaxOffset);//设置hud相对于中心位置的偏移
在mode的setter函数中会调用- (void)updateIndicators
,根据mode的新值从新配置indicator,而后调用- (void)setNeedsUpdateConstraints
触发-(void)updateConstraints
来更新UI。而在offset的setter函数中会直接调用- (void)setNeedsUpdateConstraints
触发-(void)updateConstraints
来更新UI。
④ 在函数- (void)updateConstraints
中更新布局:
NSLayoutConstraint
从新设置constraints//1.以屏幕中心为基准,应用offset。priority = 998 CGPoint offset = self.offset; NSMutableArray *centeringConstraints = [NSMutableArray array]; //x [centeringConstraints addObject:[NSLayoutConstraint constraintWithItem:bezel attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterX multiplier:1.f constant:offset.x]]; //y [centeringConstraints addObject:[NSLayoutConstraint constraintWithItem:bezel attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterY multiplier:1.f constant:offset.y]]; //为每一个constraints设置priority [self applyPriority:998.f toConstraints:centeringConstraints]; [self addConstraints:centeringConstraints]; //2.设置最小间距约束,priority = 999 NSMutableArray *sideConstraints = [NSMutableArray array]; [sideConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"|-(>=margin)-[bezel]-(>=margin)-|" options:0 metrics:metrics views:NSDictionaryOfVariableBindings(bezel)]]; [sideConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(>=margin)-[bezel]-(>=margin)-|" options:0 metrics:metrics views:NSDictionaryOfVariableBindings(bezel)]]; [self applyPriority:999.f toConstraints:sideConstraints]; [self addConstraints:sideConstraints]; //3.bezel的最小尺寸约束 priority = 997 CGSize minimumSize = self.minSize; if (!CGSizeEqualToSize(minimumSize, CGSizeZero)) { NSMutableArray *minSizeConstraints = [NSMutableArray array]; [minSizeConstraints addObject:[NSLayoutConstraint constraintWithItem:bezel attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationGreaterThanOrEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.f constant:minimumSize.width]]; [minSizeConstraints addObject:[NSLayoutConstraint constraintWithItem:bezel attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationGreaterThanOrEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.f constant:minimumSize.height]]; [self applyPriority:997.f toConstraints:minSizeConstraints]; [bezelConstraints addObjectsFromArray:minSizeConstraints]; } //4.方形约束 priority=997 if (self.square) { NSLayoutConstraint *square = [NSLayoutConstraint constraintWithItem:bezel attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:bezel attribute:NSLayoutAttributeWidth multiplier:1.f constant:0]; square.priority = 997.f; [bezelConstraints addObject:square]; } //5.根据margin和设置上下spacer的间距约束 [topSpacer addConstraint:[NSLayoutConstraint constraintWithItem:topSpacer attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationGreaterThanOrEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.f constant:margin]]; [bottomSpacer addConstraint:[NSLayoutConstraint constraintWithItem:bottomSpacer attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationGreaterThanOrEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.f constant:margin]]; [bezelConstraints addObject:[NSLayoutConstraint constraintWithItem:topSpacer attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:bottomSpacer attribute:NSLayoutAttributeHeight multiplier:1.f constant:0.f]]; //6.设置bezel子视图(topSpacer、label、detailLabel、button、bottomSpacer)的约束 [subviews enumerateObjectsUsingBlock:^(UIView *view, NSUInteger idx, BOOL *stop) { // Center in bezel [bezelConstraints addObject:[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:bezel attribute:NSLayoutAttributeCenterX multiplier:1.f constant:0.f]]; // Ensure the minimum edge margin is kept [bezelConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"|-(>=margin)-[view]-(>=margin)-|" options:0 metrics:metrics views:NSDictionaryOfVariableBindings(view)]]; // Element spacing if (idx == 0) { // First, ensure spacing to bezel edge [bezelConstraints addObject:[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:bezel attribute:NSLayoutAttributeTop multiplier:1.f constant:0.f]]; } else if (idx == subviews.count - 1) { // Last, ensure spacing to bezel edge [bezelConstraints addObject:[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:bezel attribute:NSLayoutAttributeBottom multiplier:1.f constant:0.f]]; } if (idx > 0) { // Has previous NSLayoutConstraint *padding = [NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:subviews[idx - 1] attribute:NSLayoutAttributeBottom multiplier:1.f constant:0.f]; [bezelConstraints addObject:padding]; [paddingConstraints addObject:padding]; } }]; [bezel addConstraints:bezelConstraints]; self.bezelConstraints = bezelConstraints; self.paddingConstraints = [paddingConstraints copy]; [self updatePaddingConstraints];//在该函数里,根据子视图的可视性(hidden),设置子视图的上下间距(为4)
经过上面的priority能够知道优先级:最小间距约束>bezel的偏移约束>bezel最小尺寸约束=方形约束。所以,若是你设置了hud.square = YES
,可是实际bezel并无变为方形,则极可能是由于上面的这几个约束之间存在冲突,系统采用了高优先级的约束而忽略了square约束。不信你能够把square优先级改成1000试试看:)
知识点:这里出现了一个宏NSDictionaryOfVariableBindings
,它能够用来方便的建立NSDictionary:
UIView *view1 = [UIView new]; UIView *view2 = [UIView new]; NSDictionary *dict = NSDictionaryOfVariableBindings(view1,view2);//{@"view1":view1,@"view2":view2}
总结:
至此,咱们来总结下纯文本HUD的整个建立及显示流程:
- 调用
[MBProgressHUD showHUDAddedTo:self.navigationController.view animated:YES]
建立HUD实例:包括配置属性默认值(动画类型、HUD样式、间距、内容颜色等),初始化view(backgroundView、bezelView、label、detailLabel、button、topSpacer、bottomSpacer),且会默认建立一个indicator。以后hud会显示在屏幕上,但因为约束未触发,所以用户看不到。hud.mode = MBProgressHUDModeText
。HUD会根据mode的值去隐藏indicator,并更新约束。hud.label.text = NSLocalizedString(@"Message here!", @"HUD message title")
设置要显示的文字。hud.offset = CGPointMake(0.f, MBProgressMaxOffset)
设置bezelView的偏移属性:让其显示在最底部。并更新约束。[hud hideAnimated:YES afterDelay:3.f]
设置一个延迟timer,在3s以后隐藏hud。隐藏以后调用completionBlock
和代理方法- (void)hudWasHidden:(MBProgressHUD *)hud;
。
加载样式表现为一个旋转的菊花,底部也可包含"Loading…"等字样提示。咱们以包含"Loading…"字样的HUD为例剖析其内部原理。代码以下:
- (void)labelExample { MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.navigationController.view animated:YES]; // Set the label text. hud.label.text = NSLocalizedString(@"Loading...", @"HUD loading title"); // You can also adjust other label properties if needed. // hud.label.font = [UIFont italicSystemFontOfSize:16.f]; dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{ [self doSomeWork]; dispatch_async(dispatch_get_main_queue(), ^{ [hud hideAnimated:YES]; }); }); }
MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.navigationController.view animated:YES];
建立HUD实例,过程跟纯文本是同样的。hud.label.text = NSLocalizedString(@"Loading...", @"HUD loading title");
配置提示文案为"Loading"。global_queue
里面执行任务,完成任务以后回到主线程隐藏HUD。经过分析纯文本HUD的建立过程咱们知道,hud在初始化的时候,它的mode默认为MBProgressHUDModeIndeterminate
,也就是说单纯的调用MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.navigationController.view animated:YES];
建立出来的HUD就是带有菊花加载控件的HUD,咱们接下来作的就是给它的label赋上文案便可。
MBProgressHUD提供了三种样式的进度条:条状、饼状、环状。其中饼状和环状差很少,接下来咱们分析下条状进度条的实现原理:
- (void)barDeterminateExample { MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.navigationController.view animated:YES]; // Set the bar determinate mode to show task progress. hud.mode = MBProgressHUDModeDeterminateHorizontalBar; hud.label.text = NSLocalizedString(@"Loading...", @"HUD loading title"); dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{ // Do something useful in the background and update the HUD periodically. [self doSomeWorkWithProgress]; dispatch_async(dispatch_get_main_queue(), ^{ [hud hideAnimated:YES]; }); }); }
MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.navigationController.view animated:YES];
建立HUD实例,过程跟纯文本是同样的。hud.mode = MBProgressHUDModeDeterminateHorizontalBar;
设置mode为条状进度条。在mode的setter方法中会调用- (void)updateIndicator
建立进度条indicator。MBBarProgressView
的实例。建立时默认宽为120,高为20,内容高度(intrinsicContentSize
)为10。它的样式是在- (void)drawRect
中绘制的。在其progress
属性的setter方法中调用了-(void)setNeedsDisplay
从而触发- (void)drawRect
来更新进度。hud.label.text = NSLocalizedString(@"Loading...", @"HUD loading title");
配置HUD提示文案为"Loading"。MBProgressHUD提供了显示自定义视图的功能。在Demo中是展现一个对勾。
- (void)customViewExample { MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.navigationController.view animated:YES]; // Set the custom view mode to show any view. hud.mode = MBProgressHUDModeCustomView; // Set an image view with a checkmark. UIImage *image = [[UIImage imageNamed:@"Checkmark"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; hud.customView = [[UIImageView alloc] initWithImage:image]; // Looks a bit nicer if we make it square. hud.square = YES; // Optional label text. hud.label.text = NSLocalizedString(@"Done", @"HUD done title"); [hud hideAnimated:YES afterDelay:3.f]; }
MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.navigationController.view animated:YES];
建立HUD实例,过程跟纯文本是同样的。hud.mode = MBProgressHUDModeCustomView;
设置mode为自定义视图。接下来将须要展现的自定义视图赋值给hud的customView
属性。在customView
属性的setter方法中会调用- (void)updateIndicators
将customView
添加到HUD上。hud.square = YES;
。hud.label.text = NSLocalizedString(@"Loading...", @"HUD loading title");
配置HUD提示文案为"Loading"。至此,咱们已经简单了解了MBProgressHUD的整个代码结构及使用流程,这已经足够咱们去建立和使用符合咱们需求的HUD了。但其实MBProgressHUD的源码中还包含很多高级的技术细节,咱们将在下篇文章中一个个分析学习。