MBProgressHUD 在GitHub上有14k星星,大部分的开发者应该用过这个第三方库,最近花了点时间看了一下源码,写下此文作为总结。
这篇文章主要分以下三个部分:
- 方法注释
- 方法调用流程图
- 内部实现
方法注释
类方法
/// 创建一个新的HUD,添加到被提供的视图上并显示。与此方法相对应的是hideHUDForView:animated:方法。
/// 注意:此方法会设置removeFromSuperViewOnHide属性为YES。此HUD在隐藏时会从视图层级中自动移除。
/// 参数一view: HUD将添加到此视图上。
/// 参数二animated:如果设置为YES,HUD将使用当前的 animationType属性动画出现。否则,HUD将在出现时不会使用动画。
/// 返回值: 已经创建的此HUD的引用。
+ (instancetype)showHUDAddedTo:(UIView *)view animated:(BOOL)animated;
/// 找到尚未完成的最顶级HUD子视图并隐藏它。与此方法相对应的是showHUDAddedTo:animated:方法
/// 注意:此方法会设置removeFromSuperViewOnHide属性为YES。此HUD在隐藏时会从视图层级中自动移除。
/// 参数一view:从该视图中寻找HUD子视图
/// 参数二animated: 如果设置为YES,HUD将使用当前的animationType属性动画消失。否则,HUD将在消失时不会使用动画。
/// 返回值:BOOL值,如果HUD被找到并从视图中移除则返回YES。否则,返回NO
+ (BOOL)hideHUDForView:(UIView *)view animated:(BOOL)animated;
/// 找到尚未完成的最顶级HUD子视图并返回它。
/// 参数view: 从该视图中寻找HUD子视图
/// 返回值: 最后一个被发现到的HUD子视图引用。
+ (nullable MBProgressHUD *)HUDForView:(UIView *)view;
对象方法
/// 一个便利构造器使用view的bounds来初始化HUD对象。以view的bounds作为参数调用指定初始化器
/// 参数view: 为HUD提供bounds的视图实例。应该和HUD的父视图是相同的实例。(HUD将添加到此view上)
- (instancetype)initWithView:(UIView *)view;
/// 显示HUD
/// 注意: 需要确保在主线程调用此方法后完成它的运行循环,以便能够更新用户界面。当你的任务已经设置在一个新的线程中执行时调用此方法
/// 参数animated:BOOL值,如果是YES,HUD将使用当前的animationType属性动画出现。否则,HUD将在出现时不使用动画
- (void)showAnimated:(BOOL)animated;
/// 隐藏HUD。将会调用hudWasHidden:代理方法。和此方法相对应的是showAnimated:method方法。当任务完成时使用此方法来隐藏HUD。
/// 参数animated: BOOL值,如果是YES,HUD将使用当前的animationType属性动画消失。否则,HUD将在消失时不使用动画
- (void)hideAnimated:(BOOL)animated;
/// 延迟后隐藏HUD。将会调用hudWasHidden:代理方法。和此方法相对应的是showAnimated:method方法。当任务完成时使用此方法来隐藏HUD。
/// 参数一animated:BOOL值,如果是YES,HUD将使用当前的animationType属性动画消失。否则,HUD将在消失时不使用动画。
/// 参数二delay: 以秒为单位延迟,直到HUD隐藏
- (void)hideAnimated:(BOOL)animated afterDelay:(NSTimeInterval)delay;
属性
/// HUD的代理对象。接受HUD状态通知
@property (weak, nonatomic) id<MBProgressHUDDelegate> delegate;
/// 在HUD隐藏后调用
@property (copy, nullable) MBProgressHUDCompletionBlock completionBlock;
/// 宽限期是调用方法可能在运行时没有显示HUD的时间。如果任务在宽限期内完成,HUD将一直不会显示。这可以用来防止非常短的任务时HUD显示。默认值为0。
@property (assign, nonatomic) NSTimeInterval graceTime;
/// HUD显示的最短时长。这可以用来避免HUD开始显示然后立即消失的问题出现。默认值为0。
@property (assign, nonatomic) NSTimeInterval minShowTime;
/// 是否当HUD隐藏时从它的父视图移除。默认为NO,不移除。
@property (assign, nonatomic) BOOL removeFromSuperViewOnHide;
/// MBProgressHUD操作模式。默认是MBProgressHUDModeIndeterminate
@property (assign, nonatomic) MBProgressHUDMode mode;
/// 获取转发给所有标签和支持的指示器的颜色。还为iOS7+上的自定义视图设置tintColor。设置为nil用来单独管理颜色。默认在iOS7及以后系统上设置为半透明的黑色,在之前的系统上设置为白色。
@property (strong, nonatomic, nullable) UIColor *contentColor UI_APPEARANCE_SELECTOR;
/// 当HUD显示和隐藏时应该被用到的动画类型
@property (assign, nonatomic) MBProgressHUDAnimation animationType UI_APPEARANCE_SELECTOR;
/// 表圈bezelView相对于视图中心的偏移量。可以使用MBProgressMaxOffset和 -MBProgressMaxOffset将HUD一直移动到每个方向的屏幕边缘。例如CGPointMake(0.f, MBProgressMaxOffset) 将HUD定位在底部边缘的中心。
@property (assign, nonatomic) CGPoint offset UI_APPEARANCE_SELECTOR;
/// HUD边缘与HUD元素(标签、指示器或自定义视图)之间的空间量。也表示边框到HUD视图边缘的最小距离。默认2.0f。
@property (assign, nonatomic) CGFloat margin UI_APPEARANCE_SELECTOR;
/// HUD表圈的最小尺寸。默认为CGSizeZero。
@property (assign, nonatomic) CGSize minSize UI_APPEARANCE_SELECTOR;
/// 如果可能,强制HUD尺寸相等
@property (assign, nonatomic, getter = isSquare) BOOL square UI_APPEARANCE_SELECTOR;
/// 启用后,表圈中心会收到设备加速度计数据的轻微影响。在iOS < 7.0无影响。默认为YES。
@property (assign, nonatomic, getter=areDefaultMotionEffectsEnabled) BOOL defaultMotionEffectsEnabled UI_APPEARANCE_SELECTOR;
/// 进度指示器的进度,范围是0.0 - 1.0。默认为0。
@property (assign, nonatomic) float progress;
/// NSProgress对象将进度信息提供给进度指示器
@property (strong, nonatomic, nullable) NSProgress *progressObject;
/// 表圈视图,包含标签和指示器(或自定义视图)
@property (strong, nonatomic, readonly) MBBackgroundView *bezelView;
/// 覆盖整个HUD区域的视图,放置在表圈视图的后面。
@property (strong, nonatomic, readonly) MBBackgroundView *backgroundView;
/// 当HUD的模式是MBProgressHUDModeCustomView时用于显示的自定义视图。该视图应该实现 intrinsicContentSize 以适当调整大小。为获得最佳效果,请使用大约37x37像素。
@property (strong, nonatomic, nullable) UIView *customView;
/// 标签,包含可选短信息的标签,显示在活动指示器下方。HUD会自动调整大小以适应整个文本。
@property (strong, nonatomic, readonly) UILabel *label;
/// 标签,包含可选详细信息的标签,显示在短信息标签的下方。详细文本可以跨度多行。
@property (strong, nonatomic, readonly) UILabel *detailsLabel;
/// 按钮,放置在标签下方。仅在添加target/action时才可见。
@property (strong, nonatomic, readonly) UIButton *button;
方法调用流程图
方法内部实现
show方法
+ (instancetype)showHUDAddedTo:(UIView *)view animated:(BOOL)animated {
// 创建hud对象
MBProgressHUD *hud = [[self alloc] initWithView:view];
// 当hud隐藏时从父视图移除
hud.removeFromSuperViewOnHide = YES;
// 在view上添加hud
[view addSubview:hud];
// 显示hud
[hud showAnimated:animated];
return hud;
}
// 是否动画显示
- (void)showAnimated:(BOOL)animated {
MBMainThreadAssert(); // 如果不是在主线程,则抛异常
[self.minShowTimer invalidate]; // 停止最小显示时长的定时器
self.useAnimation = animated;// 是否动画
self.finished = NO; // 标记未完成
// If the grace time is set, postpone the HUD display
// 如果宽限时间已设置,则延迟HUD显示
if (self.graceTime > 0.0) { // 如果设置了宽限时间
NSTimer *timer = [NSTimer timerWithTimeInterval:self.graceTime target:self selector:@selector(handleGraceTimer:) userInfo:nil repeats:NO];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
self.graceTimer = timer; // 启动控制宽限时长定时器
}
// ... otherwise show the HUD immediately
// 否则立即显示HUD
else {
[self showUsingAnimation:self.useAnimation]; // 是否动画显示
}
}
// 处理宽限时长定时器任务
- (void)handleGraceTimer:(NSTimer *)theTimer {
// Show the HUD only if the task is still running
// 到了宽限时间时
// 只有在任务仍在运行时才显示HUD
if (!self.hasFinished) {
[self showUsingAnimation:self.useAnimation];
}
}
// 真实的显示 是否动画
- (void)showUsingAnimation:(BOOL)animated {
// Cancel any previous animations
// 取消当前的任何动画
[self.bezelView.layer removeAllAnimations];
[self.backgroundView.layer removeAllAnimations];
// Cancel any scheduled hideDelayed: calls
// 停止隐藏延迟定时器 取消hideDelayed:方法的调用
[self.hideDelayTimer invalidate];
// 设置当前时间为开始显示时间
self.showStarted = [NSDate date];
self.alpha = 1.f; // 设置视图可见
// Needed in case we hide and re-show with the same NSProgress object attached.
// 如果使用附加的相同NSProgress对象的隐藏和重新显示,则需要
[self setNSProgressDisplayLinkEnabled:YES];
if (animated) { // 如果动画
[self animateIn:YES withType:self.animationType completion:NULL]; // 根据动画类型,动画显示
} else { // 否则不用动画显示
#pragma clang diagnostic push // 去掉方法弃用警告
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
self.bezelView.alpha = self.opacity; // 设置不透明度
#pragma clang diagnostic pop
self.backgroundView.alpha = 1.f; // 设置背景视图可见
}
}
show
方法的核心是showUsingAnimation:
方法,在这里面处理视图的显示,在此方法内部调用了setNSProgressDisplayLinkEnabled:
方法,其内部是使用CADisplayLink
来刷新进度条
- (void)setNSProgressDisplayLinkEnabled:(BOOL)enabled {
// We're using CADisplayLink, because NSProgress can change very quickly and observing it may starve the main thread,
// 使用CADisplayLink,因为NSProgress可以快速改变并观察它可能导致主线程中断
// so we're refreshing the progress only every frame draw
// 所以只刷新每一帧画面的进度
if (enabled && self.progressObject) { // 如果需要 并且有NSProgress对象
// Only create if not already active.
// 仅在尚未激活的情况下创建
if (!self.progressObjectDisplayLink) { // 如果没有,则创建
self.progressObjectDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateProgressFromProgressObject)];
}
} else { // 否则,置空
self.progressObjectDisplayLink = nil;
}
}
hide方法
+ (BOOL)hideHUDForView:(UIView *)view animated:(BOOL)animated {
// 找到尚未完成的最顶级HUD子视图
MBProgressHUD *hud = [self HUDForView:view];
if (hud != nil) { // 如果有
hud.removeFromSuperViewOnHide = YES; // 设置hud隐藏时从父视图移除
[hud hideAnimated:animated]; // 隐藏hud
return YES;
}
return NO;
}
// 找到尚未完成的最顶级HUD子视图
+ (MBProgressHUD *)HUDForView:(UIView *)view {
// 逆向遍历
NSEnumerator *subviewsEnum = [view.subviews reverseObjectEnumerator];
for (UIView *subview in subviewsEnum) {
if ([subview isKindOfClass:self]) { // 如果视图是MBProgressHUD类型
MBProgressHUD *hud = (MBProgressHUD *)subview;
if (hud.hasFinished == NO) { // 如果未完成,则返回hud
return hud;
}
}
}
return nil;
}
// 隐藏hud
- (void)hideAnimated:(BOOL)animated {
MBMainThreadAssert();
[self.graceTimer invalidate]; // 停止宽限时间定时器
self.useAnimation = animated;
self.finished = YES; // 标记已完成
// If the minShow time is set, calculate how long the HUD was shown,
// 如果设置了最小显示时长,则计算hud显示的时间,必要时推迟隐藏操作
// and postpone the hiding operation if necessary
if (self.minShowTime > 0.0 && self.showStarted) { // 设置了最小显示时长,并且有开始显示时间
// 计算现在的时间离开始显示的时间间隔
NSTimeInterval interv = [[NSDate date] timeIntervalSinceDate:self.showStarted];
// 如果时间间隔在最小显示时间之内
if (interv < self.minShowTime) {
// 开启最小显示时间定时器
NSTimer *timer = [NSTimer timerWithTimeInterval:(self.minShowTime - interv) target:self selector:@selector(handleMinShowTimer:) userInfo:nil repeats:NO];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
self.minShowTimer = timer;
return; // 返回
}
}
// ... otherwise hide the HUD immediately
// 否则立即执行hud隐藏操作
[self hideUsingAnimation:self.useAnimation];
}
- (void)handleMinShowTimer:(NSTimer *)theTimer {
// 到了最小显示时长,则隐藏hud
[self hideUsingAnimation:self.useAnimation];
}
- (void)hideUsingAnimation:(BOOL)animated {
// 如果动画 并且 有开始显示时间,说明在显示
if (animated && self.showStarted) {
self.showStarted = nil; // 将开始显示时间置空
// 放大动画为NO,则为缩小动画,动画显示
[self animateIn:NO withType:self.animationType completion:^(BOOL finished) {
// 动画完成后
[self done];
}];
} else { // 否则
self.showStarted = nil; // 将开始显示时间置空
self.bezelView.alpha = 0.f; // 不可见
self.backgroundView.alpha = 1.f; // 背景视图可见
[self done]; // 动画完成
}
}
- (void)done {
// Cancel any scheduled hideDelayed: calls
// 取消任何计划hideDelayed:方法调用
[self.hideDelayTimer invalidate]; // 停止隐藏延迟定时器
[self setNSProgressDisplayLinkEnabled:NO]; // 停止进度条显示
if (self.hasFinished) { // 如果已经完成
self.alpha = 0.0f; // 不可见
if (self.removeFromSuperViewOnHide) { // 如果需要从父视图中移除
[self removeFromSuperview]; // 从父视图中移除
}
}
// 完成的block
MBProgressHUDCompletionBlock completionBlock = self.completionBlock;
if (completionBlock) {
completionBlock(); // 回调隐藏完成的block
}
id<MBProgressHUDDelegate> delegate = self.delegate;
if ([delegate respondsToSelector:@selector(hudWasHidden:)]) {
// 代理回调hud已经隐藏
[delegate performSelector:@selector(hudWasHidden:) withObject:self];
}
}
可以发现,无论是
show
方法还是hide
方法,在设定animated
属性为YES
时,最终会走animateIn: withType: completion:
方法。此方法主要作用是处理显示和隐藏的动画效果。
// 核心方法,是否动画放大,动画类型,是否完成回调
- (void)animateIn:(BOOL)animatingIn withType:(MBProgressHUDAnimation)type completion:(void(^)(BOOL finished))completion {
// Automatically determine the correct zoom animation type
// 自动确定正确的缩放动画类型
if (type == MBProgressHUDAnimationZoom) { // 如果是不透明动画
type = animatingIn ? MBProgressHUDAnimationZoomIn : MBProgressHUDAnimationZoomOut; // 若动画放大则设置类型为放大的风格,若不动画则设置为缩小的风格
}
CGAffineTransform small = CGAffineTransformMakeScale(0.5f, 0.5f); // 缩小比例
CGAffineTransform large = CGAffineTransformMakeScale(1.5f, 1.5f); // 放大比例
// Set starting state
// 设置开始状态
UIView *bezelView = self.bezelView;
if (animatingIn && bezelView.alpha == 0.f && type == MBProgressHUDAnimationZoomIn) {
bezelView.transform = small; // 缩小
} else if (animatingIn && bezelView.alpha == 0.f && type == MBProgressHUDAnimationZoomOut) {
bezelView.transform = large; // 放大
}
// Perform animations
// 执行动画
dispatch_block_t animations = ^{
if (animatingIn) { // 如果动画放大
bezelView.transform = CGAffineTransformIdentity; // 初始状态
} else if (!animatingIn && type == MBProgressHUDAnimationZoomIn) { // 放大
bezelView.transform = large;
} else if (!animatingIn && type == MBProgressHUDAnimationZoomOut) {
bezelView.transform = small; // 缩小
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations" // 消除警告
bezelView.alpha = animatingIn ? self.opacity : 0.f; // 如果是放大状态,则设置不透明度为1,否则为0
#pragma clang diagnostic pop
self.backgroundView.alpha = animatingIn ? 1.f : 0.f;
};
// Spring animations are nicer, but only available on iOS 7+
// iOS7+ 使用Spring动画效果
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000 || TARGET_OS_TV
if (kCFCoreFoundationVersionNumber >= kCFCoreFoundationVersionNumber_iOS_7_0) {
[UIView animateWithDuration:0.3 delay:0. usingSpringWithDamping:1.f initialSpringVelocity:0.f options:UIViewAnimationOptionBeginFromCurrentState animations:animations completion:completion]; // 从当前状态动画
return;
}
#endif
[UIView animateWithDuration:0.3 delay:0. options:UIViewAnimationOptionBeginFromCurrentState animations:animations completion:completion];
}
手机晃动时抖动视差实现
/// 更新bezelView运动效果, 手机在晃动时,bezelView会抖动
- (void)updateBezelMotionEffects {
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000 || TARGET_OS_TV
MBBackgroundView *bezelView = self.bezelView; // 获取bezelView
if (![bezelView respondsToSelector:@selector(addMotionEffect:)]) return; // 不能响应addMotionEffect则不处理
if (self.defaultMotionEffectsEnabled) { // 如果启用表圈中心会受到设备加速度计数据的轻微影响
CGFloat effectOffset = 10.f; // 效果偏移量
// keyPath: 左右翻转屏幕将要影响到的属性
// type: 观察者视角,也就是屏幕倾斜的方式,目前区分水平和垂直两种方式, 此处为水平方式
UIInterpolatingMotionEffect *effectX = [[UIInterpolatingMotionEffect alloc] initWithKeyPath:@"center.x" type:UIInterpolatingMotionEffectTypeTiltAlongHorizontalAxis]; // 视差效果对象
effectX.maximumRelativeValue = @(effectOffset); // keyPath对应的值的变化范围最大值
effectX.minimumRelativeValue = @(-effectOffset); //keyPath对应的值的变化范围最小值
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 添加视差效果
[bezelView addMotionEffect:group];
} else {
// 移除bezelView上的视差效果
NSArray *effects = [bezelView motionEffects];
for (UIMotionEffect *effect in effects) {
[bezelView removeMotionEffect:effect];
}
}
#endif
}
收获:
- 使用到
bezelView.translatesAutoresizingMaskIntoConstraints = NO;
属性。 除了AutoLayout
,AutoresizingMask
也是一种布局方式。默认情况下,translatesAutoresizingMaskIntoConstraints = YES
, 此时视图的AutoresizingMask
会被转换成对应效果的约束。这样很可能就会和我们手动添加的其它约束有冲突。此属性设置成NO
时,AutoresizingMask
就不会变成约束。也就是说 当前 视图的AutoresizingMask
失效了- 消除方法弃用警告
#pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" self.bezelView.alpha = self.opacity; #pragma clang diagnostic pop
- 在显示之前添加
graceTime
宽限期,如果在宽限期内任务完成,不显示HUD,防止非常短的任务时HUD显示,影响用户体验。在显示时增加minShowTime
最小显示时间,防止HUD显示然后立即消失的现象。- 因为与界面相关的操作需要放在主线程,在此第三方库的中,使用
MBMainThreadAssert()
宏,防止不是在主线程操作。如果不是在主线程,则抛出错误异常。- 使用
CADisplayLink
定时刷新进度,它是以和屏幕刷新率同步的频率将进度内容绘制在屏幕上,防止消耗主线程。[view setContentCompressionResistancePriority:998.f forAxis:UILayoutConstraintAxisHorizontal];
设置控件水平方向抗压缩的优先级
第一次写阅读源码的文章,有写得不好的地方还希望多多指点哈~