iOS開發之原始碼解析 - MBProgressHUD

n以夢為馬發表於2017-12-25

MBProgressHUD 是一個為 APP 新增 HUD 視窗的第三方框架,使用起來極其簡單方便,關於 MBProgressHUD 的使用方法,GitHub 上有詳細的說明,這裡就不多加介紹了,本文主要是從原始碼的角度分析 MBProgressHUD的具體實現。

  • 先來對 MBProgressHUD 有個大體的認識,這是剛從 GitHub 上拉下來的程式碼,如下圖,MBProgressHUD 的主要檔案只有兩個:

MBProgressHUD 原始碼解析




下面我們就開始分析 MBProgressHUD 的具體實現。在此之前先了解下文章的目錄,本文主要有三個部分:

  1. MBProgressHUD 核心 API
  • 這一部分講的主要是 MBProgressHUD 的一些屬性方法等,主要是為了對 MBProgressHUD 先有個大概的認識
  1. show 系列方法
  • 這一部分主要是展示 HUD 視窗時呼叫的方法及程式碼分析
  1. hide 系列方法
  • 這一部分主要是隱藏 HUD 視窗時呼叫的方法及程式碼分析



begin~~~ ##一、MBProgressHUD 核心 API

這一部分講的主要是 MBProgressHUD 的一些屬性方法等,主要是為了對 MBProgressHUD 先有個大概的認識

####1.1 模式 首先來看看 MBProgressHUD 中定義的列舉,mode 一共有六種顯示樣式:

/// 顯示樣式
typedef NS_ENUM(NSInteger, MBProgressHUDMode) {
    /// 預設模式, 系統自帶的指示器
    MBProgressHUDModeIndeterminate,
    /// 圓形餅圖
    MBProgressHUDModeDeterminate,
    /// 水平進度條
    MBProgressHUDModeDeterminateHorizontalBar,
    /// 圓環
    MBProgressHUDModeAnnularDeterminate,
    /// 自定義檢視
    MBProgressHUDModeCustomView,
    /// 只顯示文字
    MBProgressHUDModeText
};
複製程式碼

簡單的效果圖如下(顏色尺寸等都可以優化,我這裡只是簡單地示例):

預設模式:hud.mode = MBProgressHUDModeIndeterminate

圓形餅圖:hud.mode = MBProgressHUDModeDeterminate

水平進度條:hud.mode = MBProgressHUDModeDeterminateHorizontalBar

圓環:hud.mode = MBProgressHUDModeAnnularDeterminate

自定義檢視:hud.mode = MBProgressHUDModeCustomView

只顯示文字:hud.mode = MBProgressHUDModeText



1.2 動畫效果

MBProgressHUD 在顯示 HUD 視窗的時候,一般都伴隨著動畫效果,MBProgressHUD 中的動畫效果也是一個列舉,如下:

typedef NS_ENUM(NSInteger, MBProgressHUDAnimation) {
    ///  預設效果,只有透明度變化
    MBProgressHUDAnimationFade,
    /// 透明度變化 + 形變 (放大時出現縮小消失)
    MBProgressHUDAnimationZoom,
    /// 透明度變化 + 形變 (縮小)
    MBProgressHUDAnimationZoomOut,
    /// 透明度變化 + 形變 (放大)
    MBProgressHUDAnimationZoomIn
};
複製程式碼

這裡先簡單的羅列出來,下文中還會多次用到。

####1.3 MBProgressHUD 組成 MBProgressHUD 主要由四部分組成:loading 動畫檢視標題文字框詳情文字框HUD 背景框,如下圖。

MBProgressHUD 組成

之前用 MBProgressHUD 設定標題文字詳情文字是通過幾個屬性來實現的,功能少也較為繁瑣,因此被遺棄了;現在設定標題文字詳情文字等十分簡便,直接通過 label 等控制元件就可以實現,而且在功能上也有很大的擴充套件,詳情請看下面這個程式碼塊:

/// bezelView 是指包括文字和指示器的檢視,和自定義的 customView 類似
@property (strong, nonatomic, readonly) MBBackgroundView *bezelView;
/// backgroundView 背景檢視
@property (strong, nonatomic, readonly) MBBackgroundView *backgroundView;
/// customView 自定義檢視
@property (strong, nonatomic, nullable) UIView *customView;
/// label 指的是標題文字
@property (strong, nonatomic, readonly) UILabel *label;
/// detailsLabel指的是詳情文字
@property (strong, nonatomic, readonly) UILabel *detailsLabel;
/// hud 視窗還可以加入button,新增事件
@property (strong, nonatomic, readonly) UIButton *button;
複製程式碼

另外這裡再附加兩張 MBProgressHUD 的整體佈局圖,以便更好地認識 MBProgressHUD

MBProgressHUD 的整體佈局圖 1

MBProgressHUD 的整體佈局圖 2

簡單介紹

  • backgroundView:整個背景圖層,可以通過 MBBackgroundView 的 style 屬性設定
  • bezelView:提供元素 (indicator、label、detailLabel、button)的背景
  • indicator:指示器顯示進度情況 這個檢視由我們設定的mode屬性決定
  • label:顯示標題文字
  • detailLabel:顯示詳情文字
  • button:新增點選事件

