MBProgressHUD 源碼學(xué)習(xí)筆記

MBProgressHUD 源碼學(xué)習(xí)筆記.png

1.前言

MBProgressHUD 是 iOS 開發(fā)中經(jīng)常會(huì)用到的一個(gè)加載動(dòng)畫庫,本文就來簡單學(xué)習(xí)一下源碼。

2.視圖層級(jí)

在開始學(xué)習(xí)源碼之前,先大概了解一下整個(gè)視圖的層級(jí)結(jié)構(gòu)吧,主要分這幾個(gè)視圖:

視圖層級(jí).png
  • backgroundView,位于最底層,是一個(gè)遮罩層,在 HUD 顯示時(shí),我們之所以無法點(diǎn)擊后邊的視圖,都是因?yàn)樗?/li>
  • bezelView,位于 backgroundView 之上,承載著下邊要說的幾個(gè)視圖,下邊 4 個(gè)視圖位于同一層級(jí)。
  • indicator,是 bezelView 的子視圖,類型不定,他自己提供了 2 種類型 MBRoundProgressViewMBBarProgressView,不過也可以使用用戶自定義視圖 customView。
  • label,indicator 下方的提示標(biāo)簽。
  • detailLabel,label 下方的補(bǔ)充提示標(biāo)簽。
  • button,最底部的按鈕,可以響應(yīng)點(diǎn)擊事件。

當(dāng)然,以上這些視圖不一定要全部顯示,可以由用戶(我們自己 O(∩_∩)O )自由選擇。

3.源碼

言歸正傳,現(xiàn)在開始讀源碼。

3.1 類結(jié)構(gòu)

雖然 MBProgressHUD 的文件數(shù)量非常少,只有 2 個(gè):MBProgressHUD.h 和 MBProgressHUD.m,但還有幾個(gè)相關(guān)的類和協(xié)議:

類之間的相互關(guān)系.png
3.2 MBProgressHUD

我們主要研究主類 MBProgressHUD,首先看幾個(gè)重要的屬性:

@property (assign, nonatomic) NSTimeInterval graceTime;   // 顯示的寬限時(shí)間,即從顯示方法被調(diào)用到真正顯示之間的時(shí)間差
@property (assign, nonatomic) NSTimeInterval minShowTime;  // HUD 最小的展示時(shí)間  

@property (nonatomic, weak) NSTimer *graceTimer;        // 延遲顯示的 timer
@property (nonatomic, weak) NSTimer *minShowTimer;      // 保證 HUD 最短顯示維持時(shí)間 的timer
@property (nonatomic, weak) NSTimer *hideDelayTimer;    // 延遲隱藏的 timer

接下來看看 MBProgressHUD 為我們提供的 3 個(gè)類方法用于展示和隱藏 HUD 的方法:

// 展示 HUD
+ (instancetype)showHUDAddedTo:(UIView *)view animated:(BOOL)animated;
// 隱藏 HUD
+ (BOOL)hideHUDForView:(UIView *)view animated:(BOOL)animated;
// 返回最頂層未結(jié)束的 HUD
+ (nullable MBProgressHUD *)HUDForView:(UIView *)view;

我們從展示的方法實(shí)現(xiàn)開始探究,實(shí)現(xiàn)代碼如下:

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

我們發(fā)現(xiàn),在 initWithView: 方法中對(duì)入?yún)?view 做了空判斷,這里使用了 NSAsset,如果為空,會(huì)報(bào)錯(cuò):

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

接著看 showAnimated: 的方法實(shí)現(xiàn)。

- (void)showAnimated:(BOOL)animated {
    
    // 1.確保在主線程執(zhí)行
    MBMainThreadAssert();
    
    // 2.關(guān)閉之前可能存在的 minShowTimer
    [self.minShowTimer invalidate];
    
    self.useAnimation = animated;
    self.finished = NO;
    
    // 3.是否延緩 HUD 的展示
    // 如果設(shè)置了 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 {
        
        // 如果沒設(shè)置 graceTime,則立即展示 HUD
        [self showUsingAnimation:self.useAnimation];
    }
}

// 定時(shí)器 graceTimer 的響應(yīng)方法
- (void)handleGraceTimer:(NSTimer *)theTimer {
    // 只有當(dāng)任務(wù)還在進(jìn)行的時(shí)候才會(huì)顯示 HUD,否則什么也不做
    if (!self.hasFinished) {
        [self showUsingAnimation:self.useAnimation];
    }
}

此處做了 3 件事:

① 首先,為了確保在主線程執(zhí)行,能夠及時(shí)更新 UI ,這里使用了 NSAsset,如果當(dāng)前線程不是主線程,就會(huì)報(bào)錯(cuò),MBMainThreadAssert() 的定義如下。

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

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

③ 最后,根據(jù) graceTime 有無值來決定是否需要延緩 HUD 的顯示,如果有值,則啟動(dòng) graceTimer,待時(shí)間到時(shí)再執(zhí)行顯示的操作;如果無值,則直接去顯示 HUD。

