[TOC]
簡介
SVProgressHUB是iOS上的的一款loading輕量級美觀的加載框。
首先來看下結(jié)構(gòu)目錄

SVIndefiniteAnimatedView 是無限旋轉(zhuǎn)的菊花視圖
SVProgressAnimatedView 是進(jìn)度條視圖
SVRadialGradientLayer 是漸變模式下漸變的背景Layer
使用
顯示
SVProgressHUD提供了顯示加載圖便利方法,而且?guī)缀跞穷惙椒ǎ陂_發(fā)中能方便的使用。如
- showWithStatus 顯示加載,并顯示文本
- showProgress 顯示加載,并展示當(dāng)前進(jìn)度條
- showInfoWithStatus 顯示使用Info狀態(tài)的圖片的加載框來替代菊花或者進(jìn)度條
- showSuccessWithStatus 顯示success狀態(tài)的圖片(一個勾)的加載框來替代菊花或者進(jìn)度條
- showErrorWithStatus 顯示error狀態(tài)的圖片(一個x)的加載框來替代菊花或者進(jìn)度條
除此之外你也可以設(shè)置自己的圖片,使用+ (void)showImage:(UIImage*)image status:(NSString*)status;
消失
SVProgressHUD提供了幾種方式來顯示一個Loding框
- dismiss 直接關(guān)閉一個加載Hub
- dismissWithDelay 延遲關(guān)閉一個Hub
- dismissWithCompletion 關(guān)閉Hub帶一個完成的Block
- dismissWithDelay:completion 延遲關(guān)閉Hub并帶一個完成的Block
可以看出SVProgressHUD提供了簡單明了的API,使用便利的類方法讓我們快速在代碼中使用
深入源碼
顯示過程
SVProgressHUD 比較簡單易懂
在眾多的類方法下面,也實(shí)現(xiàn)相應(yīng)的實(shí)例方法
//簡單版本
- (void)setStatus:(NSString*)status;
- (void)setFadeOutTimer:(NSTimer*)timer;
- (void)registerNotifications;
- (NSDictionary*)notificationUserInfo;
- (void)positionHUD:(NSNotification*)notification;
- (void)moveToPoint:(CGPoint)newCenter rotateAngle:(CGFloat)angle;
- (void)overlayViewDidReceiveTouchEvent:(id)sender forEvent:(UIEvent*)event;
- (void)showProgress:(float)progress status:(NSString*)status;
- (void)showImage:(UIImage*)image status:(NSString*)status duration:(NSTimeInterval)duration;
- (void)showStatus:(NSString*)status;
- (void)dismiss;
- (void)dismissWithDelay:(NSTimeInterval)delay completion:(SVProgressHUDDismissCompletion)completion;
@end
如何將Hub展示到屏幕上,首先我們從showWithStatus方法往里看。
內(nèi)部實(shí)現(xiàn)
+ (void)showWithStatus:(NSString*)status {
[self sharedView];
[self showProgress:SVProgressHUDUndefinedProgress status:status];
}
在內(nèi)部實(shí)現(xiàn)了一個單例,這也不難理解為什么可以在內(nèi)方法中快速的顯示一個Hub
+ (SVProgressHUD*)sharedView {
static dispatch_once_t once;
static SVProgressHUD *sharedView;
#if !defined(SV_APP_EXTENSIONS)
dispatch_once(&once, ^{ sharedView = [[self alloc] initWithFrame:[[[UIApplication sharedApplication] delegate] window].bounds]; });
#else
dispatch_once(&once, ^{ sharedView = [[self alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; });
#endif
return sharedView;
}
之后使用這個單例視圖來完成Hub的顯示,那么showProgress這個方法又做什么呢?首先我們看下簡化版代碼
- (void)showProgress:(float)progress status:(NSString*)status {
__weak SVProgressHUD *weakSelf = self;
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
__strong SVProgressHUD *strongSelf = weakSelf;
if(strongSelf){
//更新視圖層級,將SV放到當(dāng)前window最前面
[strongSelf updateViewHierachy];
//判斷如果是進(jìn)度條,使用進(jìn)度條View
if(progress >= 0) {
// Add ring to HUD and set progress
[strongSelf.hudView addSubview:strongSelf.ringView];
[strongSelf.hudView addSubview:strongSelf.backgroundRingView];
strongSelf.ringView.strokeEnd = progress;
} else {
// 使用無限旋轉(zhuǎn)的菊花模式
[strongSelf.hudView addSubview:strongSelf.indefiniteAnimatedView];
}
// Show
[strongSelf showStatus:status];
}
}];
}
可以看出,SV為了將顯示操作放到主線程使用了[NSOperationQueue mainQueue]來確保在主線程操作UI,
在updateViewHierachy方法中,SV會尋找提供Hub顯示的Window,并且將自己添加到上面。使用手法為:
NSEnumerator *frontToBackWindows = [UIApplication.sharedApplication.windows reverseObjectEnumerator];
for (UIWindow *window in frontToBackWindows) {
BOOL windowOnMainScreen = window.screen == UIScreen.mainScreen;
BOOL windowIsVisible = !window.hidden && window.alpha > 0;
BOOL windowLevelNormal = window.windowLevel == UIWindowLevelNormal;
if(windowOnMainScreen && windowIsVisible && windowLevelNormal) {
[window addSubview:self.overlayView];
break;
}
}
找出當(dāng)前的windowLevelNormal,并且是顯示的UI,將自己顯示在其之上,但是也有點(diǎn)缺點(diǎn),就是如果需要在自定義的window上面顯示Hub將無能為力。
找到window之后,生成菊花或者進(jìn)度條View,然后設(shè)置label,計算出大小,然后完成顯示
動畫的繪制
SVIndefiniteAnimatedView是無限旋轉(zhuǎn)的View。其中使用CASharpLayer和LayerMask來完成一個旋轉(zhuǎn)動畫,使用一張漸變的圖片,設(shè)置其mask屬性,然后加上旋轉(zhuǎn)動畫,來形成一個漂亮的加載動畫
動畫繪制代碼
UIBezierPath* smoothedPath = [UIBezierPath bezierPathWithArcCenter:arcCenter radius:self.radius startAngle:(CGFloat) (M_PI*3/2) endAngle:(CGFloat) (M_PI/2+M_PI*5) clockwise:YES];
_indefiniteAnimatedLayer = [CAShapeLayer layer];
_indefiniteAnimatedLayer.contentsScale = [[UIScreen mainScreen] scale];
_indefiniteAnimatedLayer.frame = CGRectMake(0.0f, 0.0f, arcCenter.x*2, arcCenter.y*2);
_indefiniteAnimatedLayer.fillColor = [UIColor clearColor].CGColor;
_indefiniteAnimatedLayer.strokeColor = self.strokeColor.CGColor;
_indefiniteAnimatedLayer.lineWidth = self.strokeThickness;
_indefiniteAnimatedLayer.lineCap = kCALineCapRound;
_indefiniteAnimatedLayer.lineJoin = kCALineJoinBevel;
_indefiniteAnimatedLayer.path = smoothedPath.CGPath;
CALayer *maskLayer = [CALayer layer];
NSBundle *bundle = [NSBundle bundleForClass:[SVProgressHUD class]];
NSURL *url = [bundle URLForResource:@"SVProgressHUD" withExtension:@"bundle"];
NSBundle *imageBundle = [NSBundle bundleWithURL:url];
NSString *path = [imageBundle pathForResource:@"angle-mask" ofType:@"png"];
maskLayer.contents = (__bridge id)[[UIImage imageWithContentsOfFile:path] CGImage];
maskLayer.frame = _indefiniteAnimatedLayer.bounds;
_indefiniteAnimatedLayer.mask = maskLayer;
NSTimeInterval animationDuration = 1;
CAMediaTimingFunction *linearCurve = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform.rotation"];
animation.fromValue = (id) 0;
animation.toValue = @(M_PI*2);
animation.duration = animationDuration;
animation.timingFunction = linearCurve;
animation.removedOnCompletion = NO;
animation.repeatCount = INFINITY;
animation.fillMode = kCAFillModeForwards;
animation.autoreverses = NO;
[_indefiniteAnimatedLayer.mask addAnimation:animation forKey:@"rotate"];
CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
animationGroup.duration = animationDuration;
animationGroup.repeatCount = INFINITY;
animationGroup.removedOnCompletion = NO;
animationGroup.timingFunction = linearCurve;
CABasicAnimation *strokeStartAnimation = [CABasicAnimation animationWithKeyPath:@"strokeStart"];
strokeStartAnimation.fromValue = @0.015;
strokeStartAnimation.toValue = @0.515;
CABasicAnimation *strokeEndAnimation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
strokeEndAnimation.fromValue = @0.485;
strokeEndAnimation.toValue = @0.985;
animationGroup.animations = @[strokeStartAnimation, strokeEndAnimation];
[_indefiniteAnimatedLayer addAnimation:animationGroup forKey:@"progress"];
圓環(huán)動畫比較簡單,原理是使用CAShareLayer,并且設(shè)置的endPath來完成一個由progress驅(qū)動的進(jìn)度條動畫
CGPoint arcCenter = CGPointMake(self.radius+self.strokeThickness/2+5, self.radius+self.strokeThickness/2+5);
UIBezierPath* smoothedPath = [UIBezierPath bezierPathWithArcCenter:arcCenter radius:self.radius startAngle:(CGFloat)-M_PI_2 endAngle:(CGFloat) (M_PI + M_PI_2) clockwise:YES];
_ringAnimatedLayer = [CAShapeLayer layer];
_ringAnimatedLayer.contentsScale = [[UIScreen mainScreen] scale];
_ringAnimatedLayer.frame = CGRectMake(0.0f, 0.0f, arcCenter.x*2, arcCenter.y*2);
_ringAnimatedLayer.fillColor = [UIColor clearColor].CGColor;
_ringAnimatedLayer.strokeColor = self.strokeColor.CGColor;
_ringAnimatedLayer.lineWidth = self.strokeThickness;
_ringAnimatedLayer.lineCap = kCALineCapRound;
_ringAnimatedLayer.lineJoin = kCALineJoinBevel;
_ringAnimatedLayer.path = smoothedPath.CGPath;
消失過程
消失調(diào)用的是dismissWithDelay:completion方法
消失過程和顯示過程相反,大致是
- 在mainQueue中添加一個Block的operation(在主線程更改UI)
- 將自己從SuperView中刪除,
- 如果指定了延遲,那么設(shè)置一個timer
- 在消失中如果指定動畫,那么加一個fade的動畫來進(jìn)行消失
小結(jié)
- SV將View添加到Window上
- SV使用NSOperationQueue 來確保自己在主線程修改UI
- SV的動畫使用layer來繪制
優(yōu)點(diǎn):API簡單明了,調(diào)用方便
缺點(diǎn):默認(rèn)在APP的UIWindow,不好定制