1.4 MBProgressHUD 中的屬性

MBProgressHUD 檔案中主要包括四個類,它們分別是 MBProgressHUDMBRoundProgressViewMBBarProgressViewMBBackgroundView。這四個類各有各的用法,比如如果是進度條模式(MBProgressHUDModeDeterminateHorizontalBar),則使用的是 MBBarProgressView 類;如果是餅圖模式(MBProgressHUDModeDeterminate)或環形模式(MBProgressHUDModeAnnularDeterminate),則使用的是 MBRoundProgressView類。下面是這四個類的相關屬性。

1.41 MBProgressHUD 相關屬性
/// show 方法觸發到顯示 HUD 視窗的間隔時間,預設是 0
@property (assign, nonatomic) NSTimeInterval graceTime;

/// HUD 視窗顯示的最短時間,預設是 0
@property (assign, nonatomic) NSTimeInterval minShowTime;

/// HUD 視窗顯示模式, 預設是系統自帶的指示器
@property (assign, nonatomic) MBProgressHUDMode mode;

/// 進度條指示器以及文字的顏色
@property (strong, nonatomic, nullable) UIColor *contentColor UI_APPEARANCE_SELECTOR;

/// HUD 視窗顯示和隱藏的動畫型別MBProgressHUD
@property (assign, nonatomic) MBProgressHUDAnimation animationType UI_APPEARANCE_SELECTOR;

/// HUD 視窗位置設定,比如 hud.offset = CGPointMake(0.f, MBProgressMaxOffset),可以移到底部中心位置
@property (assign, nonatomic) CGPoint offset UI_APPEARANCE_SELECTOR;

/// HUD 元素到 HUD 邊緣的距離,預設是 20.f
@property (assign, nonatomic) CGFloat margin UI_APPEARANCE_SELECTOR;

/// HUD 視窗背景框的最小尺寸
@property (assign, nonatomic) CGSize minSize UI_APPEARANCE_SELECTOR;

/// 是否強制 HUD 背景框寬高相等
@property (assign, nonatomic, getter = isSquare) BOOL square UI_APPEARANCE_SELECTOR;

/// 進度條 (0.0 到 1.0)
@property (nonatomic, assign) float progress;

/// bezelView 是指包括文字和指示器的檢視,和自定義的 customView 類似
@property (strong, nonatomic, readonly) MBBackgroundView *bezelView;

/// backgroundView 背景檢視
@property (strong, nonatomic, readonly) MBBackgroundView *backgroundView;

/// customView 自定義檢視
@property (strong, nonatomic, nullable) UIView *customView;

/// label 指的是標題文字
@property (strong, nonatomic, readonly) UILabel *label;

/// detailsLabel指的是詳情文字
@property (strong, nonatomic, readonly) UILabel *detailsLabel;

/// hud 視窗還可以加入button,新增事件
@property (strong, nonatomic, readonly) UIButton *button;
複製程式碼
1.42 MBRoundProgressView 相關屬性
/// 進度條 (0.0 到 1.0)
@property (nonatomic, assign) float progress;

/// 進度條顏色
@property (nonatomic, strong) UIColor *progressColor;

/// 進度條指示器的顏色
@property (nonatomic, strong) UIColor *progressTintColor;

/// 進度條指示器的背景顏色,只適用在 iOS7 以上,預設為半透明的白色 (透明度 0.1)
@property (nonatomic, strong) UIColor *backgroundTintColor;

/// 顯示模式,NO = 圓形;YES = 環形。預設是 NO
@property (nonatomic, assign, getter = isAnnular) BOOL annular;
複製程式碼
1.43 MBBarProgressView 相關屬性
/// 進度條 (0.0 到 1.0)
@property (nonatomic, assign) float progress;

/// 進度條邊界線的顏色,預設是白色
@property (nonatomic, strong) UIColor *lineColor;

/// 進度條背景色,預設是透明
@property (nonatomic, strong) UIColor *progressRemainingColor;

/// 進度條顏色
@property (nonatomic, strong) UIColor *progressColor;
複製程式碼
1.44 MBBackgroundView 相關屬性
/// 背景圖層樣式,有兩種,iOS7 或者以上版本預設風格是MBProgressHUDBackgroundStyleBlur,其他為MBProgressHUDBackgroundStyleSolidColor,由於 iOS7 不支援 UIVisualEffectView,所以在 iOS7 和更高版本中會有所不同
@property (nonatomic) MBProgressHUDBackgroundStyle style;

/// 背景顏色,由於 iOS7 不支援 UIVisualEffectView,所以在 iOS7 和更高版本中會有所不同
@property (nonatomic, strong) UIColor *color;
複製程式碼



####1.5 MBProgressHUD 中的一些方法

######1.51 類方法

/// 建立一個 HUD 視窗,並把它顯示在 view 上,還可以設定是否有動畫
+ (instancetype)showHUDAddedTo:(UIView *)view animated:(BOOL)animated;

