MBProgressHUD 源码学习笔记

作者: RiverSea | 来源:发表于2018-12-15 19:21 被阅读9次
    MBProgressHUD 源码学习笔记.png

    1.前言

    MBProgressHUD 是 iOS 开发中经常会用到的一个加载动画库,本文就来简单学习一下源码。

    2.视图层级

    在开始学习源码之前,先大概了解一下整个视图的层级结构吧,主要分这几个视图:

    视图层级.png
    • backgroundView,位于最底层,是一个遮罩层,在 HUD 显示时,我们之所以无法点击后边的视图,都是因为它。
    • bezelView,位于 backgroundView 之上,承载着下边要说的几个视图,下边 4 个视图位于同一层级。
    • indicator,是 bezelView 的子视图,类型不定,他自己提供了 2 种类型 MBRoundProgressViewMBBarProgressView,不过也可以使用用户自定义视图 customView
    • label,indicator 下方的提示标签。
    • detailLabel,label 下方的补充提示标签。
    • button,最底部的按钮,可以响应点击事件。

    当然,以上这些视图不一定要全部显示,可以由用户(我们自己 O(∩_∩)O )自由选择。

    3.源码

    言归正传,现在开始读源码。

    3.1 类结构

    虽然 MBProgressHUD 的文件数量非常少,只有 2 个:MBProgressHUD.h 和 MBProgressHUD.m,但还有几个相关的类和协议:

    类之间的相互关系.png
    3.2 MBProgressHUD

    我们主要研究主类 MBProgressHUD,首先看几个重要的属性:

    @property (assign, nonatomic) NSTimeInterval graceTime;   // 显示的宽限时间,即从显示方法被调用到真正显示之间的时间差
    @property (assign, nonatomic) NSTimeInterval minShowTime;  // HUD 最小的展示时间  
    
    @property (nonatomic, weak) NSTimer *graceTimer;        // 延迟显示的 timer
    @property (nonatomic, weak) NSTimer *minShowTimer;      // 保证 HUD 最短显示维持时间 的timer
    @property (nonatomic, weak) NSTimer *hideDelayTimer;    // 延迟隐藏的 timer
    

    接下来看看 MBProgressHUD 为我们提供的 3 个类方法用于展示和隐藏 HUD 的方法:

    // 展示 HUD
    + (instancetype)showHUDAddedTo:(UIView *)view animated:(BOOL)animated;
    // 隐藏 HUD
    + (BOOL)hideHUDForView:(UIView *)view animated:(BOOL)animated;
    // 返回最顶层未结束的 HUD
    + (nullable MBProgressHUD *)HUDForView:(UIView *)view;
    

    我们从展示的方法实现开始探究,实现代码如下:

    + (instancetype)showHUDAddedTo:(UIView *)view animated:(BOOL)animated {
        MBProgressHUD *hud = [[self alloc] initWithView:view];
        hud.removeFromSuperViewOnHide = YES;
        [view addSubview:hud];
        [hud showAnimated:animated];
        return hud;
    }
    

    我们发现,在 initWithView: 方法中对入参 view 做了空判断,这里使用了 NSAsset,如果为空,会报错:

    - (id)initWithView:(UIView *)view {
        NSAssert(view, @"View must not be nil.");
        return [self initWithFrame:view.bounds];
    }
    

    接着看 showAnimated: 的方法实现。

    - (void)showAnimated:(BOOL)animated {
        
        // 1.确保在主线程执行
        MBMainThreadAssert();
        
        // 2.关闭之前可能存在的 minShowTimer
        [self.minShowTimer invalidate];
        
        self.useAnimation = animated;
        self.finished = NO;
        
        // 3.是否延缓 HUD 的展示
        // 如果设置了 graceTime,则延缓 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;
            
        } else {
            
            // 如果没设置 graceTime,则立即展示 HUD
            [self showUsingAnimation:self.useAnimation];
        }
    }
    
    // 定时器 graceTimer 的响应方法
    - (void)handleGraceTimer:(NSTimer *)theTimer {
        // 只有当任务还在进行的时候才会显示 HUD,否则什么也不做
        if (!self.hasFinished) {
            [self showUsingAnimation:self.useAnimation];
        }
    }
    

    此处做了 3 件事:

    ① 首先,为了确保在主线程执行,能够及时更新 UI ,这里使用了 NSAsset,如果当前线程不是主线程,就会报错,MBMainThreadAssert() 的定义如下。

    #define MBMainThreadAssert() NSAssert([NSThread isMainThread], @"MBProgressHUD needs to be accessed on the main thread.");
    

    ② 然后,移除了可能存在的 minShowTimer。

    ③ 最后,根据 graceTime 有无值来决定是否需要延缓 HUD 的显示,如果有值,则启动 graceTimer,待时间到时再执行显示的操作;如果无值,则直接去显示 HUD。

    graceTime 应该是针对耗时比较少的操作准备的,定时器 graceTimer 时间到的时候,操作有可能已经完成了 (self.hasFinished == YES),这时就不需要展示 HUD 了,以免影响用户体验。

    另外,minShowTime 是在隐藏的时候使用的,通过 minShowTimer 保证 HUD 展示时间 >= minShowTime 从而避免 HUD 显示时间过短的问题,也是为了提升用户体验。

    做完 ③ 的判断就该去显示了,即 showUsingAnimation: 的实现,它主要也做了 3 件事:

    - (void)showUsingAnimation:(BOOL)animated {
    
        [self.bezelView.layer removeAllAnimations];
        [self.backgroundView.layer removeAllAnimations];
        [self.hideDelayTimer invalidate];
    
        self.showStarted = [NSDate date];
        self.alpha = 1.f;
    
        [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;
        }
    }
    

    ① 取消之前可能存在的 animations 并停止 ‘延缓隐藏’ 的 timer。

    ② 根据是否需要展示进度做相应的处理。此处传的是 YES,即需要展示:创建一个 CADisplayLink对象并启动(启动的代码写在了 progressObjectDisplayLink 的 setter 里边),在响应方法里将外界传入的 progressObject(NSProgress)的值 progressObject.fractionCompleted 赋给 progress(CGFloat),然后在 progress 的 setter 里更新控件的值。

    // 创建 CADisplayLink 对象,用于更新
    - (void)setNSProgressDisplayLinkEnabled:(BOOL)enabled {
        if (enabled && self.progressObject) {
            if (!self.progressObjectDisplayLink) {
                self.progressObjectDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateProgressFromProgressObject)];
            }
        } else {
            self.progressObjectDisplayLink = nil;
        }
    }
    
    // 为 progressObjectDisplayLink 赋值,并启动 CADisplayLink
    - (void)setProgressObjectDisplayLink:(CADisplayLink *)progressObjectDisplayLink {
        if (progressObjectDisplayLink != _progressObjectDisplayLink) {
            [_progressObjectDisplayLink invalidate];
            
            _progressObjectDisplayLink = progressObjectDisplayLink;
            
            [_progressObjectDisplayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
        }
    }
    
    // 给 progress 赋值
    - (void)updateProgressFromProgressObject {
        self.progress = self.progressObject.fractionCompleted;
    }
    
    // progress 的 setter,并给展示进度的控件赋值
    - (void)setProgress:(float)progress {
        if (progress != _progress) {
            _progress = progress;
            UIView *indicator = self.indicator;
            if ([indicator respondsToSelector:@selector(setProgress:)]) {
                [(id)indicator setValue:@(self.progress) forKey:@"progress"];
            }
        }
    }
    

    外界给 _progressObject 赋值的方法实现如下。这里加了 if (progressObject != _progressObject) 的判断,避免了重复赋值。

    - (void)setProgressObject:(NSProgress *)progressObject {
        if (progressObject != _progressObject) {
            _progressObject = progressObject;
            [self setNSProgressDisplayLinkEnabled:YES];
        }
    }
    

    ③ 如果需要过渡动画,无论是 show 还是 hide 都会调用 animateIn: withType: completion: 这个方法,代码实现如下,详见注释。

    - (void)animateIn:(BOOL)animatingIn withType:(MBProgressHUDAnimation)type completion:(void(^)(BOOL finished))completion {
        
        // 确定缩放动画的类型
        if (type == MBProgressHUDAnimationZoom) {
            type = animatingIn ? MBProgressHUDAnimationZoomIn : MBProgressHUDAnimationZoomOut;
        }
    
        CGAffineTransform small = CGAffineTransformMakeScale(0.5f, 0.5f); // x、y 方向的缩放倍数均为 0.5
        CGAffineTransform large = CGAffineTransformMakeScale(1.5f, 1.5f); // x、y 方向的缩放倍数均为 1.5
        
        UIView *bezelView = self.bezelView;
        
    // * 设置初始状态的值(show 的 过渡动画 的初值,hide 的初值就不用设置了)
        if (animatingIn && bezelView.alpha == 0.f && type == MBProgressHUDAnimationZoomIn) {
            bezelView.transform = small;
        } else if (animatingIn && bezelView.alpha == 0.f && type == MBProgressHUDAnimationZoomOut) {
            bezelView.transform = large;
        }
    
        // 执行动画的 block,作为后边方法的参数
        dispatch_block_t animations = ^{
            
    // * show 的过渡动画 的 终值
            if (animatingIn) {
                bezelView.transform = CGAffineTransformIdentity;
                
    // * 下边 2 个 if 均为 hide 的过渡动画 的 终值
            } 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;
    #pragma clang diagnostic pop
            self.backgroundView.alpha = animatingIn ? 1.f : 0.f;
        };
    
        // 此方法 iOS 7.0 之后才支持
    #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
        
        // iOS 4.0 就开始支持
        [UIView animateWithDuration:0.3
                              delay:0.
                            options:UIViewAnimationOptionBeginFromCurrentState
                         animations:animations
                         completion:completion];
    }
    

    隐藏的逻辑与此类似,可自行参看代码,这里简单绘制了一张方法调用流程图作为小结,如下所示:

    show-hide.png

    相关文章

      网友评论

        本文标题:MBProgressHUD 源码学习笔记

        本文链接:https://www.haomeiwen.com/subject/mybbhqtx.html