MBProgressHUD源码解析

一直以来都没有用心阅读三方源码,于是现在告诉自己每个月要认真看一下别人的源码,要行动起来。学习别人优秀的代码和编程思想其实很有必要的,本来想着仔细去看看SDWebImageAFNetworking这两个非常优秀的第三发源码,我们也经常用到,但是发现里面内容和文件很多,需要认真耐心的花费一定时间去仔细琢磨研究,
最后看到我们项目里用到的MBProgressHUD,也是一个比较优秀的第三方,文件相对较少,只有一个.h.m文件,于是从它来开始我的源码认真阅读之路了。

1:核心方法属性

1.1公开属性

这里列举一些MBProgressHUD公开属性的

//这个是调用show方法到真正显示HUD的时间,默认是0
@property (assign, nonatomic) NSTimeInterval graceTime;
//设置HUD显示的最短时间,防止HUD显示时间过短一闪而过
@property (assign, nonatomic) NSTimeInterval minShowTime;
//HUD显示和隐藏动画类型
@property (assign, nonatomic) MBProgressHUDAnimation animationType UI_APPEARANCE_SELECTOR;
//设置HUD窗口样式,默认是MBProgressHUDModeIndeterminate,菊花旋转
@property (assign, nonatomic) MBProgressHUDMode mode;
/**
相对于视图中心的偏移,可以通过CGPointMake(0.f, -MBProgressMaxOffset)让HUD显示在顶部,
CGPointMake(0.f, MBProgressMaxOffset)显示在底部
*/
@property (assign, nonatomic) CGPoint offset UI_APPEARANCE_SELECTOR; 

公开属性有很多,我只列举了一些,然后发现有些方法后面加了一个宏UI_APPEARANCE_SELECTOR,我自己平时很少用到,查了一下大概是
对于需要支持使用 appearance 来设置的属性,在属性后增加 UI_APPEARANCE_SELECTOR 宏声明即可。
文档中也有解释 UI_APPEARANCE_SELECTOR 用来标记属性用于外观代理,支持哪些类型。有一点需要注意的:appearance 生效是在被添加到视图树时,
在此之后设置 appearance,则不会起作用,而在手动设置属性之后被添加到视图树上,手动设置的会被覆盖
可参考
iOS UIAppearance 探秘.

1.2公开的类方法和对象方法
/**
在一个视图View之上显示HUD
*/
+ (instancetype)showHUDAddedTo:(UIView *)view animated:(BOOL)animated;
/**
隐藏一个View上的HUD,返回YES代表移除成功
*/
+ (BOOL)hideHUDForView:(UIView *)view animated:(BOOL)animated;
/**
找到View最顶层的HUD并返回
*/
+ (nullable MBProgressHUD *)HUDForView:(UIView *)view;
/**
HUD便利构造器,用一个View初始化HU
*/
- (instancetype)initWithView:(UIView *)view;
/**
是否动画显示显示HUD
*/
- (void)showAnimated:(BOOL)animated;
/**
是否动画隐藏HUD

@param animated <#animated description#>
*/
- (void)hideAnimated:(BOOL)animated;
/**
延迟delay之后隐藏HUD
*/
- (void)hideAnimated:(BOOL)animated afterDelay:(NSTimeInterval)delay;
1.3方法调用流程

我画了一个大概的流程图:


流程.png

如果需要在一个视图之上显示HUD,我们只需要简单调用一下+ (instancetype)showHUDAddedTo:(UIView *)view animated:(BOOL)animated就会出现一个HUD的界面,

show系列调用

