FrameWork Learning-MBProgressHUD

 源碼來源: https://github.com/jdg/MBProgressHUD

版本:0.9.1

MBProgressHUD是一個顯示HUD窗口的第三方類庫,用於在執行一些後臺任務時,在程序中顯示一個表示進度的loading視圖和兩個可選的文本提示的HUD窗口。我想最多是應用在加載網絡數據的時候。其實蘋果官方自己有一個帶有此功能的類UIProgressHUD,只不過它是私有的,現在不讓用。至於實際的效果,可以看看github上工程給出的幾張圖例(貌似我這經常無法單獨打開圖片,所以就不在這貼圖片了),也可以運行一下Demo。

具體用法我們就不多說了,參考github上的說明就能用得很順的。本文主要還是從源碼的角度來分析一下它的具體實現。

模式

在分析實現代碼之前,我們先來看看MBProgressHUD中定義的MBProgressHUDMode枚舉。它用來表示HUD窗口的模式,即我們從效果圖中看到的幾種顯示樣式。其具體定義如下:

typedef enum {
  // 使用UIActivityIndicatorView來顯示進度,這是默認值
  MBProgressHUDModeIndeterminate,
  // 使用一個圓形餅圖來作爲進度視圖
  MBProgressHUDModeDeterminate,
  // 使用一個水平進度條
  MBProgressHUDModeDeterminateHorizontalBar,
  // 使用圓環作爲進度條
  MBProgressHUDModeAnnularDeterminate,
  // 顯示一個自定義視圖,通過這種方式,可以顯示一個正確或錯誤的提示圖
  MBProgressHUDModeCustomView,
  // 只顯示文本
  MBProgressHUDModeText
} MBProgressHUDMode;

通過設置MBProgressHUD的模式,我們可以使用MBProgressHUD自定義的表示進度的視圖來滿足我們的需求,也可以自定義這個進度視圖,當然還可以只顯示文本。在下面我們會討論源碼中是如何使用這幾個值的。

外觀

我們先來了解一下MBProgressHUD的基本組成。一個MBProgressHUD視圖主要由四個部分組成:

  1. loading動畫視圖(在此做個統稱,當然這個區域可以是自定義的一個UIImageView視圖)。這個視圖由我們設定的模式值決定,可以是菊花、進度條,也可以是我們自定義的視圖;
  2. 標題文本框(label):主要用於顯示提示的主題信息。這個文本框是可選的,通常位於loading動畫視圖的下面,且它是單行顯示。它會根據labelText屬性來自適應文本的大小(有一個長度上限),如果過長,則超出的部分會顯示爲”…“;
  3. 詳情文本框(detailsLabel)。如果覺得標題不夠詳細,或者有附屬信息,就可以將詳細信息放在這裏面顯示。該文本框對應的是顯示detailsLabelText屬性的值,它是可以多行顯示的。另外,詳情的顯示還依賴於labelText屬性的設置,只有labelText屬性被設置了,且不爲空串,纔會顯示detailsLabel;
  4. HUD背景框。主要是作爲上面三個部分的一個背景,用來突出上面三部分。

爲了讓我們更好地自定義這幾個部分,MBProgressHUD還提供了一些屬性,我們簡單瞭解一下:

// 背景框的透明度,默認值是0.8
@property (assign) float opacity;

// 背景框的顏色
// 需要注意的是如果設置了這個屬性,則opacity屬性會失效,即不會有半透明效果
@property (MB_STRONG) UIColor *color;

// 背景框的圓角半徑。默認值是10.0
@property (assign) float cornerRadius;

// 標題文本的字體及顏色
@property (MB_STRONG) UIFont* labelFont;
@property (MB_STRONG) UIColor* labelColor;

// 詳情文本的字體及顏色
@property (MB_STRONG) UIFont* detailsLabelFont;
@property (MB_STRONG) UIColor* detailsLabelColor;

// 菊花的顏色,默認是白色
@property (MB_STRONG) UIColor *activityIndicatorColor;

通過以上屬性,我們可以根據自己的需要來設置這幾個部分的外觀。

另外還有一個比較有意思的屬性是dimBackground,用於爲HUD窗口的視圖區域覆蓋上一層徑向漸變(radial gradient)層,其定義如下:

