MBProcessHUD-源码分析与仿写(三)

前言

阅读优秀的开源项目是提高编程能力的有效手段,我们能够从中开拓思维、拓宽视野,学习到很多不同的设计思想以及最佳实践。阅读他人代码很重要,但动手仿写、练习却也是很有必要的,它能进一步加深我们对项目的理解,将这些东西内化为自己的知识和能力。然而真正做起来却很不容易,开源项目阅读起来还是比较困难,需要一些技术基础和耐心。
本系列将对一些著名的iOS开源类库进行深入阅读及分析,并仿写这些类库的基本实现,加深我们对底层实现的理解和认识,提升我们iOS开发的编程技能。

MBProcessHUD

MBProcessHUD是一个iOS上的提示框库,支持加载提示、进度框、文字提示等,使用简单,功能强大,还能够自定义显示内容,广泛应用于iOS app中。这是它的地址:https://github.com/jdg/MBProgressHUD
简单看一下界面效果:

Paste_Image.png

实现原理

MBProcessHUD继承自UIView,实际上是一个覆盖全屏的半透明指示器组件。它由以下几个部分构成,分别是:Loading加载动画,标题栏,背景栏以及其它栏(如详情栏、按钮)。我们把MBProcessHUD添加到页面上,显示任务进度及提示信息,同时屏蔽用户交互操作。
MBProcessHUD的Loading加载动画来自系统类UIActivityIndicatorView,在页面加载时,开启转圈动画,页面销毁时取消转圈动画。
MBProcessHUD根据加载内容动态布局,它通过计算需要显示的内容,动态调整页面元素的位置大小,放置到屏幕的中央,显示的内容可以由使用者指定。MBProcessHUD v1.0版之前是通过frame计算各个元素的位置,最新的版本采用了约束布局。
MBProcessHUD使用KVO监听一些属性值的变化,如labelText,model。这些属性被修改时,MBProcessHUD视图相应更新,传入新值。

仿写MBProcessHUD

我们模仿MBProcessHUD写一个简单的弹出框组件,以加深对它的理解。在这个demo中,我们不完全重写MBProcessHUD,只实现基本功能。
首先在demo中创建ZCJHUD,继承UIView。

Paste_Image.png

在ZCJHUD头文件中,定义几种显示模式

typedef NS_ENUM(NSInteger, ZCJHUDMode) {
    /** 转圈动画模式,默认值 */
    ZCJHUDModeIndeterminate,
    /** 只显示标题 */
    ZCJHUDModeText
};

定义对外的接口,显示模式mode,标题内容labelText
@interface ZCJHUD : UIView

@property (nonatomic, assign) ZCJHUDMode mode;

@property (nonatomic, strong) NSString *labelText;

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

- (void)show;

- (void)hide;

@end

自身初始化,设置组件默认属性,更新布局,注册kvo监视属性变化。
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
_mode = ZCJHUDModeIndeterminate;
_labelText = nil;
_size = CGSizeZero;
self.opaque = NO;
self.backgroundColor = [UIColor clearColor];
self.alpha = 0;

        [self setupView];
        [self updateIndicators];
        [self registerForKVO];
    }
    return self;
}

初始化转圈动画,并添加到hud上,ZCJHUDModeIndeterminate模式才有这个动画
- (void)updateIndicators {
BOOL isActivityIndicator = [_indicator isKindOfClass:[UIActivityIndicatorView class]];

    if (_mode == ZCJHUDModeIndeterminate) {
        if (!isActivityIndicator) {
            // Update to indeterminate indicator
            [_indicator removeFromSuperview];
            self.indicator = ([[UIActivityIndicatorView alloc]
                                             initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge]);
            [(UIActivityIndicatorView *)_indicator startAnimating];
            [self addSubview:_indicator];
        }
    } else if (_mode == ZCJHUDModeText) {
        [_indicator removeFromSuperview];
        self.indicator = nil;
    }
}

两个主要方法,显示和隐藏hud

-(void)show {
    self.alpha = 1;
}

-(void)hide {
    self.alpha = 0;
    [self removeFromSuperview];
}