+ (instancetype)showHUDAddedTo:(UIView *)view animated:(BOOL)animated {
    //页面布局
    MBProgressHUD *hud = [[self alloc] initWithView:view];
    //当隐藏HUD的时候将它从父视图移除
    hud.removeFromSuperViewOnHide = YES;
    [view addSubview:hud];
    //开启showHUD动画
    [hud showAnimated:animated];
    return hud;
}
- (void)showAnimated:(BOOL)animated {
    MBMainThreadAssert();
    //销毁HUD的minShowTimer定时器,这个是定时器是用来设置HUD最少显示时间,防止一闪而过
    [self.minShowTimer invalidate];
    //标记是否使用动画
    self.useAnimation = animated;
    //标记当前状态正在还未完成HUD的显示
    self.finished = NO;
    // 如果延迟显示时间>0,将timer添加到NSRunLoopCommonModes,防止timer暂停回调
    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;
    } 
    // ... otherwise show the HUD immediately
    else {
        [self showUsingAnimation:self.useAnimation];
    }
}
//self.graceTime调用的方法
- (void)handleGraceTimer:(NSTimer *)theTimer {
    if (!self.hasFinished) {
        [self showUsingAnimation:self.useAnimation];
    }
}
- (void)showUsingAnimation:(BOOL)animated {
    // Cancel any previous animations
    //移除视图动画
    [self.bezelView.layer removeAllAnimations];
    [self.backgroundView.layer removeAllAnimations];

    //延迟隐藏HUD的Timer销毁
    [self.hideDelayTimer invalidate];
    //记录开始显示HUD的时间
    self.showStarted = [NSDate date];
    self.alpha = 1.f;

    // Needed in case we hide and re-show with the same NSProgress object attached.
    //使用 CADisplayLink 来刷新progress的变化
    [self setNSProgressDisplayLinkEnabled:YES];

    if (animated) {
        [self animateIn:YES withType:self.animationType completion:NULL];
    } else {
        self.bezelView.alpha = 1.f;
        self.backgroundView.alpha = 1.f;
    }
}

如果需要在一个视图之上隐藏HUD,我们只需要简单调用一下+ (BOOL)hideHUDForView:(UIView *)view animated:(BOOL)animated
hide方法系列调用

 + (BOOL)hideHUDForView:(UIView *)view animated:(BOOL)animated {
    //获取View最上层的HUD
    MBProgressHUD *hud = [self HUDForView:view];
    if (hud != nil) {
        hud.removeFromSuperViewOnHide = YES;
        [hud hideAnimated:animated];
        return YES;
    }
    return NO;
}
+ (MBProgressHUD *)HUDForView:(UIView *)view {
    //枚举器法 倒序查找
    NSEnumerator *subviewsEnum = [view.subviews reverseObjectEnumerator];
    for (UIView *subview in subviewsEnum) {
        if ([subview isKindOfClass:self]) {
            MBProgressHUD *hud = (MBProgressHUD *)subview;
            if (hud.hasFinished == NO) {
                return hud;
            }
        }
    }
    return nil;
}
- (void)hideAnimated:(BOOL)animated {
    MBMainThreadAssert();
    //销毁定时器
    [self.graceTimer invalidate];
    self.useAnimation = animated;
    //标记HUD已经完成
    self.finished = YES;
    // If the minShow time is set, calculate how long the HUD was shown,
    // and postpone the hiding operation if necessary
    //如果开始Show的时间小于最小显示HUD时间,则到minShowTime才隐藏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];
}
//self.minShowTime触发方法
- (void)handleMinShowTimer:(NSTimer *)theTimer {
    [self hideUsingAnimation:self.useAnimation];
}
- (void)hideUsingAnimation:(BOOL)animated {
    //延迟隐藏Timer销毁
    [self.hideDelayTimer invalidate];
    //
    if (animated && self.showStarted) {
        self.showStarted = nil;
        [self animateIn:NO withType:self.animationType completion:^(BOOL finished) {
            //隐藏HUD完成做一些回调操作
            [self done];
        }];
    } else {
        self.showStarted = nil;
        self.bezelView.alpha = 0.f;
        self.backgroundView.alpha = 1.f;
         //隐藏HUD完成做一些回调操作
        [self done];
    }
}

不管是show还是hide最终都会走- (void)animateIn:(BOOL)animatingIn withType:(MBProgressHUDAnimation)type
这里贴出代码,可以看出这里用的是CGAffineTransform缩放和animateWithDuration动画,相对简单明了

- (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);

    //设置承载指示器和label等控件的bezelView属性
    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;
        }
        CGFloat alpha = animatingIn ? 1.f : 0.f;
        bezelView.alpha = alpha;
        self.backgroundView.alpha = alpha;
    };

    // 执行UIView动画
#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];
}

2:页面布局

当我们初始化HUD的时候会发现调用了commonInit方法,这个就是初始化页面布局相关的东西了,
贴出部分代码

  - (void)commonInit {
    ...//...代表省去的部分代码
    //设置视图不透明
      self.opaque = NO;
      self.backgroundColor = [UIColor clearColor];
      // Make it invisible for now
      //设置视图透明图为0.0
      self.alpha = 0.0f;
      //自动布局,自动调整宽度和高度,保证与superView、上下左右的距离不变
      self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
      self.layer.allowsGroupOpacity = NO;
      //设置视图布局
      [self setupViews];
      //更新指示器样式
      [self updateIndicators];
      //注册通知
      [self registerForNotifications];
  }

