SVProgressHUD
简介
SVProgressHUD 是一款干净且易于使用的HUD,旨在显示iOS和tvOS上正在进行的任务的进度。
常用于指示一个任务正在持续进行中, 其采用单例模式创建对象, 所以我们在使用过程中只需通过 [SVProgressHUD method]
的方式调用对应方法即可。
[SVProgressHUD show];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// time-consuming task
dispatch_async(dispatch_get_main_queue(), ^{
[SVProgressHUD dismiss];
});
});
#pragma mark - Show Methods
// 显示无限旋转组件
+ (void)show;
+ (void)showWithStatus:(nullable NSString*)status;
// 显示进度组件
+ (void)showProgress:(float)progress;
+ (void)showProgress:(float)progress status:(nullable NSString*)status;
// 显示警告、成功、失败组件
+ (void)showInfoWithStatus:(nullable NSString*)status;
+ (void)showSuccessWithStatus:(nullable NSString*)status;
+ (void)showErrorWithStatus:(nullable NSString*)status;
// shows a image + status, use white PNGs with the imageViewSize (default is 28x28 pt)
+ (void)showImage:(nonnull UIImage*)image status:(nullable NSString*)status;
+ (void)dismiss;
+ (void)dismissWithDelay:(NSTimeInterval)delay completion:(nullable SVProgressHUDDismissCompletion)completion;
// 自定义里面的一些属性,比如字体大小,提示图片等
+ (void)setDefaultStyle:(SVProgressHUDStyle)style; // default is SVProgressHUDStyleLight
+ (void)setDefaultMaskType:(SVProgressHUDMaskType)maskType; // default is SVProgressHUDMaskTypeNone
+ (void)setDefaultAnimationType:(SVProgressHUDAnimationType)type; // default is SVProgressHUDAnimationTypeFlat
+ (void)setContainerView:(nullable UIView*)containerView; // default is window level
+ (void)setMinimumSize:(CGSize)minimumSize; // default is CGSizeZero, can be used to avoid resizing for a larger message
+ (void)setRingThickness:(CGFloat)ringThickness; // default is 2 pt
+ (void)setRingRadius:(CGFloat)radius; // default is 18 pt
+ (void)setRingNoTextRadius:(CGFloat)radius; // default is 24 pt
+ (void)setCornerRadius:(CGFloat)cornerRadius; // default is 14 pt
+ (void)setBorderColor:(nonnull UIColor*)color; // default is nil
+ (void)setBorderWidth:(CGFloat)width; // default is 0
+ (void)setFont:(nonnull UIFont*)font; // default is [UIFont preferredFontForTextStyle:UIFontTextStyleSubheadline]
+ (void)setForegroundColor:(nonnull UIColor*)color; // default is [UIColor blackColor], only used for SVProgressHUDStyleCustom
+ (void)setBackgroundColor:(nonnull UIColor*)color; // default is [UIColor whiteColor], only used for SVProgressHUDStyleCustom
+ (void)setBackgroundLayerColor:(nonnull UIColor*)color; // default is [UIColor colorWithWhite:0 alpha:0.5], only used for SVProgressHUDMaskTypeCustom
+ (void)setImageViewSize:(CGSize)size; // default is 28x28 pt
+ (void)setShouldTintImages:(BOOL)shouldTintImages; // default is YES
+ (void)setInfoImage:(nonnull UIImage*)image; // default is the bundled info image provided by Freepik
+ (void)setSuccessImage:(nonnull UIImage*)image; // default is the bundled success image provided by Freepik
+ (void)setErrorImage:(nonnull UIImage*)image; // default is the bundled error image provided by Freepik
+ (void)setViewForExtension:(nonnull UIView*)view; // default is nil, only used if #define SV_APP_EXTENSIONS is set
+ (void)setGraceTimeInterval:(NSTimeInterval)interval; // default is 0 seconds
+ (void)setMinimumDismissTimeInterval:(NSTimeInterval)interval; // default is 5.0 seconds
+ (void)setMaximumDismissTimeInterval:(NSTimeInterval)interval; // default is infinite
+ (void)setFadeInAnimationDuration:(NSTimeInterval)duration; // default is 0.15 seconds
+ (void)setFadeOutAnimationDuration:(NSTimeInterval)duration; // default is 0.15 seconds
+ (void)setMaxSupportedWindowLevel:(UIWindowLevel)windowLevel; // default is UIWindowLevelNormal
+ (void)setHapticsEnabled:(BOOL)hapticsEnabled; // default is NO
typedef NS_ENUM(NSInteger, SVProgressHUDStyle) {
SVProgressHUDStyleLight NS_SWIFT_NAME(light), // default style, white HUD with black text, HUD background will be blurred
SVProgressHUDStyleDark NS_SWIFT_NAME(dark), // black HUD and white text, HUD background will be blurred
SVProgressHUDStyleCustom NS_SWIFT_NAME(custom) // uses the fore- and background color properties
};
typedef NS_ENUM(NSUInteger, SVProgressHUDMaskType) {
SVProgressHUDMaskTypeNone NS_SWIFT_NAME(none) = 1, // default mask type, allow user interactions while HUD is displayed
SVProgressHUDMaskTypeClear NS_SWIFT_NAME(clear), // don't allow user interactions with background objects
SVProgressHUDMaskTypeBlack NS_SWIFT_NAME(black), // don't allow user interactions with background objects and dim the UI in the back of the HUD (as seen in iOS 7 and above)
SVProgressHUDMaskTypeGradient NS_SWIFT_NAME(gradient), // don't allow user interactions with background objects and dim the UI with a a-la UIAlertView background gradient (as seen in iOS 6)
SVProgressHUDMaskTypeCustom NS_SWIFT_NAME(custom) // don't allow user interactions with background objects and dim the UI in the back of the HUD with a custom color
};
// 通知 以及extern的使用
.h
extern NSString * _Nonnull const SVProgressHUDDidReceiveTouchEventNotification;
extern NSString * _Nonnull const SVProgressHUDDidTouchDownInsideNotification;
extern NSString * _Nonnull const SVProgressHUDWillDisappearNotification;
extern NSString * _Nonnull const SVProgressHUDDidDisappearNotification;
extern NSString * _Nonnull const SVProgressHUDWillAppearNotification;
extern NSString * _Nonnull const SVProgressHUDDidAppearNotification;
.m
NSString * const SVProgressHUDDidReceiveTouchEventNotification = @"SVProgressHUDDidReceiveTouchEventNotification";
NSString * const SVProgressHUDDidTouchDownInsideNotification = @"SVProgressHUDDidTouchDownInsideNotification";
NSString * const SVProgressHUDWillDisappearNotification = @"SVProgressHUDWillDisappearNotification";
NSString * const SVProgressHUDDidDisappearNotification = @"SVProgressHUDDidDisappearNotification";
NSString * const SVProgressHUDWillAppearNotification = @"SVProgressHUDWillAppearNotification";
NSString * const SVProgressHUDDidAppearNotification = @"SVProgressHUDDidAppearNotification";
值得学习的地方
// 有趣的东西 更改maskType、使用、还原
SVProgressHUDMaskType existingMaskType = [self sharedView].defaultMaskType;
[self setDefaultMaskType:maskType];
[self showWithStatus:status];
[self setDefaultMaskType:existingMaskType];
- (UIWindow *)frontWindow {
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 windowLevelSupported = (window.windowLevel >= UIWindowLevelNormal && window.windowLevel <= self.maxSupportedWindowLevel);
BOOL windowKeyWindow = window.isKeyWindow;
if(windowOnMainScreen && windowIsVisible && windowLevelSupported && windowKeyWindow) {
return window;
}
}
return nil;
}
核心实现代码