/// 找到最上層的 HUD subview 並把它隱藏,成功為YES、其他情況為 NO
+ (BOOL)hideHUDForView:(UIView *)view animated:(BOOL)animated;

/// 返回最上層的 HUD subview
+ (nullable MBProgressHUD *)HUDForView:(UIView *)view;
複製程式碼

這三個類方法中,常用的是第一個函式+ (instancetype)showHUDAddedTo:(UIView *)view animated:(BOOL)animated;直接建立 HUD,並把它顯示在 view 上,用起來極其方便

######1.52 物件方法

/// 以view為基準建立初始化一個HUD物件,為HUD的初始化建構函式
- (instancetype)initWithView:(UIView *)view;

/// 顯示HUD控制元件,此函式應該在主執行緒中呼叫
- (void)showAnimated:(BOOL)animated;

/// 隱藏HUD控制元件,animated控制是否顯示動畫。對應於- (void)showAnimated:(BOOL)animated;
- (void)hideAnimated:(BOOL)animated;

/// 在delay時間之後隱藏HUD,animated控制顯示動畫與否,delay控制延遲時間
- (void)hideAnimated:(BOOL)animated afterDelay:(NSTimeInterval)delay;
複製程式碼

這幾個物件方法中,常用的也有兩個- (void)hideAnimated:(BOOL)animated;- (void)hideAnimated:(BOOL)animated afterDelay:(NSTimeInterval)delay;



二、show 系列方法

這一部分主要是展示 HUD 視窗時呼叫的方法及程式碼分析

下面這個方法在我們建立 MBProgressHUD 物件時首先呼叫

+ (instancetype)showHUDAddedTo:(UIView *)view animated:(BOOL)animated {

	/// 建立並初始化 MBProgressHUD 物件,根據傳進來的 view 來設定
    MBProgressHUD *hud = [[self alloc] initWithView:view];
    
    /// 移除 HUD 視窗
    hud.removeFromSuperViewOnHide = YES;
    
    /// 新增到 View 上,並顯示
    [view addSubview:hud];
    [hud showAnimated:animated];
    return hud;
}
複製程式碼

這個方法會呼叫兩個主要方法:- (id)initWithView:(UIView *)view- (void)showAnimated:(BOOL)animated,具體的呼叫流程如下圖:

show 相關的方法呼叫

當然在 MBProgressHUD 中,+ (instancetype)showHUDAddedTo:(UIView *)view animated:(BOOL)animated 呼叫的方法遠不止上圖列的這些,圖上列的只是幾個主要方法。接下來我們就根據程式的執行過程來一步一步分析一下程式碼。

在方法 - (id)initWithView:(UIView *)view中,呼叫 - (instancetype)initWithFrame:(CGRect)frame,接著會呼叫- (void)commonInit

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

- (instancetype)initWithFrame:(CGRect)frame {
    if ((self = [super initWithFrame:frame])) {
        [self commonInit];
    }
    return self;
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    if ((self = [super initWithCoder:aDecoder])) {
        [self commonInit];
    }
    return self;
}

- (void)commonInit {

    /// 預設效果, 透明度變化
    _animationType = MBProgressHUDAnimationFade;
    
    /// 預設模式, 系統自帶的指示器
    _mode = MBProgressHUDModeIndeterminate;
    
    /// HUD 元素到 HUD 邊緣的距離,預設是 20.f
    _margin = 20.0f;
    _opacity = 1.f;
    _defaultMotionEffectsEnabled = YES;

    // 預設顏色,根據當前的 iOS 版本
    BOOL isLegacy = kCFCoreFoundationVersionNumber < kCFCoreFoundationVersionNumber_iOS_7_0;
    
    /// 進度條指示器以及文字的顏色
    _contentColor = isLegacy ? [UIColor whiteColor] : [UIColor colorWithWhite:0.f alpha:0.7f];

	/// opaque 類似 Alpha,表示當前 UIView 的不透明度,設定是否之後對於 UIView 的顯示並沒有什麼影響,官方文件的意思是 opaque 預設為 YES,如果 alpha 小於 1,那麼應該設定 opaque 設定為 NO,當 alpha 為 1,opaque設定為 NO
    self.opaque = NO;
    
    /// 背景色
    self.backgroundColor = [UIColor clearColor];
    
    // 透明度為 0 
    self.alpha = 0.0f;
    /// 自動調整子控制元件與父控制元件之間的寬高
    self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    self.layer.allowsGroupOpacity = NO;

	/// 設定所需的子view
    [self setupViews];
    /// 設定指示器樣式
    [self updateIndicators];
    /// 註冊通知
    [self registerForNotifications];
}

複製程式碼

上面程式碼塊中的程式碼已經加過註釋,因此在這裡不再累述某句程式碼有什麼作用,這裡直接說程式的執行流程。當程式執行到 - (void)commonInit 這個方法時,會相繼執行- (void)setupViews- (void)updateIndicators- (void)registerForNotifications 這三個方法,當然在執行這三個方法期間,也會執行其它的方法,比如會執行- (void)updateForBackgroundStyle- (void)updateBezelMotionEffects等等,這和你設定的 mode 的模式,以及和 label,detailsLabel ,button 這一系列元素,以及和相應的屬性都有一定的關係。

