MBProgressHUD 源碼分析

在項目中經(jīng)常會使用MBProgressHUD來實現(xiàn)彈窗提醒,所有來分析下MBProgressHUD這個三方庫的代碼。所分析的源碼版本號為1.0.0擒抛。


這篇總結(jié)主要分三個部分來介紹分析這個框架:

  • 代碼結(jié)構(gòu)
  • 方法調(diào)用流程圖
  • 方法內(nèi)部實現(xiàn)

代碼結(jié)構(gòu)
類圖
MBProgressHUD.png
核心API
屬性
/*
* 用來推遲HUD的顯示,避免HUD顯示時間過短,出現(xiàn)一閃而逝的情況拌蜘,默認值為0。
*/
@property (assign, nonatomic) NSTimeInterval graceTime;

/**
*  HUD最短顯示時間牙丽,單位為s简卧,默認值為0。 
*/
@property (assign, nonatomic) NSTimeInterval minShowTime;

/**
* HUD隱藏時烤芦,將其從父視圖上移除 举娩。默認值為NO 
*/
@property (assign, nonatomic) BOOL removeFromSuperViewOnHide; 

/** 
* HUD顯示類型,默認為 MBProgressHUDModeIndeterminate.
*/
@property (assign, nonatomic) MBProgressHUDMode mode; 
類方法
/**
 * 創(chuàng)建HUD,添加到提供的視圖上并顯示
 */
+ (instancetype)showHUDAddedTo:(UIView *)view animated:(BOOL)animated;

/**
 * 找到最上層的HUD,并隱藏构罗。
 */
+ (BOOL)hideHUDForView:(UIView *)view animated:(BOOL)animated;

/**
 * 在傳入的View上找到最上層的HUD并隱藏此HUD
 */
+ (nullable MBProgressHUD *)HUDForView:(UIView *)view;
實例方法
/**
 * 構(gòu)造函數(shù)铜涉,用來初始化HUD
 */
- (instancetype)initWithView:(UIView *)view;

/** 
 * 顯示HUD
 */
- (void)showAnimated:(BOOL)animated;

/** 
 * 隱藏HUD
 */
- (void)hideAnimated:(BOOL)animated;
方法調(diào)用流程圖

MBProgressHUD提供的主要接口可以看出,主要有顯示HUD和隱藏HUD這兩個功能遂唧,一步步追溯芙代,得出的方法調(diào)用流程圖如下:

MBProgressHUD流程圖.png

方法內(nèi)部實現(xiàn)

方法的內(nèi)部實現(xiàn)主要從兩個方面來分析,顯示HUD和隱藏HUD盖彭。

顯示HUD

首先是MBProgressHUD的構(gòu)造方法

+ (instancetype)showHUDAddedTo:(UIView *)view animated:(BOOL)animated {
    //初始化MBProgressHUD
    MBProgressHUD *hud = [[self alloc] initWithView:view];
    hud.removeFromSuperViewOnHide = YES;
    [view addSubview:hud];
    [hud showAnimated:animated];
    [[UINavigationBar appearance] setBarTintColor:nil];
    return hud;
}

首先進入- (id)initWithView:(UIView *)view方法纹烹,再進入- (instancetype)initWithFrame:(CGRect)frame方法,最后調(diào)用- (void)commonInit方法召边,進行屬性的初始化和添加子視圖铺呵。

- (void)commonInit {
    // Set default values for properties
    _animationType = MBProgressHUDAnimationFade;
    _mode = MBProgressHUDModeIndeterminate;
    _margin = 20.0f;
    _opacity = 1.f;
    _defaultMotionEffectsEnabled = YES;

    // Default color, depending on the current iOS version
    BOOL isLegacy = kCFCoreFoundationVersionNumber < kCFCoreFoundationVersionNumber_iOS_7_0;
    _contentColor = isLegacy ? [UIColor whiteColor] : [UIColor colorWithWhite:0.f alpha:0.7f];
    // Transparent background
    self.opaque = NO;
    self.backgroundColor = [UIColor clearColor];
    // Make it invisible for now
    self.alpha = 0.0f;
    self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    self.layer.allowsGroupOpacity = NO;
    //添加子視圖
    [self setupViews];
    //更新指示器
    [self updateIndicators];
    [self registerForNotifications];
}