1. SVIndefiniteAnimatedView,无限转圈动画的实现原理
SVIndefiniteAnimatedView 是实现无限转圈圈的视图,他是用两个 layer 层:CAShapeLayer
+ maskLayer
, 并且使用旋转动画
造成的一种假象,可以说这个动画过程真是巧妙。
indefiniteAnimatedLayer 这个方法一步一步的解释
- (CAShapeLayer*)indefiniteAnimatedLayer
{
if(!_indefiniteAnimatedLayer)
{
CGPoint arcCenter = CGPointMake(self.radius+self.strokeThickness/2+5, self.radius+self.strokeThickness/2+5);
// 确定画圆这个动画的起始位置和结束位置,从 M_PI*3/2 到 M_PI/2+M_PI*5 实际上是4π 两圈,下面解释为什么要画两圈。
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;
/*
其他代码
*/
}
return _indefiniteAnimatedLayer;
}
这些代码完成后layer 的展示应该像下图一样

紧接着就是给layer 上添加一层 mask
- (CAShapeLayer*)indefiniteAnimatedLayer
{
if(!_indefiniteAnimatedLayer)
{
/*
接着上面的代码
*/
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;
}
return _indefiniteAnimatedLayer;
}
关于 mask 是这样的:遮罩的不透明部分
和被遮罩的layer内容
的重叠
部分的 layer 才会去渲染。
那么 maskLayer之外的 layer 的部分默认是 clear 透明的,所以都不会被渲染。
maskLayer 和 layer 重叠部分的非透明部分才会被渲染。
例如: maskLayer 的背景颜色是 clear, 那么整个 layer 都不会被渲染。maskLayer 的 contents 设置成一张图片,但是这张图片有部分是透明的,那么 maskLayer 的非透明部分和 layer 的重叠部分才会被渲染, 例如 SVProgress 的遮罩图片(图片本身就是渐进透明的)
网上一个经典示例:

有许多很炫酷的动画效果都是通过这样实现的。比如以下几种


加了 mask 之后的效果是这样的:

这个时候只需要对 mask, 就是那张渐进色的 png 图片做旋转动画, 那么其实无限转圈的动画效果就出来了类似于下面这样

好像一切都很美好,其实在意细节的话应该已经注意到旋转的“黑线”的头是被切平的,当我们把 HUD 的尺寸再扩大一些的时候可以看出这种 UI 有点丑

为了优化线条的 UI,于是乎有了下面的代码,也就是最巧妙的地方
- (CAShapeLayer*)indefiniteAnimatedLayer
{
if(!_indefiniteAnimatedLayer)
{
/*
接着上面的代码
*/
//给 mask 添加一个旋转动画,那么线条就旋转起来了
NSTimeInterval animationDuration = 1.0;
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"];
// 还记得 _indefiniteAnimatedLayer 的 path 是两个360°吗?
// 因为 strokeStart 和 strokeEnd 的动画都是0.5的差距(取值范围0 ~ 1)
// 所以0.5的比例就是一圈的距离,那么这条线的长度就刚好是一个360°
// 这里就要对_indefiniteAnimatedLayer.stroke 做动画
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;
// strokeStart 从为什么从0.015开始呢?因如果line 很粗的情况下(用户可以自定义)
// _indefiniteAnimatedLayer.lineCap = kCALineCapRound; line 的头部是圆的,会超出它本来的界限[如下图]
CABasicAnimation *strokeEndAnimation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
//strokeEnd 从0.485开始,保证与strokeStart 有一段距离,这样才能看到 line 的圆角
//如果直接写成0.5 那么 line 就连在了一起看不出来 line 的头部在哪里
strokeEndAnimation.fromValue = @0.485;
strokeEndAnimation.toValue = @0.985;
animationGroup.animations = @[strokeStartAnimation, strokeEndAnimation];
[_indefiniteAnimatedLayer addAnimation:animationGroup forKey:@"progress"];
// / @0.485 是一个将要一圈的位置
// | \ @0.015 是一个一圈刚刚开始的位置
// | |
// | |
// | |
// | |
// | |
// | |
// | |
// | |
// |
// 这样strokeStart和strokeEnd之间就有一个@0.03的空隙,以防止因为设置圆头而导致超过原有的界限
}
return _indefiniteAnimatedLayer;
}

给 _indefiniteAnimatedLayer.mask 做旋转动画的同时,也给 _indefiniteAnimatedLayer.stroke 做旋转动画,而且动画要是同步的,这样就能展示 line 的风格了,如下图。

如果很难理解这个动画的过程,可以单独看下分别对 strokeEnd 和 strokeStart做动画的动画效果 strokeStart和strokeEnd,会加深理解。
2. SVProgressAnimatedView显示进度的视图实现原理
相比较SVIndefiniteAnimatedView的实现来说,这个环形的视图实现起来要相对容易些。就是两个环形叠加在一起,这样就可以显示进度,如下图

3. SVRadialGradientLayer
SVRadialGradientLayer继承自CALayer类, 用于实现一个放射渐变层(2016笔记——CGContextDrawRadialGradient)
- (void)drawInContext:(CGContextRef)context {
size_t locationsCount = 2;
CGFloat locations[2] = {0.0f, 1.0f};
CGFloat colors[8] = {0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.75f};
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGGradientRef gradient = CGGradientCreateWithColorComponents(colorSpace, colors, locations, locationsCount);
CGColorSpaceRelease(colorSpace);
float radius = MIN(self.bounds.size.width , self.bounds.size.height);
CGContextDrawRadialGradient (context, gradient, self.gradientCenter, 0, self.gradientCenter, radius, kCGGradientDrawsAfterEndLocation);
CGGradientRelease(gradient);
}

显示原理
SVProgressAnimatedView类中重写了如下方法, 当父视图存在时(即视图被add时), 将ringAnimatedLayer添加为self.layer的子layer; 当父视图不存在时(即视图被remove时), 将ringAnimatedLayer从self.layer中移除
- (void)willMoveToSuperview:(UIView*)newSuperview {
if (newSuperview) {
[self layoutAnimatedLayer];
} else {
[_ringAnimatedLayer removeFromSuperlayer];
_ringAnimatedLayer = nil;
}
}
注: 该方法在父视图将要发生改变(add/remove)时会被系统调用, 该方法默认实现没有进行任何操作, 子类可以覆盖该方法以执行一些额外的操作, 当视图被add时, newSuperview为父视图; 当视图被remove时, newSuperview为nil
大小原理
SVProgressAnimatedView类中重写了如下方法, 当调用sizeToFit方法时, 系统会自动调用如下方法, 并设置自身大小
- (CGSize)sizeThatFits:(CGSize)size {
return CGSizeMake((self.radius+self.strokeThickness/2+5)*2, (self.radius+self.strokeThickness/2+5)*2);
}
UIBezierPath介绍
SVProgressHUD原理解析
iOS Animation CATransaction事务 详解
SVProgressHUD 源码解析
网友评论