这里我们注意self.opaque = NO,这个属性我平时很少注意,从官方中得知
此属性为绘图系统提供了如何处理视图的提示。如果设置为YES,则绘图系统将视图视为完全不透明,这允许绘图系统优化某些绘图操作并提高性能。如果设置为NO,则绘图系统通常将视图与其他内容合成。此属性的默认值为YES。
不透明视图将使用完全不透明的内容填充其边界 - 即其的alpha值应为1。如果视图不透明且未填充其边界或包含完全或部分透明的内容,则结果是不可预测的。如果视图完全透明或部分透明,则应始终将此属性的值设置为NO。
可查看进一步说明.
还有一个需要注意:
GroupOpacity 开启离屏渲染的条件是:layer.opacity != 1.0并且有子 layer 或者背景图。
self.layer.allowsGroupOpacity = NO防止触发离屏渲染,进而影响性能。


我们接着看布局的实现:

1:先看setupViews方法,这个方法是用来生成子视图控件,里面用了updateBezelMotionEffects方法,使用了UIInterpolatingMotionEffect类使子视图随着屏幕倾斜移动,
这里就不贴代码了详细说明了,有兴趣的可以看对应源码方法。
2:updateIndicators方法,每次更新MBProgressHUDMode指示器样式都会调用,使用简单的if语句判断。

- (void)updateIndicators {
...
 MBProgressHUDMode mode = self.mode;
    if (mode == MBProgressHUDModeIndeterminate) {
        ...
    }
    else if (mode == MBProgressHUDModeDeterminateHorizontalBar) {
       ...
    }
    ...
}

3:基本布局说完,其实核心就是使用了自动布局,用的是NSLayoutConstraint来自动布局,主要涉及到的是updateConstraintsupdatePaddingConstraints方法.
主要功能:

  • 移除原有约束
  • bezel位于视图中心的约束和距离中心的偏移约束,最小尺寸约束,上下间隔约束等等
  • bezelView的子控件约束
- (void)updateConstraints {
   ...
if (self.indicator) [subviews insertObject:self.indicator atIndex:1];

    // 移除存在的视图
    [self removeConstraints:self.constraints];
    [topSpacer removeConstraints:topSpacer.constraints];
    [bottomSpacer removeConstraints:bottomSpacer.constraints];
    if (self.bezelConstraints) {
        [bezel removeConstraints:self.bezelConstraints];
        self.bezelConstraints = nil;
    }
    //设置中心偏移
     CGPoint offset = self.offset;
        NSMutableArray *centeringConstraints = [NSMutableArray array];
        [centeringConstraints addObject:[NSLayoutConstraint constraintWithItem:bezel attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterX multiplier:1.f constant:offset.x]];
        [centeringConstraints addObject:[NSLayoutConstraint constraintWithItem:bezel attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterY multiplier:1.f constant:offset.y]];
        [self applyPriority:998.f toConstraints:centeringConstraints];
        [self addConstraints:centeringConstraints];
    ...
    //正方形约束
    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];
        }
        ...
          // subviews 布局
            NSMutableArray *paddingConstraints = [NSMutableArray new];
            [subviews enumerateObjectsUsingBlock:^(UIView *view, NSUInteger idx, BOOL *stop) {
                // bezel处于中心位置的约束
                [bezelConstraints addObject:[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:bezel attribute:NSLayoutAttributeCenterX multiplier:1.f constant:0.f]];
               ...
            }];
            ....

}

3:绘图

绘图部分主要是使用Quartz2D,然后创建对应的视图,我们这里看下MBRoundProgressView,环形的进度条作为例子简单说一下。

圆弧进度条.png