添加子視圖都是常見的方式,讓視圖跟隨陀螺儀運動隧熙,這個之前沒有接觸過片挂,后續(xù)需要了解下。

- (void)updateBezelMotionEffects {
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000 || TARGET_OS_TV
    MBBackgroundView *bezelView = self.bezelView;
    if (![bezelView respondsToSelector:@selector(addMotionEffect:)]) return;

    if (self.defaultMotionEffectsEnabled) {
        CGFloat effectOffset = 10.f;
        UIInterpolatingMotionEffect *effectX = [[UIInterpolatingMotionEffect alloc] initWithKeyPath:@"center.x" type:UIInterpolatingMotionEffectTypeTiltAlongHorizontalAxis];
        effectX.maximumRelativeValue = @(effectOffset);
        effectX.minimumRelativeValue = @(-effectOffset);

        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 addMotionEffect:group];
    } else {
        NSArray *effects = [bezelView motionEffects];
        for (UIMotionEffect *effect in effects) {
            [bezelView removeMotionEffect:effect];
        }
    }
#endif
}

再主要看下更新指示器的代碼贞盯。

- (void)updateIndicators { 
    UIView *indicator = self.indicator;
    BOOL isActivityIndicator = [indicator isKindOfClass:[UIActivityIndicatorView class]];
    BOOL isRoundIndicator = [indicator isKindOfClass:[MBRoundProgressView class]];

    MBProgressHUDMode mode = self.mode;
    //菊花動畫
    if (mode == MBProgressHUDModeIndeterminate) {
        if (!isActivityIndicator) {
            // Update to indeterminate indicator
            [indicator removeFromSuperview];
            indicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
            [(UIActivityIndicatorView *)indicator startAnimating];
            [self.bezelView addSubview:indicator];
        }
    }
    //水平進度條動畫
    else if (mode == MBProgressHUDModeDeterminateHorizontalBar) {
        // Update to bar determinate indicator
        [indicator removeFromSuperview];
        indicator = [[MBBarProgressView alloc] init];
        [self.bezelView addSubview:indicator];
    }
    //圓形進度動畫
    else if (mode == MBProgressHUDModeDeterminate || mode == MBProgressHUDModeAnnularDeterminate) {
        if (!isRoundIndicator) {
            // Update to determinante indicator
            [indicator removeFromSuperview];
            indicator = [[MBRoundProgressView alloc] init];
            [self.bezelView addSubview:indicator];
        }
        //環(huán)形動畫
        if (mode == MBProgressHUDModeAnnularDeterminate) {
            [(MBRoundProgressView *)indicator setAnnular:YES];
        }
    }
    //自定義動畫
    else if (mode == MBProgressHUDModeCustomView && self.customView != indicator) {
        // Update custom view indicator
        [indicator removeFromSuperview];
        indicator = self.customView;
        [self.bezelView addSubview:indicator];
    }
    //只顯示文本
    else if (mode == MBProgressHUDModeText) {
        [indicator removeFromSuperview];
        indicator = nil;
    }
    indicator.translatesAutoresizingMaskIntoConstraints = NO;
    self.indicator = indicator;

    if ([indicator respondsToSelector:@selector(setProgress:)]) {
        [(id)indicator setValue:@(self.progress) forKey:@"progress"];
    }

    [indicator setContentCompressionResistancePriority:998.f forAxis:UILayoutConstraintAxisHorizontal];
    [indicator setContentCompressionResistancePriority:998.f forAxis:UILayoutConstraintAxisVertical];

    [self updateViewsForColor:self.contentColor];
    [self setNeedsUpdateConstraints];
}

在這個方法中音念,主要是根據(jù)顯示的模式,將不同的indicator視圖賦值給indicator屬性躏敢。更新完指示器后闷愤,就是開始將視圖顯示在界面上。調(diào)用的是- (void)showAnimated:(BOOL)animated方法父丰。


