本篇博文記錄MBProgressHUD源碼學習過程,從官方提供的Demo項目入手,一步步瞭解其代碼結構,學習它使用的技術,體會做者的編程思想。編程
咱們先來看下MBProgressHUD的結構,查看其類的定義。
1.MBProgressHUD是UIView的子類。
2.屬性:app
1. //代理,<MBProgressHUDDelegate>僅定義了一個方法:- (void)hudWasHidden:(MBProgressHUD *)hud;用於執行HUD隱藏以後的操做 @property (weak, nonatomic) id<MBProgressHUDDelegate> delegate; //執行HUD隱藏以後的操做的Block,目的同上 @property (copy, nullable) MBProgressHUDCompletionBlock completionBlock; 2. //延遲時間,若任務在graceTime到時以前就完成了,HUD再也不展現,即防止爲短期任務顯示HUD @property (assign, nonatomic) NSTimeInterval graceTime; //最短展現時間,防止HUD隱藏的過快 @property (assign, nonatomic) NSTimeInterval minShowTime; //配置HUD是否隱藏以後就從其superview上移除。默認NO @property (assign, nonatomic) BOOL removeFromSuperViewOnHide; 3. //指定進度條的樣式,包括菊花、圓餅、環形、水平進度條、自定義樣式和純文本等 @property (assign, nonatomic) MBProgressHUDMode mode; //內容(label+indicator+customView)顏色 @property (strong, nonatomic, nullable) UIColor *contentColor; //顯示和隱藏時的動畫類型:Fade(淡入淡出)、Zoom(放大顯示縮小隱藏)、ZoomIn、ZoomOut @property (assign, nonatomic) MBProgressHUDAnimation animationType; //內容框(bezelView)距離中心位置的偏移,例如CGPointMake(0.f, MBProgressMaxOffset),內容框會在底部居中 @property (assign, nonatomic) CGPoint offset; @property (assign, nonatomic) CGFloat margin;//各元素到HUD的邊距 @property (assign, nonatomic) CGSize minSize;//內容框的最小尺寸 @property (assign, nonatomic, getter = isSquare) BOOL square;//強制HUD爲方形 @property (assign, nonatomic, getter=areDefaultMotionEffectsEnabled) BOOL defaultMotionEffectsEnabled;//內容框(bezelView)是否受設備加速計的影響,默認YES 4. @property (assign, nonatomic) float progress;//進度 @property (strong, nonatomic, nullable) NSProgress *progressObject;//進度對象,用於更新進度條 5. //內容框,即展現實際內容(文本、indicator)的矩形框 @property (strong, nonatomic, readonly) MBBackgroundView *bezelView; //背景試圖,會覆蓋整個屏幕 @property (strong, nonatomic, readonly) MBBackgroundView *backgroundView; //自定義視圖用於展現 @property (strong, nonatomic, nullable) UIView *customView; @property (strong, nonatomic, readonly) UILabel *label;//文本 @property (strong, nonatomic, readonly) UILabel *detailsLabel;//文本下面的詳細文本 @property (strong, nonatomic, readonly) UIButton *button;//文本下面的action button
3.其餘相關類async
(1) MBBackgroundViewide
UIVisualEffectView
和UIBlurEffect
實現的。(2) MBRoundProgressView函數
(3) MBBarProgressView佈局
(4) MBProgressHUDRoundedButton學習
知識點:HUD中有個button屬性以下:動畫
/** * A button that is placed below the labels. Visible only if a target / action is added. */ @property (strong, nonatomic, readonly) UIButton *button;
注意它的註釋Visible only if a target / action is added
。也就是說,只有給button添加事件以後,該按鈕纔會展現出來。這是如何作到的呢?那就是重寫UIView的函數- (CGSize)intrinsicContentSize
:atom
- (CGSize)intrinsicContentSize { // Only show if we have associated control events if (self.allControlEvents == 0) return CGSizeZero; CGSize size = [super intrinsicContentSize]; // Add some side padding size.width += 20.f; return size; }
這個函數用來設置控件的內置尺寸。能夠看到,經過判斷allControlEvents
的個數來判斷button上是否有事件,若是有事件,就在原來內置的尺寸上加20。spa
瞭解了MBProgressHUD的基本結構以後,接下來咱們就看看具體的功能是如何實現的。HUDDemo提供了15個樣例,咱們選取純文本、加載(菊花)、條狀進度條和自定義視圖進行分析,其餘的樣例與它們相似。
咱們先從最簡單的純文本開始。啓動HUDDemo項目,點開MBHudDemoViewController.m
文件,找到函數- (void)textExample{…}
,這個函數就是顯示純文本的處理函數:
- (void)textExample { MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.navigationController.view animated:YES]; // Set the text mode to show only text. hud.mode = MBProgressHUDModeText; hud.label.text = NSLocalizedString(@"Message here!", @"HUD message title"); // Move to bottm center. hud.offset = CGPointMake(0.f, MBProgressMaxOffset); [hud hideAnimated:YES afterDelay:3.f]; }
① 進入到函數showHUDAddedTo:animated:
中查看MBProgressHUD實例的建立過程:
initWithView:
->initWithFrame:
->commonInit
使用self.navigationController.view
的bounds初始化HUD,而後在commonInit
裏指定動畫類型(Fade)、HUD模式(菊花)、間距(20)、內容顏色(黑色半透明)。除此以外,還設置HUD爲徹底透明,背景色爲clear,配置HUD的尺寸自動調整:
//保證上下間距比例不變、左右間距比例不變,即防止橫豎屏切換時HUD位置錯誤 self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; //讓HUD的各個子視圖本身控制本身的透明度,使其不受HUD透明度的影響 self.layer.allowsGroupOpacity = NO;
[self setupViews]
在這個函數中真正執行子視圖的建立工做。
背景視圖(backgroundView)
爲類MBBackgroundView
的實例。MBBackgroundView
實例默認會建立成白色半透明模糊效果,並覆蓋全屏,但在本例中,建立完成以後會更改其style
爲MBProgressHUDBackgroundStyleSolidColor
,並將背景色設置爲透明(clear)。
內容框(bezelView)
同爲類MBBackgroundView
實例,是實際展現內容的View(即中間的黑框),包含文本、indicator、進度條等。bezelView
會默認建立成白色半透明模糊效果,但frame爲0。建立後會設置其邊角半徑爲5。
知識點:做者爲bezelView添加了MotionEffect,也就是說在bezelView顯示的時候,它會根據手機的傾斜方向調整本身的位置!
- (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 }
label和detailsLabel
設置顯示文字的label,其中detailsLabel容許多行。
button
爲MBProgressHUDRoundedButton
的實例,做爲HUD上的功能按鈕,好比進度條下方能夠顯示一個"取消"按鈕。
topSpacer和bottomSpacer
均爲UIView
的實例,用於調節上下間距的輔助View。
設置label、detailsLabel及button的抗壓係數,並添加到父視圖上。
for (UIView *view in @[label, detailsLabel, button]) { view.translatesAutoresizingMaskIntoConstraints = NO;//本身手動管理約束 [view setContentCompressionResistancePriority:998.f forAxis:UILayoutConstraintAxisHorizontal];//設置水平抗壓縮係數,值越大,越不容易被壓縮 [view setContentCompressionResistancePriority:998.f forAxis:UILayoutConstraintAxisVertical];//設置垂直抗壓縮係數,值越大,越不容易被壓縮 [bezelView addSubview:view]; }
[self updateIndicators]
HUD的indicator
是UIView
的實例,用來記錄HUD上顯示的視圖,進度條、加載圖標(菊花)、自定義視圖等都是用HUD的indicator
屬性記錄的。在函數- (void)updaetIndicators
中,根據HUD的mode值配置不一樣的indicator。最後會調用[self setNeedsUpdateConstraints]
觸發約束更新函數-(void)updateConstraints
來更新UI。
[self registerForNotifications]
註冊通知,處理屏幕旋轉的問題。
② 在建立完HUD以後,會調用[hud showAnimated:animated];
將HUD展現到屏幕上。事實上,雖然當前HUD已經在屏幕上了,但因爲初始化HUD的時候bezelView的frame爲0,用戶看不到。
③ 配置HUD實例的屬性
hud.mode = MBProgressHUDModeText;//設置hud只顯示純文本 hud.label.text = NSLocalizedString(@"Message here!", @"HUD message title");//設置文本內容 hud.offset = CGPointMake(0.f, MBProgressMaxOffset);//設置hud相對於中心位置的偏移
在mode的setter函數中會調用- (void)updateIndicators
,根據mode的新值從新配置indicator,而後調用- (void)setNeedsUpdateConstraints
觸發-(void)updateConstraints
來更新UI。而在offset的setter函數中會直接調用- (void)setNeedsUpdateConstraints
觸發-(void)updateConstraints
來更新UI。
④ 在函數- (void)updateConstraints
中更新佈局:
NSLayoutConstraint
從新設置constraints//1.以屏幕中心爲基準,應用offset。priority = 998 CGPoint offset = self.offset; NSMutableArray *centeringConstraints = [NSMutableArray array]; //x [centeringConstraints addObject:[NSLayoutConstraint constraintWithItem:bezel attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterX multiplier:1.f constant:offset.x]]; //y [centeringConstraints addObject:[NSLayoutConstraint constraintWithItem:bezel attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterY multiplier:1.f constant:offset.y]]; //爲每一個constraints設置priority [self applyPriority:998.f toConstraints:centeringConstraints]; [self addConstraints:centeringConstraints]; //2.設置最小間距約束,priority = 999 NSMutableArray *sideConstraints = [NSMutableArray array]; [sideConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"|-(>=margin)-[bezel]-(>=margin)-|" options:0 metrics:metrics views:NSDictionaryOfVariableBindings(bezel)]]; [sideConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(>=margin)-[bezel]-(>=margin)-|" options:0 metrics:metrics views:NSDictionaryOfVariableBindings(bezel)]]; [self applyPriority:999.f toConstraints:sideConstraints]; [self addConstraints:sideConstraints]; //3.bezel的最小尺寸約束 priority = 997 CGSize minimumSize = self.minSize; if (!CGSizeEqualToSize(minimumSize, CGSizeZero)) { NSMutableArray *minSizeConstraints = [NSMutableArray array]; [minSizeConstraints addObject:[NSLayoutConstraint constraintWithItem:bezel attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationGreaterThanOrEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.f constant:minimumSize.width]]; [minSizeConstraints addObject:[NSLayoutConstraint constraintWithItem:bezel attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationGreaterThanOrEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.f constant:minimumSize.height]]; [self applyPriority:997.f toConstraints:minSizeConstraints]; [bezelConstraints addObjectsFromArray:minSizeConstraints]; } //4.方形約束 priority=997 if (self.square) { NSLayoutConstraint *square = [NSLayoutConstraint constraintWithItem:bezel attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:bezel attribute:NSLayoutAttributeWidth multiplier:1.f constant:0]; square.priority = 997.f; [bezelConstraints addObject:square]; } //5.根據margin和設置上下spacer的間距約束 [topSpacer addConstraint:[NSLayoutConstraint constraintWithItem:topSpacer attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationGreaterThanOrEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.f constant:margin]]; [bottomSpacer addConstraint:[NSLayoutConstraint constraintWithItem:bottomSpacer attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationGreaterThanOrEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.f constant:margin]]; [bezelConstraints addObject:[NSLayoutConstraint constraintWithItem:topSpacer attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:bottomSpacer attribute:NSLayoutAttributeHeight multiplier:1.f constant:0.f]]; //6.設置bezel子視圖(topSpacer、label、detailLabel、button、bottomSpacer)的約束 [subviews enumerateObjectsUsingBlock:^(UIView *view, NSUInteger idx, BOOL *stop) { // Center in bezel [bezelConstraints addObject:[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:bezel attribute:NSLayoutAttributeCenterX multiplier:1.f constant:0.f]]; // Ensure the minimum edge margin is kept [bezelConstraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"|-(>=margin)-[view]-(>=margin)-|" options:0 metrics:metrics views:NSDictionaryOfVariableBindings(view)]]; // Element spacing if (idx == 0) { // First, ensure spacing to bezel edge [bezelConstraints addObject:[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:bezel attribute:NSLayoutAttributeTop multiplier:1.f constant:0.f]]; } else if (idx == subviews.count - 1) { // Last, ensure spacing to bezel edge [bezelConstraints addObject:[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:bezel attribute:NSLayoutAttributeBottom multiplier:1.f constant:0.f]]; } if (idx > 0) { // Has previous NSLayoutConstraint *padding = [NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:subviews[idx - 1] attribute:NSLayoutAttributeBottom multiplier:1.f constant:0.f]; [bezelConstraints addObject:padding]; [paddingConstraints addObject:padding]; } }]; [bezel addConstraints:bezelConstraints]; self.bezelConstraints = bezelConstraints; self.paddingConstraints = [paddingConstraints copy]; [self updatePaddingConstraints];//在該函數裏,根據子視圖的可視性(hidden),設置子視圖的上下間距(爲4)
經過上面的priority能夠知道優先級:最小間距約束>bezel的偏移約束>bezel最小尺寸約束=方形約束。所以,若是你設置了hud.square = YES
,可是實際bezel並無變爲方形,則極可能是由於上面的這幾個約束之間存在衝突,系統採用了高優先級的約束而忽略了square約束。不信你能夠把square優先級改成1000試試看:)
知識點:這裏出現了一個宏NSDictionaryOfVariableBindings
,它能夠用來方便的建立NSDictionary:
UIView *view1 = [UIView new]; UIView *view2 = [UIView new]; NSDictionary *dict = NSDictionaryOfVariableBindings(view1,view2);//{@"view1":view1,@"view2":view2}
總結:
至此,咱們來總結下純文本HUD的整個建立及顯示流程:
- 調用
[MBProgressHUD showHUDAddedTo:self.navigationController.view animated:YES]
建立HUD實例:包括配置屬性默認值(動畫類型、HUD樣式、間距、內容顏色等),初始化view(backgroundView、bezelView、label、detailLabel、button、topSpacer、bottomSpacer),且會默認建立一個indicator。以後hud會顯示在屏幕上,但因爲約束未觸發,所以用戶看不到。hud.mode = MBProgressHUDModeText
。HUD會根據mode的值去隱藏indicator,並更新約束。hud.label.text = NSLocalizedString(@"Message here!", @"HUD message title")
設置要顯示的文字。hud.offset = CGPointMake(0.f, MBProgressMaxOffset)
設置bezelView的偏移屬性:讓其顯示在最底部。並更新約束。[hud hideAnimated:YES afterDelay:3.f]
設置一個延遲timer,在3s以後隱藏hud。隱藏以後調用completionBlock
和代理方法- (void)hudWasHidden:(MBProgressHUD *)hud;
。
加載樣式表現爲一個旋轉的菊花,底部也可包含"Loading…"等字樣提示。咱們以包含"Loading…"字樣的HUD爲例剖析其內部原理。代碼以下:
- (void)labelExample { MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.navigationController.view animated:YES]; // Set the label text. hud.label.text = NSLocalizedString(@"Loading...", @"HUD loading title"); // You can also adjust other label properties if needed. // hud.label.font = [UIFont italicSystemFontOfSize:16.f]; dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{ [self doSomeWork]; dispatch_async(dispatch_get_main_queue(), ^{ [hud hideAnimated:YES]; }); }); }
MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.navigationController.view animated:YES];
建立HUD實例,過程跟純文本是同樣的。hud.label.text = NSLocalizedString(@"Loading...", @"HUD loading title");
配置提示文案爲"Loading"。global_queue
裏面執行任務,完成任務以後回到主線程隱藏HUD。經過分析純文本HUD的建立過程咱們知道,hud在初始化的時候,它的mode默認爲MBProgressHUDModeIndeterminate
,也就是說單純的調用MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.navigationController.view animated:YES];
建立出來的HUD就是帶有菊花加載控件的HUD,咱們接下來作的就是給它的label賦上文案便可。
MBProgressHUD提供了三種樣式的進度條:條狀、餅狀、環狀。其中餅狀和環狀差很少,接下來咱們分析下條狀進度條的實現原理:
- (void)barDeterminateExample { MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.navigationController.view animated:YES]; // Set the bar determinate mode to show task progress. hud.mode = MBProgressHUDModeDeterminateHorizontalBar; hud.label.text = NSLocalizedString(@"Loading...", @"HUD loading title"); dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{ // Do something useful in the background and update the HUD periodically. [self doSomeWorkWithProgress]; dispatch_async(dispatch_get_main_queue(), ^{ [hud hideAnimated:YES]; }); }); }
MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.navigationController.view animated:YES];
建立HUD實例,過程跟純文本是同樣的。hud.mode = MBProgressHUDModeDeterminateHorizontalBar;
設置mode爲條狀進度條。在mode的setter方法中會調用- (void)updateIndicator
建立進度條indicator。MBBarProgressView
的實例。建立時默認寬爲120,高爲20,內容高度(intrinsicContentSize
)爲10。它的樣式是在- (void)drawRect
中繪製的。在其progress
屬性的setter方法中調用了-(void)setNeedsDisplay
從而觸發- (void)drawRect
來更新進度。hud.label.text = NSLocalizedString(@"Loading...", @"HUD loading title");
配置HUD提示文案爲"Loading"。MBProgressHUD提供了顯示自定義視圖的功能。在Demo中是展現一個對勾。
- (void)customViewExample { MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.navigationController.view animated:YES]; // Set the custom view mode to show any view. hud.mode = MBProgressHUDModeCustomView; // Set an image view with a checkmark. UIImage *image = [[UIImage imageNamed:@"Checkmark"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; hud.customView = [[UIImageView alloc] initWithImage:image]; // Looks a bit nicer if we make it square. hud.square = YES; // Optional label text. hud.label.text = NSLocalizedString(@"Done", @"HUD done title"); [hud hideAnimated:YES afterDelay:3.f]; }
MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.navigationController.view animated:YES];
建立HUD實例,過程跟純文本是同樣的。hud.mode = MBProgressHUDModeCustomView;
設置mode爲自定義視圖。接下來將須要展現的自定義視圖賦值給hud的customView
屬性。在customView
屬性的setter方法中會調用- (void)updateIndicators
將customView
添加到HUD上。hud.square = YES;
。hud.label.text = NSLocalizedString(@"Loading...", @"HUD loading title");
配置HUD提示文案爲"Loading"。至此,咱們已經簡單瞭解了MBProgressHUD的整個代碼結構及使用流程,這已經足夠咱們去建立和使用符合咱們需求的HUD了。但其實MBProgressHUD的源碼中還包含很多高級的技術細節,咱們將在下篇文章中一個個分析學習。