@property (assign) BOOL dimBackground;

讓我們來看看通過它,MBProgressHUD都做了些什麼。代碼如下:

- (void)drawRect:(CGRect)rect {
  ...
  if (self.dimBackground) {
    //Gradient colours
    size_t gradLocationsNum = 2;
    CGFloat gradLocations[2] = {0.0f, 1.0f};
    CGFloat gradColors[8] = {0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.75f}; 
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGGradientRef gradient = CGGradientCreateWithColorComponents(colorSpace, gradColors, gradLocations, gradLocationsNum);
    CGColorSpaceRelease(colorSpace);
    //Gradient center
    CGPoint gradCenter= CGPointMake(self.bounds.size.width/2, self.bounds.size.height/2);
    //Gradient radius
    float gradRadius = MIN(self.bounds.size.width , self.bounds.size.height) ;
    // 由中心向四周繪製漸變
    CGContextDrawRadialGradient (context, gradient, gradCenter,
                   0, gradCenter, gradRadius,
                   kCGGradientDrawsAfterEndLocation);
    CGGradientRelease(gradient);
  }
  ... 
}

這段代碼由中心向MBProgressHUD視圖的四周繪製了一個漸變層。當然,這裏的顏色值是寫死的,我們無法自行定義。有興趣的話,大家可以將這個屬性設置爲YES,看看實際的效果。

創建、佈局與繪製

除了繼承自UIView的-initWithFrame:初始化方法,MBProgressHUD還爲我們提供了兩個初始化方法,如下所示:

- (id)initWithWindow:(UIWindow *)window;

- (id)initWithView:(UIView *)view;

這兩個方法分別傳入一個UIWindow對象和一個UIView對象。傳入的視圖對象僅僅是做爲MBProgressHUD視圖定義其frame屬性的參照,而不會直接將MBProgressHUD視圖添加到傳入的視圖對象上。這個添加操作還得我們自行處理(當然,MBProgressHUD還提供了幾個便捷的類方法,我們下面會說明)。

MBProgressHUD提供了幾個屬性,可以讓我們控制HUD的佈局,這些屬性主要有以下幾個:

// HUD相對於父視圖中心點的x軸偏移量和y軸偏移量
@property (assign) float xOffset;
@property (assign) float yOffset;

// HUD各元素與HUD邊緣的間距
@property (assign) float margin;

// HUD背景框的最小大小
@property (assign) CGSize minSize;

// HUD的實際大小
@property (atomic, assign, readonly) CGSize size;

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

需要注意的是,MBProgressHUD視圖會充滿其父視圖的frame內,爲此,在MBProgressHUD的layoutSubviews方法中,還專門做了處理,如下代碼所示:

- (void)layoutSubviews {
  [super layoutSubviews];
  // Entirely cover the parent view
  UIView *parent = self.superview;
  if (parent) {
    self.frame = parent.bounds;
  }
  ...
}

也因此,當MBProgressHUD顯示時,它也會屏蔽父視圖的各種交互操作。

在佈局的過程中,會先根據我們要顯示的視圖計算出容納這些視圖所需要的總的寬度和高度。當然,會設置一個最大值。我們截取其中一段來看看:

CGRect bounds = self.bounds;
...
CGFloat remainingHeight = bounds.size.height - totalSize.height - kPadding - 4 * margin; 
CGSize maxSize = CGSizeMake(maxWidth, remainingHeight);
CGSize detailsLabelSize = MB_MULTILINE_TEXTSIZE(detailsLabel.text, detailsLabel.font, maxSize, detailsLabel.lineBreakMode);
totalSize.width = MAX(totalSize.width, detailsLabelSize.width);
totalSize.height += detailsLabelSize.height;
if (detailsLabelSize.height > 0.f && (indicatorF.size.height > 0.f || labelSize.height > 0.f)) {
  totalSize.height += kPadding;
}
totalSize.width += 2 * margin;
totalSize.height += 2 * margin;

之後,就開始從上到下放置各個視圖。在佈局代碼的最後,計算了一個size值,這是爲後面繪製背景框做準備的。