- (void)drawRect:(CGRect)rect {
//获取当前绘图上下文
    CGContextRef context = UIGraphicsGetCurrentContext();
    BOOL isPreiOS7 = kCFCoreFoundationVersionNumber < kCFCoreFoundationVersionNumber_iOS_7_0;
    //是否是环形
    if (_annular) {
        // 绘制背景圆形边框
        CGFloat lineWidth = isPreiOS7 ? 5.f : 2.f;
        UIBezierPath *processBackgroundPath = [UIBezierPath bezierPath];
        processBackgroundPath.lineWidth = lineWidth;
        processBackgroundPath.lineCapStyle = kCGLineCapButt;
        CGPoint center = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
        CGFloat radius = (self.bounds.size.width - lineWidth)/2;
        CGFloat startAngle = - ((float)M_PI / 2); // 90 degrees
        CGFloat endAngle = (2 * (float)M_PI) + startAngle;
        //绘制圆形贝塞尔曲线
        [processBackgroundPath addArcWithCenter:center radius:radius startAngle:startAngle endAngle:endAngle clockwise:YES];
        [_backgroundTintColor set];
        // 绘制圆环路径
        [processBackgroundPath stroke];
        //绘制环形进度条
        UIBezierPath *processPath = [UIBezierPath bezierPath];
        processPath.lineCapStyle = isPreiOS7 ? kCGLineCapRound : kCGLineCapSquare;
        processPath.lineWidth = lineWidth;
        endAngle = (self.progress * 2 * (float)M_PI) + startAngle;
        //绘制圆形贝塞尔曲线
        [processPath addArcWithCenter:center radius:radius startAngle:startAngle endAngle:endAngle clockwise:YES];
        [_progressTintColor set];
        [processPath stroke];
    } else {
        // Draw background
        CGFloat lineWidth = 2.f;
        CGRect allRect = self.bounds;
        CGRect circleRect = CGRectInset(allRect, lineWidth/2.f, lineWidth/2.f);
        CGPoint center = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
        [_progressTintColor setStroke];
       //使用_backgroundTintColor颜色填充
        [_backgroundTintColor setFill];
        CGContextSetLineWidth(context, lineWidth);
        if (isPreiOS7) {
          //iOS7之前使用CGContextFillEllipseInRect方法,填充颜色
            CGContextFillEllipseInRect(context, circleRect);
        }
        CGContextStrokeEllipseInRect(context, circleRect);
        // 90 degrees
        CGFloat startAngle = - ((float)M_PI / 2.f);
        // 绘制进度条
        if (isPreiOS7) {
            CGFloat radius = (CGRectGetWidth(self.bounds) / 2.f) - lineWidth;
            CGFloat endAngle = (self.progress * 2.f * (float)M_PI) + startAngle;
            [_progressTintColor setFill];
            //绘制饼图
            CGContextMoveToPoint(context, center.x, center.y);
            CGContextAddArc(context, center.x, center.y, radius, startAngle, endAngle, 0);
            CGContextClosePath(context);
            CGContextFillPath(context);
        } else {
            UIBezierPath *processPath = [UIBezierPath bezierPath];
            processPath.lineCapStyle = kCGLineCapButt;
            processPath.lineWidth = lineWidth * 2.f;
            CGFloat radius = (CGRectGetWidth(self.bounds) / 2.f) - (processPath.lineWidth / 2.f);
            CGFloat endAngle = (self.progress * 2.f * (float)M_PI) + startAngle;
          //绘制圆形贝塞尔曲线
            [processPath addArcWithCenter:center radius:radius startAngle:startAngle endAngle:endAngle clockwise:YES];
            // Ensure that we don't get color overlapping when _progressTintColor alpha < 1.f.
            CGContextSetBlendMode(context, kCGBlendModeCopy);
            [_progressTintColor set];
            [processPath stroke];
        }
    }
}

最后

由于自己的之前很少仔细阅读三方源码,第一次分析的比较简陋,加上最近要离职了,精力有限暂时先分析到这了,不足之处后面会通过多阅读和分析三方源码提高自己。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 简书博客已经暂停更新,想看更多技术博客请到: 掘金 :J_Knight_ 个人博客: J_Knight_ 个人公众...
    J_Knight_阅读 5,950评论 36 38
  • HUD在iOS中一般特指“透明提示层”,常见的有SVProgressHUD、JGProgressHUD、Toast...
    foolishBoy阅读 1,174评论 0 2
  • MBProgressHUD 是一个为 APP 添加 HUD 窗口的第三方框架,使用起来极其简单方便,关于 MBPr...
    Q以梦为马阅读 3,067评论 5 27
  • 做过iOS开发的同学应该都使用过或者了解MBProgressHUD这个第三方框架,由于它对外接口简洁,只需要几句代...
    纯情_小火鸡阅读 321评论 0 0
  • 此时的夜,火树银花,此时的你,笑魇如花,此时的荧屏,欢声笑语,此时的愿望,旺年发发发。此时,别人在微信里用红包拜年...
    云端传奇阅读 180评论 4 1