接著我們來分析一下 - (void)setupViews- (void)updateIndicators- (void)registerForNotifications 這三個方法。

- (void)setupViews {

	/// 進度條指示器以及文字的顏色
    UIColor *defaultColor = self.contentColor;
    

	/// 建立背景檢視
    MBBackgroundView *backgroundView = [[MBBackgroundView alloc] initWithFrame:self.bounds];
    
    /// 背景圖層樣式
    backgroundView.style = MBProgressHUDBackgroundStyleSolidColor;
    backgroundView.backgroundColor = [UIColor clearColor];
    
    /// 自動調整 view 的寬度和高度
    backgroundView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    backgroundView.alpha = 0.f;
    [self addSubview:backgroundView];
    _backgroundView = backgroundView;

	/// 建立背景檢視(和上面那個大小不同)
    MBBackgroundView *bezelView = [MBBackgroundView new];
    
    /// 程式碼層面使用 Autolayout,需要對使用的 View 的translatesAutoresizingMaskIntoConstraints 屬性設定為NO
    bezelView.translatesAutoresizingMaskIntoConstraints = NO;
    bezelView.layer.cornerRadius = 5.f;
    bezelView.alpha = 0.f;
    [self addSubview:bezelView];
    _bezelView = bezelView;
    
    /// 呼叫 updateBezelMotionEffects 方法,設定視差效果
    [self updateBezelMotionEffects];
    

	/// 建立 label 標籤,顯示主要文字
    UILabel *label = [UILabel new];
    
    /// 取消文字大小自適應
    label.adjustsFontSizeToFitWidth = NO;
    label.textAlignment = NSTextAlignmentCenter;
    label.textColor = defaultColor;
    label.font = [UIFont boldSystemFontOfSize:MBDefaultLabelFontSize];
    
    /// opaque 類似 Alpha,表示當前 UIView 的不透明度,設定是否之後對於 UIView 的顯示並沒有什麼影響,官方文件的意思是 opaque 預設為 YES,如果 alpha 小於 1,那麼應該設定 opaque 設定為 NO,當 alpha 為 1,opaque設定為 NO
    label.opaque = NO;
    label.backgroundColor = [UIColor clearColor];
    _label = label;
	
	
	/// 建立 detailsLabel 標籤,顯示詳細資訊
	
    UILabel *detailsLabel = [UILabel new];
    /// 取消文字大小自適應
    detailsLabel.adjustsFontSizeToFitWidth = NO;
    detailsLabel.textAlignment = NSTextAlignmentCenter;
    detailsLabel.textColor = defaultColor;
    detailsLabel.numberOfLines = 0;
    detailsLabel.font = [UIFont boldSystemFontOfSize:MBDefaultDetailsLabelFontSize];
    
    /// opaque 類似 Alpha,表示當前 UIView 的不透明度,設定是否之後對於 UIView 的顯示並沒有什麼影響,官方文件的意思是 opaque 預設為 YES,如果 alpha 小於 1,那麼應該設定 opaque 設定為 NO,當 alpha 為 1,opaque設定為 NO
    detailsLabel.opaque = NO;
    detailsLabel.backgroundColor = [UIColor clearColor];
    _detailsLabel = detailsLabel;
    

	/// 建立 button 按鈕,並新增響應按鈕
    UIButton *button = [MBProgressHUDRoundedButton buttonWithType:UIButtonTypeCustom];
    button.titleLabel.textAlignment = NSTextAlignmentCenter;
    button.titleLabel.font = [UIFont boldSystemFontOfSize:MBDefaultDetailsLabelFontSize];
    [button setTitleColor:defaultColor forState:UIControlStateNormal];
    _button = button;
	
	
	/// 將 label,detailLabel,button 新增到蒙版檢視
    for (UIView *view in @[label, detailsLabel, button]) {
    
    	/// 程式碼層面使用 Autolayout,需要對使用的 View 的translatesAutoresizingMaskIntoConstraints 屬性設定為NO
        view.translatesAutoresizingMaskIntoConstraints = NO;
        
        /// 為檢視設定水平方向上優先順序為 998 的壓縮阻力
        [view setContentCompressionResistancePriority:998.f forAxis:UILayoutConstraintAxisHorizontal];
        
        /// 為檢視設定垂直方向上優先順序為 998 的壓縮阻力
        [view setContentCompressionResistancePriority:998.f forAxis:UILayoutConstraintAxisVertical];
        [bezelView addSubview:view];
    }
	
	
	/// 建立頂部檢視
    UIView *topSpacer = [UIView new];
    
    /// 程式碼層面使用 Autolayout,需要對使用的 View 的translatesAutoresizingMaskIntoConstraints 屬性設定為NO
    topSpacer.translatesAutoresizingMaskIntoConstraints = NO;
    topSpacer.hidden = YES;
    [bezelView addSubview:topSpacer];
    _topSpacer = topSpacer;
	
	/// 建立底部檢視
    UIView *bottomSpacer = [UIView new];
    
        /// 程式碼層面使用 Autolayout,需要對使用的 View 的translatesAutoresizingMaskIntoConstraints 屬性設定為NO
    bottomSpacer.translatesAutoresizingMaskIntoConstraints = NO;
    bottomSpacer.hidden = YES;
    [bezelView addSubview:bottomSpacer];
    _bottomSpacer = bottomSpacer;
}