在上面的佈局代碼中,主要是處理了loading動畫視圖、標題文本框和詳情文本框,而HUD背景框主要是在drawRect:中來繪製的。背景框的繪製代碼如下:

// Center HUD
CGRect allRect = self.bounds;
// Draw rounded HUD backgroud rect
CGRect boxRect = CGRectMake(round((allRect.size.width - size.width) / 2) + self.xOffset,
  round((allRect.size.height - size.height) / 2) + self.yOffset, size.width, size.height);
float radius = self.cornerRadius;
CGContextBeginPath(context);
CGContextMoveToPoint(context, CGRectGetMinX(boxRect) + radius, CGRectGetMinY(boxRect));
CGContextAddArc(context, CGRectGetMaxX(boxRect) - radius, CGRectGetMinY(boxRect) + radius, radius, 3 * (float)M_PI / 2, 0, 0);
CGContextAddArc(context, CGRectGetMaxX(boxRect) - radius, CGRectGetMaxY(boxRect) - radius, radius, 0, (float)M_PI / 2, 0);
CGContextAddArc(context, CGRectGetMinX(boxRect) + radius, CGRectGetMaxY(boxRect) - radius, radius, (float)M_PI / 2, (float)M_PI, 0);
CGContextAddArc(context, CGRectGetMinX(boxRect) + radius, CGRectGetMinY(boxRect) + radius, radius, (float)M_PI, 3 * (float)M_PI / 2, 0);
CGContextClosePath(context);
CGContextFillPath(context);

這是最平常的繪製操作,在此不多做解釋。

我們上面講過MBProgressHUD提供了幾種窗口模式,這幾種模式的主要區別在於loading動畫視圖的展示。默認情況下,使用的是菊花(MBProgressHUDModeIndeterminate)。我們可以通過設置以下屬性,來改變loading動畫視圖:

@property (assign) MBProgressHUDMode mode;

對於其它幾種模式,MBProgressHUD專門我們提供了幾個視圖類。如果是進度條模式(MBProgressHUDModeDeterminateHorizontalBar),則使用的是MBBarProgressView類;如果是餅圖模式(MBProgressHUDModeDeterminate)或環形模式(MBProgressHUDModeAnnularDeterminate),則使用的是MBRoundProgressView類。上面這兩個類的主要操作就是在drawRect:中根據一些進度參數來繪製形狀,大家可以自己詳細看一下。

當然,我們還可以自定義loading動畫視圖,此時選擇的模式是MBProgressHUDModeCustomView。或者不顯示loading動畫視圖,而只顯示文本框(MBProgressHUDModeText)。

具體顯示哪一種loading動畫視圖,是在-updateIndicators方法中來處理的,其實現如下所示:

- (void)updateIndicators {
  BOOL isActivityIndicator = [indicator isKindOfClass:[UIActivityIndicatorView class]];
  BOOL isRoundIndicator = [indicator isKindOfClass:[MBRoundProgressView class]];
  if (mode == MBProgressHUDModeIndeterminate) {
    ...
  }
  else if (mode == MBProgressHUDModeDeterminateHorizontalBar) {
    // Update to bar determinate indicator
    [indicator removeFromSuperview];
    self.indicator = MB_AUTORELEASE([[MBBarProgressView alloc] init]);
    [self addSubview:indicator];
  }
  else if (mode == MBProgressHUDModeDeterminate || mode == MBProgressHUDModeAnnularDeterminate) {
    if (!isRoundIndicator) {
      ...
    }
    if (mode == MBProgressHUDModeAnnularDeterminate) {
      [(MBRoundProgressView *)indicator setAnnular:YES];
    }
  } 
  else if (mode == MBProgressHUDModeCustomView && customView != indicator) {
    ...
  } else if (mode == MBProgressHUDModeText) {
    ...
  }
}

顯示與隱藏

MBRoundProgressView爲我們提供了豐富的顯示與隱藏HUD窗口的。在分析這些方法之前,我們先來看看MBProgressHUD爲顯示與隱藏提供的一些屬性:

// HUD顯示和隱藏的動畫類型
@property (assign) MBProgressHUDAnimation animationType;