这里使用了frame动态布局
- (void)layoutSubviews {
[super layoutSubviews];

    // 覆盖整个视图,屏蔽交互操作
    UIView *parent = self.superview;
    if (parent) {
        self.frame = parent.bounds;
    }
    CGRect bounds = self.bounds;
    
    
    CGFloat maxWidth = bounds.size.width - 4 * kMargin;
    CGSize totalSize = CGSizeZero;
    
    CGRect indicatorF = _indicator.bounds;
    indicatorF.size.width = MIN(indicatorF.size.width, maxWidth);
    totalSize.width = MAX(totalSize.width, indicatorF.size.width);
    totalSize.height += indicatorF.size.height;
    
    CGSize labelSize = MB_TEXTSIZE(_label.text, _label.font);
    labelSize.width = MIN(labelSize.width, maxWidth);
    totalSize.width = MAX(totalSize.width, labelSize.width);
    totalSize.height += labelSize.height;
    if (labelSize.height > 0.f && indicatorF.size.height > 0.f) {
        totalSize.height += kPadding;
    }
    
    totalSize.width += 2 * kMargin;
    totalSize.height += 2 * kMargin;
    
    // Position elements
    CGFloat yPos = round(((bounds.size.height - totalSize.height) / 2)) + kMargin;
    CGFloat xPos = 0;
    indicatorF.origin.y = yPos;
    indicatorF.origin.x = round((bounds.size.width - indicatorF.size.width) / 2) + xPos;
    _indicator.frame = indicatorF;
    yPos += indicatorF.size.height;
    
    if (labelSize.height > 0.f && indicatorF.size.height > 0.f) {
        yPos += kPadding;
    }
    CGRect labelF;
    labelF.origin.y = yPos;
    labelF.origin.x = round((bounds.size.width - labelSize.width) / 2) + xPos;
    labelF.size = labelSize;
    _label.frame = labelF;
    
    _size = totalSize;
}

绘制背景框
- (void)drawRect:(CGRect)rect {

    CGContextRef context = UIGraphicsGetCurrentContext();
    UIGraphicsPushContext(context);
    
    
    CGContextSetGrayFillColor(context, 0.0f, 0.8);
    
    // Center HUD
    CGRect allRect = self.bounds;
    // Draw rounded HUD backgroud rect
    CGRect boxRect = CGRectMake(round((allRect.size.width - _size.width) / 2),
                                round((allRect.size.height - _size.height) / 2) , _size.width, _size.height);
    float radius = 10;
    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);
    
    UIGraphicsPopContext();
}

kvo监控属性变化,使用者在修改属性时,触发页面刷新,赋上新值。注意在页面销毁时要取消kvo监控,否则程序会崩溃

#pragma mark - KVO
- (void)registerForKVO {
    for (NSString *keyPath in [self observableKeypaths]) {
        [self addObserver:self forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:NULL];
    }
}

- (void)unregisterFromKVO {
    for (NSString *keyPath in [self observableKeypaths]) {
        [self removeObserver:self forKeyPath:keyPath];
    }
}

- (NSArray *)observableKeypaths {
    return [NSArray arrayWithObjects:@"mode", @"labelText", nil];
}

- (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"]) {
        [self updateIndicators];
    } else if ([keyPath isEqualToString:@"labelText"]) {
        _label.text = self.labelText;
    }
}


- (void)dealloc {
    [self unregisterFromKVO];
}

最终效果如下图:

Paste_Image.png

最后附上 demo的地址:https://github.com/superzcj/ZCJHUD

总结

MBProcessHUD还是比较简单的,都是一些常用的东西。
希望借助这篇文章,动手仿写一遍MBProcessHUD,能更深刻地理解和认识MBProcessHUD。

推荐阅读更多精彩内容

  • 4.13 半块鹅蛋 饼子 青菜 八宝粥 中午,油饼一小块,米饭6口 土豆丝➕西红柿鸡蛋汤 晚上一块饼干 一个橘子 红豆汤
    麦子旋阅读 33评论 0 0
  • 9.18成都文理军训帷幕拉开。8点左右收拾好寝室内务,出门吃早饭。踏上去操场集合的道路。 来到操场,开始军训前...
    灯塔的守望者阅读 92评论 2 6
  • 在我实习之前,我和大多数人一样,认为小孩子都特别难管、特别烦人、特别不听话。而在我当了小学教师之后,我才发现原来孩...
    Airing阅读 371评论 0 3