- (void)updateIndicators {
    UIView *indicator = self.indicator;
    
    /// 判斷當前指示器是否是 UIActivityIndicatorView
    BOOL isActivityIndicator = [indicator isKindOfClass:[UIActivityIndicatorView class]];
    
    /// 判斷當前指示器是否是 MBRoundProgressView
    BOOL isRoundIndicator = [indicator isKindOfClass:[MBRoundProgressView class]];

    MBProgressHUDMode mode = self.mode;
    /// MBProgressHUDModeIndeterminate:系統自帶的指示器
    if (mode == MBProgressHUDModeIndeterminate) {
        if (!isActivityIndicator) {
             // 如果當前指示器不屬於 UIActivityIndicatorView 型別,則移除之前的indicator,重新建立
            [indicator removeFromSuperview];
            indicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
            [(UIActivityIndicatorView *)indicator startAnimating];
            [self.bezelView addSubview:indicator];
        }
    }
    else if (mode == MBProgressHUDModeDeterminateHorizontalBar) {
        /// 如果當前指示器不屬於 MBBarProgressView 型別,則移除之前的indicator,重新建立
        [indicator removeFromSuperview];
        indicator = [[MBBarProgressView alloc] init];
        [self.bezelView addSubview:indicator];
    }
    else if (mode == MBProgressHUDModeDeterminate || mode == MBProgressHUDModeAnnularDeterminate) {
        if (!isRoundIndicator) {
            /// 如果當前指示器不屬於 MBRoundProgressView 型別,則移除之前的indicator,重新建立
            [indicator removeFromSuperview];
            indicator = [[MBRoundProgressView alloc] init];
            [self.bezelView addSubview:indicator];
        }
        if (mode == MBProgressHUDModeAnnularDeterminate) { /// 圓環指示器
            [(MBRoundProgressView *)indicator setAnnular:YES];
        }
    } 
    else if (mode == MBProgressHUDModeCustomView && self.customView != indicator) { /// 自定義檢視指示器
        [indicator removeFromSuperview];
        indicator = self.customView;
        [self.bezelView addSubview:indicator];
    }
    else if (mode == MBProgressHUDModeText) { /// 文字形式,去除指示器檢視
        [indicator removeFromSuperview];
        indicator = nil;
    }
        /// 程式碼層面使用 Autolayout,需要對使用的 View 的translatesAutoresizingMaskIntoConstraints 屬性設定為NO
    indicator.translatesAutoresizingMaskIntoConstraints = NO;
    self.indicator = indicator;

    if ([indicator respondsToSelector:@selector(setProgress:)]) {
    	/// 設定進度條的數值
        [(id)indicator setValue:@(self.progress) forKey:@"progress"];
    }
	
	
	 /// 為檢視設定水平方向上優先順序為 998 的壓縮阻力
    [indicator setContentCompressionResistancePriority:998.f forAxis:UILayoutConstraintAxisHorizontal];
    
    /// 為檢視設定垂直方向上優先順序為 998 的壓縮阻力
    [indicator setContentCompressionResistancePriority:998.f forAxis:UILayoutConstraintAxisVertical];
	
	/// 設定控制元件顏色
    [self updateViewsForColor:self.contentColor];
    /// 更新佈局
    [self setNeedsUpdateConstraints];
}

- (void)registerForNotifications {
#if !TARGET_OS_TV
    NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
	/// 通過通知 UIApplicationDidChangeStatusBarOrientationNotification 來處理螢幕轉屏事件
    [nc addObserver:self selector:@selector(statusBarOrientationDidChange:)
               name:UIApplicationDidChangeStatusBarOrientationNotification object:nil];
#endif
}

複製程式碼
  • 由上面程式碼我們可以看出,在方法- (void)setupViews中,建立了 backgroundView、bezelView、label、detailsLabel、button 這幾個控制元件,並使用 for 迴圈把 label、detailsLabel、button 新增到bezelView 檢視中,最後還建立了頂部檢視和底部檢視,不過預設是隱藏的。有一點值得說明,在建立 button 時並沒有設定 button 的 size 等屬性,那麼這個按鈕是不會顯示的。在這裡 MBProgressHUD 重寫了一個 Unbutton 的子類 MBProgressHUDRoundedButton。這個子類裡面有一個方法,- (CGSize)intrinsicContentSize,通過這個方法來設定 Unbutton 的 size。