// HUD顯示的最短時間。設置這個值是爲了避免HUD顯示後立即被隱藏。默認值爲0
@property (assign) float minShowTime;

// 這個屬性設置了一個寬限期,它是在沒有顯示HUD窗口前被調用方法可能運行的時間。
// 如果被調用方法在寬限期內執行完,則HUD不會被顯示。
// 這主要是爲了避免在執行很短的任務時,去顯示一個HUD窗口。
// 默認值是0。只有當任務狀態是已知時,才支持寬限期。具體我們看實現代碼。
@property (assign) float graceTime;

// 這是一個標識位,標明執行的操作正在處理中。這個屬性是配合graceTime使用的。
// 如果沒有設置graceTime,則這個標識是沒有太大意義的。在使用showWhileExecuting:onTarget:withObject:animated:方法時,
// 會自動去設置這個屬性爲YES,其它情況下都需要我們自己手動設置。
@property (assign) BOOL taskInProgress;

// 隱藏時是否將HUD從父視圖中移除,默認是NO。
@property (assign) BOOL removeFromSuperViewOnHide;

// 進度指示器,從0.0到1.0,默認值爲0.0
@property (assign) float progress;

// 在HUD被隱藏後的回調
@property (copy) MBProgressHUDCompletionBlock completionBlock;

以上這些屬性都還好理解,可能需要注意的就是graceTime和taskInProgress的配合使用。在下面我們將會看看這兩個屬性的用法。

對於顯示操作,最基本的就是-show:方法(其它幾個顯示方法都會調用該方法來顯示HUD窗口),我們先來看看它的實現,

- (void)show:(BOOL)animated {
  useAnimation = animated;
  // If the grace time is set postpone the HUD display
  if (self.graceTime > 0.0) {
    self.graceTimer = [NSTimer scheduledTimerWithTimeInterval:self.graceTime target:self 
               selector:@selector(handleGraceTimer:) userInfo:nil repeats:NO];
  } 
  // ... otherwise show the HUD imediately 
  else {
    [self showUsingAnimation:useAnimation];
  }
}

可以看到,如果我們沒有設置graceTime屬性,則會立即顯示HUD;而如果設置了graceTime,則會創建一個定時器,並讓顯示操作延遲到graceTime所設定的時間再執行,而-handleGraceTimer:實現如下:

- (void)handleGraceTimer:(NSTimer *)theTimer {
    // Show the HUD only if the task is still running
    if (taskInProgress) {
        [self showUsingAnimation:useAnimation];
    }
}

可以看到,只有在設置了taskInProgress標識位爲YES的情況下,纔會去顯示HUD窗口。所以,如果我們要自己調用-show:方法的話,需要酌情考慮設置taskInProgress標識位。

除了-show:方法以外,MBProgressHUD還爲我們提供了一組顯示方法,可以讓我們在顯示HUD的同時,執行一些後臺任務,我們在此主要介紹兩個。其中一個是-showWhileExecuting:onTarget:withObject:animated:,它是基於target-action方式的調用,在執行一個後臺任務時顯示HUD,等後臺任務執行完成後再隱藏HUD,具體實現如下所示:

- (void)showWhileExecuting:(SEL)method onTarget:(id)target withObject:(id)object animated:(BOOL)animated {
  methodForExecution = method;
  targetForExecution = MB_RETAIN(target);
  objectForExecution = MB_RETAIN(object); 
  // Launch execution in new thread
  self.taskInProgress = YES;
  [NSThread detachNewThreadSelector:@selector(launchExecution) toTarget:self withObject:nil];
  // Show HUD view
  [self show:animated];
}
- (void)launchExecution {
  @autoreleasepool {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    // Start executing the requested task
    [targetForExecution performSelector:methodForExecution withObject:objectForExecution];
#pragma clang diagnostic pop
    // Task completed, update view in main thread (note: view operations should
    // be done only in the main thread)
    [self performSelectorOnMainThread:@selector(cleanUp) withObject:nil waitUntilDone:NO];
  }
}

可以看到,-showWhileExecuting:onTarget:withObject:animated:首先將taskInProgress屬性設置爲YES,這樣在調用-show:方法時,即使設置了graceTime,也確保能在任務完成之前顯示HUD。然後開啓一個新線程,來異步執行我們的後臺任務,最後去顯示HUD。