graceTime 應(yīng)該是針對(duì)耗時(shí)比較少的操作準(zhǔn)備的,定時(shí)器 graceTimer 時(shí)間到的時(shí)候,操作有可能已經(jīng)完成了 (self.hasFinished == YES),這時(shí)就不需要展示 HUD 了,以免影響用戶體驗(yàn)。

另外,minShowTime 是在隱藏的時(shí)候使用的,通過 minShowTimer 保證 HUD 展示時(shí)間 >= minShowTime 從而避免 HUD 顯示時(shí)間過短的問題,也是為了提升用戶體驗(yàn)。

做完 ③ 的判斷就該去顯示了,即 showUsingAnimation: 的實(shí)現(xiàn),它主要也做了 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。

② 根據(jù)是否需要展示進(jìn)度做相應(yīng)的處理。此處傳的是 YES,即需要展示:創(chuàng)建一個(gè) CADisplayLink對(duì)象并啟動(dòng)(啟動(dòng)的代碼寫在了 progressObjectDisplayLink 的 setter 里邊),在響應(yīng)方法里將外界傳入的 progressObject(NSProgress)的值 progressObject.fractionCompleted 賦給 progress(CGFloat),然后在 progress 的 setter 里更新控件的值。

// 創(chuàng)建 CADisplayLink 對(duì)象,用于更新
- (void)setNSProgressDisplayLinkEnabled:(BOOL)enabled {
    if (enabled && self.progressObject) {
        if (!self.progressObjectDisplayLink) {
            self.progressObjectDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateProgressFromProgressObject)];
        }
    } else {
        self.progressObjectDisplayLink = nil;
    }
}

// 為 progressObjectDisplayLink 賦值,并啟動(dòng) 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,并給展示進(jìn)度的控件賦值
- (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 賦值的方法實(shí)現(xiàn)如下。這里加了 if (progressObject != _progressObject) 的判斷,避免了重復(fù)賦值。

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

③ 如果需要過渡動(dòng)畫,無論是 show 還是 hide 都會(huì)調(diào)用 animateIn: withType: completion: 這個(gè)方法,代碼實(shí)現(xiàn)如下,詳見注釋。

- (void)animateIn:(BOOL)animatingIn withType:(MBProgressHUDAnimation)type completion:(void(^)(BOOL finished))completion {
    
    // 確定縮放動(dòng)畫的類型
    if (type == MBProgressHUDAnimationZoom) {
        type = animatingIn ? MBProgressHUDAnimationZoomIn : MBProgressHUDAnimationZoomOut;
    }

    CGAffineTransform small = CGAffineTransformMakeScale(0.5f, 0.5f); // x、y 方向的縮放倍數(shù)均為 0.5
    CGAffineTransform large = CGAffineTransformMakeScale(1.5f, 1.5f); // x、y 方向的縮放倍數(shù)均為 1.5
    
    UIView *bezelView = self.bezelView;
    
// * 設(shè)置初始狀態(tài)的值(show 的 過渡動(dòng)畫 的初值,hide 的初值就不用設(shè)置了)
    if (animatingIn && bezelView.alpha == 0.f && type == MBProgressHUDAnimationZoomIn) {
        bezelView.transform = small;
    } else if (animatingIn && bezelView.alpha == 0.f && type == MBProgressHUDAnimationZoomOut) {
        bezelView.transform = large;
    }

    // 執(zhí)行動(dòng)畫的 block,作為后邊方法的參數(shù)
    dispatch_block_t animations = ^{
        
// * show 的過渡動(dòng)畫 的 終值
        if (animatingIn) {
            bezelView.transform = CGAffineTransformIdentity;
            
// * 下邊 2 個(gè) if 均為 hide 的過渡動(dòng)畫 的 終值
        } 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];
}

隱藏的邏輯與此類似,可自行參看代碼,這里簡單繪制了一張方法調(diào)用流程圖作為小結(jié),如下所示:

show-hide.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 源碼來源:gitHub源碼 轉(zhuǎn)載于: CocoaChina 來源:南峰子的技術(shù)博客 版本:0.9.1 MBPr...
    李小六_閱讀 6,564評(píng)論 2 5
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,423評(píng)論 4 61
  • 沒有卑微的塵世此刻幸福這就是我的一生光明于黑暗 它 銜接了多少回 你從歷史中走來像是歷史的人第一次開鑿窗牖的微風(fēng)微...
    Amaorent阿毛的空瓶子閱讀 341評(píng)論 8 6
  • 生活是由那些最重要,而不是最緊迫的事情決定的。 一旦你的生活總是由最緊迫的事情決定,你就會(huì)一直被別人牽著走,就會(huì)把...
    蘭亭小館閱讀 249評(píng)論 0 1
  • 今天,男神君教你如何在星巴克裝逼!廢話少說,直接進(jìn)入主題。 首先你必須帶一本雜志,啥?《讀者》?呸,你丫只配去廣州...
    嘉有女神閱讀 2,079評(píng)論 0 0

友情鏈接更多精彩內(nèi)容