- (void)showAnimated:(BOOL)animated {
    //保證當前線程是主線程
    MBMainThreadAssert();
    [self.minShowTimer invalidate];
    self.useAnimation = animated;
    self.finished = NO;
    // 如果設(shè)置了寬限時間肝谭,則推遲HUD的顯示
    if (self.graceTime > 0.0) {
        NSTimer *timer = [NSTimer timerWithTimeInterval:self.graceTime
                                                 target:self
                                               selector:@selector(handleGraceTimer:)
                                               userInfo:nil
                                                repeats:NO];
        //默認把你的Timer以NSDefaultRunLoopMode添加到MainRunLoop上掘宪,而當當前視圖在滾動時,當前的MainRunLoop是處于UITrackingRunLoopMode的模式下攘烛,在這個模式下魏滚,是不會處理NSDefaultRunLoopMode的消息,要想在scrollView滾動的同時Timer也執(zhí)行的話坟漱,我們需要將Timer以NSRunLoopCommonModes的模式注冊到當前RunLoop中.
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
        self.graceTimer = timer;
    } 
    // ... otherwise show the HUD immediately
    else {
        [self showUsingAnimation:self.useAnimation];
    }
}

- (void)showAnimated:(BOOL)animated方法中鼠次,主要做的是判斷是否設(shè)置了推遲顯示HUD的時間,如果設(shè)置了芋齿,就推遲設(shè)置的時間再顯示腥寇。最后,執(zhí)行- (void)showUsingAnimation:(BOOL)animated方法觅捆。


- (void)showUsingAnimation:(BOOL)animated {
    // Cancel any previous animations
    [self.bezelView.layer removeAllAnimations];
    [self.backgroundView.layer removeAllAnimations];

    // Cancel any scheduled hideDelayed: calls
    [self.hideDelayTimer invalidate];
    //記錄當前顯示的時間赦役,在HUD隱藏時,比較HUD顯示到HUD隱藏之間的間隔與最小顯示時間栅炒,
    //如果小于掂摔,繼續(xù)顯示,直到顯示時間等于最小顯示時間赢赊,再隱藏HUD
    self.showStarted = [NSDate date];
    self.alpha = 1.f;

    // Needed in case we hide and re-show with the same NSProgress object attached.
    //好像是通過這個去刷新進度乙漓,這個需要再查下。
    [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;
    }
}

最后執(zhí)行- (void)animateIn:(BOOL)animatingIn withType:(MBProgressHUDAnimation)type completion:(void(^)(BOOL finished))completion方法释移,這個方法顯示和隱藏均會調(diào)用叭披。

//這個方法主要對self.bezelView視圖進行動畫
- (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;
    }

    // 使用動畫
    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;
#pragma clang diagnostic pop
        self.backgroundView.alpha = animatingIn ? 1.f : 0.f;
    };

    // Spring animations are nicer, but only available on iOS 7+
#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];
}

從代碼可以看出,這里只是對指示器的父視圖做了放大縮小的動畫玩讳。

隱藏HUD

+ (BOOL)hideHUDForView:(UIView *)view animated:(BOOL)animated {
    //獲取當前顯示的hud,如果存在涩蜘,當前隱藏時,將其從父視圖移除
    MBProgressHUD *hud = [self HUDForView:view];
    if (hud != nil) {
        hud.removeFromSuperViewOnHide = YES;
        [hud hideAnimated:animated];
        return YES;
    }
    return NO;
}

在這個方法的執(zhí)行過程中锋边,調(diào)用- (void)hideAnimated:(BOOL)animated方法皱坛。

 - (void)hideAnimated:(BOOL)animated {
    MBMainThreadAssert();
    [self.graceTimer invalidate];
    self.useAnimation = animated;
    self.finished = YES;
    // 如果設(shè)置了最小顯示時間编曼,計算HUD顯示時長豆巨,
    // 如果HUD顯示時長小于最小顯示時間,延遲顯示
    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
    [self hideUsingAnimation:self.useAnimation];
}