而在異步調用方法-launchExecution中,線程首先是維護了自己的一個@autoreleasepool,所以在我們自己的方法中,就不需要再去維護一個@autoreleasepool了。之後是去執行我們的任務,在任務完成之後,再回去主線程去執行清理操作,並隱藏HUD窗口。

另一個顯示方法是-showAnimated:whileExecutingBlock:onQueue:completionBlock:,它是基於GCD的調用,當block中的任務在指定的隊列中執行時,顯示HUD窗口,任務完成之後執行completionBlock操作,最後隱藏HUD窗口。我們來看看它的具體實現:

- (void)showAnimated:(BOOL)animated whileExecutingBlock:(dispatch_block_t)block onQueue:(dispatch_queue_t)queue
   completionBlock:(MBProgressHUDCompletionBlock)completion {
  self.taskInProgress = YES;
  self.completionBlock = completion;
  dispatch_async(queue, ^(void) {
    block();
    dispatch_async(dispatch_get_main_queue(), ^(void) {
      [self cleanUp];
    });
  });
  [self show:animated];
}

這個方法也是首先將taskInProgress屬性設置爲YES,然後開啓一個線程去執行block任務,最後主線程去執行清理操作,並隱藏HUD窗口。

對於HUD的隱藏,MBProgressHUD提供了兩個方法,一個是-hide:,另一個是-hide:afterDelay:,後者基於前者,所以我們主要來看看-hide:的實現:

- (void)hide:(BOOL)animated {
  useAnimation = animated;
  // If the minShow time is set, calculate how long the hud was shown,
  // and pospone the hiding operation if necessary
  if (self.minShowTime > 0.0 && showStarted) {
    NSTimeInterval interv = [[NSDate date] timeIntervalSinceDate:showStarted];
    if (interv < self.minShowTime) {
      self.minShowTimer = [NSTimer scheduledTimerWithTimeInterval:(self.minShowTime - interv) target:self 
                selector:@selector(handleMinShowTimer:) userInfo:nil repeats:NO];
      return;
    } 
  }
  // ... otherwise hide the HUD immediately
  [self hideUsingAnimation:useAnimation];
}

我們可以看到,在設置了minShowTime屬性並且已經顯示了HUD窗口的情況下,會去判斷顯示的時間是否小於minShowTime指定的時間,如果是,則會開啓一個定時器,等到顯示的時間到了minShowTime所指定的時間,纔會去隱藏HUD窗口;否則會直接去隱藏HUD窗口。

隱藏的實際操作主要是去做了些清理操作,包括根據設定的removeFromSuperViewOnHide值來執行是否從父視圖移除HUD窗口,以及執行completionBlock操作,還有就是執行代理的hudWasHidden:方法。這些操作是在私有方法-done裏面執行的,實現如下:

- (void)done {
  [NSObject cancelPreviousPerformRequestsWithTarget:self];
  isFinished = YES;
  self.alpha = 0.0f;
  if (removeFromSuperViewOnHide) {
    [self removeFromSuperview];
  }
#if NS_BLOCKS_AVAILABLE
  if (self.completionBlock) {
    self.completionBlock();
    self.completionBlock = NULL;
  }
#endif
  if ([delegate respondsToSelector:@selector(hudWasHidden:)]) {
    [delegate performSelector:@selector(hudWasHidden:) withObject:self];
  }
}

其它

MBProgressHUD的一些主要的代碼差不多已經分析完了,最後還有些邊邊角角的地方,一起來看看。

顯示和隱藏的便捷方法

除了上面描述的實例方法之外,MBProgressHUD還爲我們提供了幾個便捷顯示和隱藏HUD窗口的方法,如下所示:

+ (MB_INSTANCETYPE)showHUDAddedTo:(UIView *)view animated:(BOOL)animated

+ (BOOL)hideHUDForView:(UIView *)view animated:(BOOL)animated

+ (NSUInteger)hideAllHUDsForView:(UIView *)view animated:(BOOL)animated

方法的簽名已經很能說明問題了,在此不多描述。

部分屬性值的設置

