读源码-MBProgressHUD

MBProgressHUD 在GitHub上有14k星星,大部分的开发者应该用过这个第三方库,最近花了点时间看了一下源码,写下此文作为总结。


这篇文章主要分以下三个部分:

  1. 方法注释
  2. 方法调用流程图
  3. 内部实现

方法注释

类方法

/// 创建一个新的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 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;

方法调用流程图
读源码-MBProgressHUD_第1张图片
显示和隐藏流程图

方法内部实现

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 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
}

收获:

  1. 使用到bezelView.translatesAutoresizingMaskIntoConstraints = NO;属性。 除了AutoLayoutAutoresizingMask也是一种布局方式。默认情况下,translatesAutoresizingMaskIntoConstraints = YES , 此时视图的AutoresizingMask会被转换成对应效果的约束。这样很可能就会和我们手动添加的其它约束有冲突。此属性设置成NO时,AutoresizingMask就不会变成约束。也就是说 当前 视图的 AutoresizingMask失效了
  2. 消除方法弃用警告
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
self.bezelView.alpha = self.opacity; 
#pragma clang diagnostic pop
  1. 在显示之前添加graceTime宽限期,如果在宽限期内任务完成,不显示HUD,防止非常短的任务时HUD显示,影响用户体验。在显示时增加minShowTime最小显示时间,防止HUD显示然后立即消失的现象。
  2. 因为与界面相关的操作需要放在主线程,在此第三方库的中,使用MBMainThreadAssert()宏,防止不是在主线程操作。如果不是在主线程,则抛出错误异常。
  3. 使用CADisplayLink定时刷新进度,它是以和屏幕刷新率同步的频率将进度内容绘制在屏幕上,防止消耗主线程。
  4. [view setContentCompressionResistancePriority:998.f forAxis:UILayoutConstraintAxisHorizontal]; 设置控件水平方向抗压缩的优先级

第一次写阅读源码的文章,有写得不好的地方还希望多多指点哈~

你可能感兴趣的:(读源码-MBProgressHUD)