- (void)hideAnimated:(BOOL)animated方法中掐场,主要做的是判斷是否需要推遲隱藏HUD往扔,最后調(diào)用- (void)hideUsingAnimation:(BOOL)animated方法,


- (void)hideUsingAnimation:(BOOL)animated {
    //判斷是否需要動畫效果熊户,如無萍膛,則直接隱藏
    if (animated && self.showStarted) {
        self.showStarted = nil;
        //跟顯示HUD差不多,只是指示器父視圖沒有做放大縮小的動畫
        [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];
    }
}

最后嚷堡,調(diào)用- (void)done方法蝗罗。這個方法主要負責屬性的釋放和隱藏完成回調(diào)的處理艇棕。

- (void)done {
    // Cancel any scheduled hideDelayed: calls
    [self.hideDelayTimer invalidate];
    //指示進度的顯示問題,后續(xù)還需再補充
    [self setNSProgressDisplayLinkEnabled:NO];

    if (self.hasFinished) {
        self.alpha = 0.0f;
        if (self.removeFromSuperViewOnHide) {
            [self removeFromSuperview];
        }
    }
    MBProgressHUDCompletionBlock completionBlock = self.completionBlock;
    if (completionBlock) {
        completionBlock();
    }
    id<MBProgressHUDDelegate> delegate = self.delegate;
    if ([delegate respondsToSelector:@selector(hudWasHidden:)]) {
        [delegate performSelector:@selector(hudWasHidden:) withObject:self];
    }
}
總結(jié)

從代碼來看串塑,MBProgressHUD這個三方庫有幾個地方值借鑒:

  • graceTimeminShowTime沼琉,在開發(fā)的時候會出現(xiàn)顯示HUD后,存在緩存或者網(wǎng)速較好時桩匪,HUD顯示到HUD隱藏的時間較短打瘪,界面出現(xiàn)閃動的情況,這時傻昙,就可以通過設(shè)置graceTimeminShowTime來處理闺骚,達到更好的用戶體驗。 這個在封裝彈窗控件時妆档,可以參考僻爽。

本文已經(jīng)同步到我的個人技術(shù)博客: 傳送門 ,歡迎常來^^贾惦。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末进泼,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子纤虽,更是在濱河造成了極大的恐慌乳绕,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件逼纸,死亡現(xiàn)場離奇詭異洋措,居然都是意外死亡,警方通過查閱死者的電腦和手機杰刽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進店門菠发,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人贺嫂,你說我怎么就攤上這事滓鸠。” “怎么了第喳?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵糜俗,是天一觀的道長。 經(jīng)常有香客問我曲饱,道長悠抹,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任扩淀,我火速辦了婚禮楔敌,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘驻谆。我一直安慰自己卵凑,他們只是感情好庆聘,可當我...
    茶點故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著勺卢,像睡著了一般掏觉。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上值漫,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天澳腹,我揣著相機與錄音,去河邊找鬼杨何。 笑死酱塔,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的危虱。 我是一名探鬼主播羊娃,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼埃跷!你這毒婦竟也來了蕊玷?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤弥雹,失蹤者是張志新(化名)和其女友劉穎垃帅,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體剪勿,經(jīng)...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡贸诚,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了厕吉。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片酱固。...
    茶點故事閱讀 39,841評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖头朱,靈堂內(nèi)的尸體忽然破棺而出运悲,到底是詐尸還是另有隱情,我是刑警寧澤项钮,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布班眯,位于F島的核電站,受9級特大地震影響寄纵,放射性物質(zhì)發(fā)生泄漏鳖敷。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一程拭、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧棍潘,春花似錦恃鞋、人聲如沸崖媚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽畅哑。三九已至,卻和暖如春水由,著一層夾襖步出監(jiān)牢的瞬間荠呐,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工砂客, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留泥张,地道東北人。 一個月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓鞠值,卻偏偏與公主長得像媚创,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子彤恶,可洞房花燭夜當晚...
    茶點故事閱讀 44,781評論 2 354