MBProgressHUD 源码学习笔记

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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,794评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,050评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,587评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,861评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,901评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,898评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,832评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,617评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,077评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,349评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,483评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,199评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,824评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,442评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,632评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,474评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,393评论 2 352

推荐阅读更多精彩内容

  • 源码来源:gitHub源码 转载于: CocoaChina 来源:南峰子的技术博客 版本:0.9.1 MBPr...
    李小六_阅读 6,433评论 2 5
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,090评论 4 62
  • 没有卑微的尘世此刻幸福这就是我的一生光明于黑暗 它 衔接了多少回 你从历史中走来像是历史的人第一次开凿窗牖的微风微...
    Amaorent阿毛的空瓶子阅读 311评论 8 6
  • 生活是由那些最重要,而不是最紧迫的事情决定的。 一旦你的生活总是由最紧迫的事情决定,你就会一直被别人牵着走,就会把...
    兰亭小馆阅读 173评论 0 1
  • 今天,男神君教你如何在星巴克装逼!废话少说,直接进入主题。 首先你必须带一本杂志,啥?《读者》?呸,你丫只配去广州...
    嘉有女神阅读 1,378评论 0 0