一直以来都没有用心阅读三方源码,于是现在告诉自己每个月要认真看一下别人的源码,要行动起来。学习别人优秀的代码和编程思想其实很有必要的,本来想着仔细去看看SDWebImage和AFNetworking这两个非常优秀的第三发源码,我们也经常用到,但是发现里面内容和文件很多,需要认真耐心的花费一定时间去仔细琢磨研究,
最后看到我们项目里用到的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方法调用流程
我画了一个大概的流程图:
如果需要在一个视图之上显示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
来自动布局,主要涉及到的是updateConstraints
和updatePaddingConstraints
方法.
主要功能:
- 移除原有约束
- 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
,环形的进度条作为例子简单说一下。
- (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];
}
}
}
最后
由于自己的之前很少仔细阅读三方源码,第一次分析的比较简陋,加上最近要离职了,精力有限暂时先分析到这了,不足之处后面会通过多阅读和分析三方源码提高自己。