- (CGSize)intrinsicContentSize {
    /// 只有當有事件才顯示(這裡也告訴我們,如果這個 button 沒有任何事件的話,它的大小就是 CGSizeZero,即不會顯示)
    if (self.allControlEvents == 0) return CGSizeZero;
    CGSize size = [super intrinsicContentSize];
    // Add some side padding
    size.width += 20.f;
    return size;
}
複製程式碼
  • - (void)updateIndicators這個方法主要是用來設定 indicator 指示器的,根據 mode 的屬性顯示不同的形式,具體可以參看程式碼註釋。這個方法最後呼叫的是setNeedsUpdateConstraints函式,這個函式是系統自帶的方法,它會自動呼叫- (void)updateConstraints 方法,- (void)updateConstraints 主要作用是更新各個控制元件的佈局,我們稍後再對這個方法進行詳細分析。

  • - (void)registerForNotifications這個方法中的程式碼量很少,它的作用是通過通知 UIApplicationDidChangeStatusBarOrientationNotification 來處理螢幕轉屏事件

- (void)registerForNotifications這一系列方法執行完畢之後,程式會重新返回到+ (instancetype)showHUDAddedTo:(UIView *)view animated:(BOOL)animated這個方法中,接著呼叫另一個主要函式- (void)showAnimated:(BOOL)animated

- (void)showAnimated:(BOOL)animated {
	
	/// 顯示放在主執行緒中
    MBMainThreadAssert();
    
    /// 取消定時器
    [self.minShowTimer invalidate];
    self.useAnimation = animated;
    self.finished = NO;
    
    /// 如果設定了寬限時間graceTime,則延遲顯示(避免 HUD 一閃而過的差體驗)
    if (self.graceTime > 0.0) {
    
    	/// 建立定時器,把它加入 NSRunLoop 中 
        NSTimer *timer = [NSTimer timerWithTimeInterval:self.graceTime target:self selector:@selector(handleGraceTimer:) userInfo:nil repeats:NO];
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
        self.graceTimer = timer;
    } 
    
    /// 沒有設定 graceTime,則直接顯示
    else {
        [self showUsingAnimation:self.useAnimation];
    }
}

/// 設定寬限時間 graceTime時呼叫的方法
- (void)handleGraceTimer:(NSTimer *)theTimer {
    // Show the HUD only if the task is still running
    if (!self.hasFinished) {
        [self showUsingAnimation:self.useAnimation];
    }
}

- (void)showUsingAnimation:(BOOL)animated {
    /// 移除所有動畫
    [self.bezelView.layer removeAllAnimations];
    [self.backgroundView.layer removeAllAnimations];

    /// 取消 hideDelayTimer
    [self.hideDelayTimer invalidate];

    /// 開始時間
    self.showStarted = [NSDate date];
    self.alpha = 1.f;

    /// 以防我們隱藏 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;
    }
}
複製程式碼

由上面這段程式碼我們可以看出,在方法- (void)showAnimated:(BOOL)animated中,無論我們有沒有設定graceTime這個屬性,最後都會去執行一個方法 - (void)showUsingAnimation:(BOOL)animated- (void)showUsingAnimation:(BOOL)animated 這個方法在上面已經做過註釋,不再細說,不過有兩小點值得我們注意,第一點是 - (void)showUsingAnimation:(BOOL)animated 在執行過程中呼叫了一個方法 - (void)setNSProgressDisplayLinkEnabled:(BOOL)enabled,先來看下這個方法

- (void)setNSProgressDisplayLinkEnabled:(BOOL)enabled {
    
    /// 這裡使用 CADisplayLink,是因為如果使用 KVO 機制會非常消耗主執行緒(因為 NSProgress 頻率非常快)
    if (enabled && self.progressObject) {
        /// 建立 CADisplayLink 物件
        if (!self.progressObjectDisplayLink) {
            self.progressObjectDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateProgressFromProgressObject)];
        }
    } else {
        self.progressObjectDisplayLink = nil;
    }
}
複製程式碼

這個方法是關於 CADisplayLink 的,CADisplayLink 是一個能讓我們以和螢幕重新整理率相同的頻率將內容畫到螢幕上的定時器。我們在應用中建立一個新的 CADisplayLink 物件,把它新增到一個runloop 中,並給它提供一個 targetselector 在螢幕重新整理的時候呼叫。一旦 CADisplayLink 以特定的模式註冊到 runloop 之後,每當螢幕需要重新整理,runloop 就會向 CADisplayLink 指定的target 傳送一次指定的 selector 訊息, CADisplayLink 類對應的 selector 就會被呼叫一次。

- (void)showUsingAnimation:(BOOL)animated 這個方法中還有一點值得注意,就是隻有具有動畫效果的前提下,即 animated 為真時才會呼叫 - (void)animateIn:(BOOL)animatingIn withType:(MBProgressHUDAnimation)type completion:(void(^)(BOOL finished))completion 這個方法,下面我們再一起來看下這個方法。

