美文网首页
SVProgressHUD探究

SVProgressHUD探究

作者: konglei | 来源:发表于2019-03-18 16:14 被阅读0次

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 源码解析

相关文章

网友评论

      本文标题:SVProgressHUD探究

      本文链接:https://www.haomeiwen.com/subject/mcrsmqtx.html