對於部分屬性(主要是”外觀”一節中針對菊花、標題文本框和詳情文本框的幾個屬性值),爲了在設置將這些屬性時修改對應視圖的屬性,並沒有直接爲每個屬性生成一個setter,而是通過KVO來監聽這些屬性值的變化,再將這些值賦值給視圖的對應屬性,如下所示:

// 監聽的屬性數組
- (NSArray *)observableKeypaths {
  return [NSArray arrayWithObjects:@"mode", @"customView", @"labelText", @"labelFont", @"labelColor", ..., nil];
}
// 註冊KVO
- (void)registerForKVO {
  for (NSString *keyPath in [self observableKeypaths]) {
    [self addObserver:self forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:NULL];
  }
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
  if (![NSThread isMainThread]) {
    [self performSelectorOnMainThread:@selector(updateUIForKeypath:) withObject:keyPath waitUntilDone:NO];
  } else {
    [self updateUIForKeypath:keyPath];
  }
}
- (void)updateUIForKeypath:(NSString *)keyPath {
  if ([keyPath isEqualToString:@"mode"] || [keyPath isEqualToString:@"customView"] ||
    [keyPath isEqualToString:@"activityIndicatorColor"]) {
    [self updateIndicators];
  } else if ([keyPath isEqualToString:@"labelText"]) {
    label.text = self.labelText;
  } 
  ...
  [self setNeedsLayout];
  [self setNeedsDisplay];
}

代理

MBProgressHUD還爲我們提供了一個代理MBProgressHUDDelegate,這個代理中只提供了一個方法,即:

- (void)hudWasHidden:(MBProgressHUD *)hud;

這個代理方法是在隱藏HUD窗口後調用,如果此時我們需要在我們自己的實現中執行某些操作,則可以實現這個方法。

問題

MBProgressHUD爲我們提供了一個HUD窗口的很好的實現,不過個人在使用過程中,覺得它給我們提供的交互功能太少。其代理只提供了一個-hudWasHidden:方法,而且我們也無法通過點擊HUD來執行一些操作。在現實的需求中,可能存在這種情況:比如一個網絡操作,在發送請求等待響應的過程中,我們會顯示一個HUD窗口以顯示一個loading框。但如果我們想在等待響應的過程中,在當前視圖中取消這個網絡請求,就沒有相應的處理方式,MBProgressHUD沒有爲我們提供這樣的交互操作。當然這時候,我們可以根據自己的需求來修改源碼。

與SVProgressHUD的對比

與MBProgressHUD類似,SVProgressHUD類庫也爲我們提供了在視圖中顯示一個HUD窗口的功能。兩者的基本思路是差不多的,差別更多的是在實現細節上。相對於MBProgressHUD來說,SVProgressHUD的實現有以下幾點不同:

  1. SVProgressHUD類對外提供的都是類方法,包括顯示、隱藏、和視圖屬性設置都是使用類方法來操作。其內部實現爲一個單例對象,類方法實際是針對這個單例對象來操作的。
  2. SVProgressHUD主要包含三部分:loading視圖、提示文本框和背景框,沒有詳情文本框。
  3. SVProgressHUD默認提供了正確、錯誤和信息三種狀態視圖(與loading視圖同一位置,根據需要來設置)。當然MBProgressHUD中,也可以自定義視圖(customView)來顯示相應的狀態視圖。
  4. SVProgressHUD爲我們提供了更多的交互操作,包括點擊事件、顯示事件及隱藏事件。不過這些都是通過通知的形式向外發送,所以我們需要自己去監聽這些事件。
  5. SVProgressHUD中一些loading動畫是以Layer動畫的形式來實現的。

SVProgressHUD的實現細節還未詳細去看,有興趣的讀者可以去研究一下。這兩個HUD類庫各有優點,大家在使用時,可根據自己的需要和喜好來選擇。

小結

總體來說,MBProgressHUD的代碼相對樸實,簡單易懂,沒有什麼花哨難懂的東西。就技術點而言,也沒有太多複雜的技術,都是我們常用的一些東西。就使用而言,也是挺方便的,參考一下github上的使用指南就能很快上手。

發佈了42 篇原創文章 · 獲贊 9 · 訪問量 28萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章