/// animated 為真時呼叫,消失或出現時的伸縮效果,以及透明度
- (void)animateIn:(BOOL)animatingIn withType:(MBProgressHUDAnimation)type completion:(void(^)(BOOL finished))completion {
    /// 自動確定正確的縮放動畫型別,關於 MBProgressHUDAnimation 的幾種型別,上文已全部列出,這裡不再詳細介紹
    if (type == MBProgressHUDAnimationZoom) {
        type = animatingIn ? MBProgressHUDAnimationZoomIn : MBProgressHUDAnimationZoomOut;
    }
    
    /// CGAffineTransformMakeScale 中的兩個引數,分別代表x和y方向縮放倍數
    CGAffineTransform small = CGAffineTransformMakeScale(0.5f, 0.5f);
    CGAffineTransform large = CGAffineTransformMakeScale(1.5f, 1.5f);

    /// 設定初始狀態
    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;
    };

    /// 動畫的兩種形式,>= iOS7 的是一種形式,iOS7以下是另一種
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000 || TARGET_OS_TV
    if (kCFCoreFoundationVersionNumber >= kCFCoreFoundationVersionNumber_iOS_7_0) {
        /// 只支援 >= iOS7
        [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];
}
複製程式碼

- (void)animateIn:(BOOL)animatingIn withType:(MBProgressHUDAnimation)type completion:(void(^)(BOOL finished))completion 這個方法,無論是處於 show 狀態還是處於 hide 狀態,都會呼叫,下邊我們再一起看下 hide 系列的一些方法。

三、hide 系列方法

這一部分主要是隱藏 HUD 視窗時呼叫的方法及程式碼分析

關於隱藏 HUD 視窗,MBProgressHUD 給我們提供的方法有以下幾個:

/// 找到最上層的 HUD subview 並把它隱藏,成功為YES、其他情況為 NO
+ (BOOL)hideHUDForView:(UIView *)view animated:(BOOL)animated;

//隱藏HUD控制元件,animated控制是否顯示動畫。對應於- (void)showAnimated:(BOOL)animated;
- (void)hideAnimated:(BOOL)animated;

//在delay時間之後隱藏HUD,animated控制顯示動畫與否,delay控制延遲時間
- (void)hideAnimated:(BOOL)animated afterDelay:(NSTimeInterval)delay;
複製程式碼

最常用的是後面兩個: - (void)hideAnimated:(BOOL)animated- (void)hideAnimated:(BOOL)animated afterDelay:(NSTimeInterval)delay,這兩個方法的本質是相同的,不同的只是形式,也就是說這兩個方法的實現流程基本上是一致的,只不過 - (void)hideAnimated:(BOOL)animated afterDelay:(NSTimeInterval)delay 多執行一兩個方法而已。下面我們就來具體分析下 hide 系列的方法。

  • 首先還是來說說+ (BOOL)hideHUDForView:(UIView *)view animated:(BOOL)animated這個函式,如果呼叫這個方法來隱藏 HUD 視窗,那麼會先呼叫兩個方法:
+ (BOOL)hideHUDForView:(UIView *)view animated:(BOOL)animated {
	/// 獲取當前 view 的最上面的 HUD
    MBProgressHUD *hud = [self HUDForView:view];
    if (hud != nil) {
    	/// 移除 HUD 視窗
        hud.removeFromSuperViewOnHide = YES;
        [hud hideAnimated:animated];
        return YES;
    }
    return NO;
}

+ (MBProgressHUD *)HUDForView:(UIView *)view {
	/// NSEnumerator 是一個列舉器,依附於集合類(NSArray,NSSet,NSDictionary等),reverseObjectEnumerator 倒序遍歷
    NSEnumerator *subviewsEnum = [view.subviews reverseObjectEnumerator];
    for (UIView *subview in subviewsEnum) {
        if ([subview isKindOfClass:self]) {
            return (MBProgressHUD *)subview;
        }
    }
    return nil;
}
複製程式碼

當執行完上面這兩個方法之後,接下來執行的方法和呼叫- (void)hideAnimated:(BOOL)animated隱藏 HUD 視窗時執行的方法相同,所以下邊會詳細分析。

  • 接下來說說呼叫- (void)hideAnimated:(BOOL)animated afterDelay:(NSTimeInterval)delay 隱藏 HUD 視窗時的情況,上文已經說過,呼叫這個方法會比呼叫- (void)hideAnimated:(BOOL)animated 多執行一兩個方法:
- (void)hideAnimated:(BOOL)animated afterDelay:(NSTimeInterval)delay {
    /// 建立定時器,並把它加入到 NSRunLoop 中
    NSTimer *timer = [NSTimer timerWithTimeInterval:delay target:self selector:@selector(handleHideTimer:) userInfo:@(animated) repeats:NO];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    self.hideDelayTimer = timer;
}

- (void)handleHideTimer:(NSTimer *)timer {
    [self hideAnimated:[timer.userInfo boolValue]];
}
複製程式碼

由上面程式碼可以清晰的看出,- (void)hideAnimated:(BOOL)animated afterDelay:(NSTimeInterval)delay 這個方法中加了一個定時器,當執行完這個定時器的selector時,就會執行- (void)hideAnimated:(BOOL)animated方法。

  • 由此可見無論使用哪種方法隱藏 HUD 視窗,最終都會來到這個方法,- (void)hideAnimated:(BOOL)animated,接下來我們就來分析下這個方法的具體呼叫流程,先看個圖:

hide 相關的方法呼叫

上圖顯示的是 hide 相關的方法呼叫,只羅列了幾個主要方法。接下來我們就來分析下這幾個主要方法。先來到- (void)hideAnimated:(BOOL)animated方法中:

- (void)hideAnimated:(BOOL)animated {
    MBMainThreadAssert();
    [self.graceTimer invalidate];
    self.useAnimation = animated;
    self.finished = YES;
    
    /// 如果設定了最小顯示時間,則執行此步驟,否則直接隱藏
    if (self.minShowTime > 0.0 && self.showStarted) {
        NSTimeInterval interv = [[NSDate date] timeIntervalSinceDate:self.showStarted];
        
        /// 如果 minShowTime 比較大,則暫時不觸發 HUD 的隱藏,而是啟動一個 NSTimer
        if (interv < self.minShowTime) {
            /// 建立定時器,並把它加入到 NSRunLoop 中
            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;
        } 
    }
    /// 直接隱藏 HUD
    [self hideUsingAnimation:self.useAnimation];
}

- (void)handleMinShowTimer:(NSTimer *)theTimer {
    [self hideUsingAnimation:self.useAnimation];
}
複製程式碼

從上面程式碼塊中可以看出,無論我們有沒有設定最小顯示時間 self.minShowTime,都會觸發 - (void)hideUsingAnimation:(BOOL)animated 這個方法,因此程式最後都會來到 - (void)hideUsingAnimation:(BOOL)animated 這個方法中:

- (void)hideUsingAnimation:(BOOL)animated {
    if (animated && self.showStarted) {
        /// 將 showStarted 設為 nil
        self.showStarted = nil;
        [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];
    }
}
複製程式碼

這個方法和 show 系列的 - (void)showUsingAnimation:(BOOL)animated 方法一樣,只要設定 animated 的屬性為 YES,最終都會走到 - (void)animateIn:(BOOL)animatingIn withType:(MBProgressHUDAnimation)type completion:(void(^)(BOOL finished))completion 這個方法中,同時會執行一個方法:- (void)done,接下來我們來看一下這兩個方法:

/// animated 為真時呼叫,消失或出現時的伸縮效果,以及透明度
- (void)animateIn:(BOOL)animatingIn withType:(MBProgressHUDAnimation)type completion:(void(^)(BOOL finished))completion {
    /// 自動確定正確的縮放動畫型別,關於 MBProgressHUDAnimation 的幾種型別,上文已全部列出,這裡不再詳細介紹
    if (type == MBProgressHUDAnimationZoom) {
        type = animatingIn ? MBProgressHUDAnimationZoomIn : MBProgressHUDAnimationZoomOut;
    }
    
    /// CGAffineTransformMakeScale 中的兩個引數,分別代表x和y方向縮放倍數
    CGAffineTransform small = CGAffineTransformMakeScale(0.5f, 0.5f);
    CGAffineTransform large = CGAffineTransformMakeScale(1.5f, 1.5f);

    /// 設定初始狀態
    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;
    };

    /// 動畫的兩種形式,>= iOS7 的是一種形式,iOS7以下是另一種
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000 || TARGET_OS_TV
    if (kCFCoreFoundationVersionNumber >= kCFCoreFoundationVersionNumber_iOS_7_0) {
        /// 只支援 >= iOS7
        [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];
}

- (void)done {
    /// 取消 hideDelayTimer
    [self.hideDelayTimer invalidate];
    /// 隱藏 NSProgress 物件
    [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];
    }
}

複製程式碼

關於 - (void)animateIn:(BOOL)animatingIn withType:(MBProgressHUDAnimation)type completion:(void(^)(BOOL finished))completion 這個方法,只要 animated 的屬性為 YES,都會呼叫;而在- (void)done 這個方法中,如果 removeFromSuperViewOnHide 屬性為 YES,則將自己從父檢視上移除;如果有 completionBlock 回撥函式,則執行回撥;如果實現了代理並實現了代理方法,則執行代理方法。而且我們還觀察到在 hide 時,也會呼叫 - (void)setNSProgressDisplayLinkEnabled:(BOOL)enabled 方法,只是在 hide 時 enabled 為 NO。

- (void)setNSProgressDisplayLinkEnabled:(BOOL)enabled {
    
    /// 這裡使用 CADisplayLink,是因為如果使用 KVO 機制會非常消耗主執行緒(因為 NSProgress 頻率非常快)
    if (enabled && self.progressObject) {
        /// 建立 CADisplayLink 物件
        if (!self.progressObjectDisplayLink) {
            self.progressObjectDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateProgressFromProgressObject)];
        }
    } else {
        self.progressObjectDisplayLink = nil;
    }
}
複製程式碼

over


以上便是對 MBProgressHUD 原始碼的一些總結和認識,如果有不足之處,還希望各位道友能多指點哈!


相關文章