GitHub 中文社区

SVProgressHUD 源码解析

2017-08-08  本文已影响58人  its程

最近看了SVProgressHUD的源码,文件结构如下(更详细的源代码分析在github

SVProgress.png

作者对细节处理得很用心,主要体现在一下几个方面

1. vibrancy 的抖动效果

首先来看效果

UIVibrancyEffect.gif

调整 SVProgress 的显示时间,和放大倍数,在模拟器中显示还是很明显的看到HUD 边缘部分的 vibrancy(抖动)效果。可惜在真机上的效果并不明显

2. 为HUD增加了视觉差

利用UIInterpolatingMotionEffect 增强视觉差效果 ,这个视觉差效果只能在真机上才能看出,大概就是 SVProgress 出现后, 你左右移动手机,HUD 的位置会发生一定的偏移,有种视觉差。

3. 为HUD增加了accessibility 为障碍人士设置了提醒

SVProgress 中给 HUD 添加了UIAccessibility的一些功能,在开启了 VoiceOver
的情况下,语音会播报 SVProgress 的相关状态,感兴趣的话可以自己尝试一下。

核心实现代码

1. SVIndefiniteAnimatedView,无限转圈动画的实现原理

SVIndefiniteAnimatedView 是实现无限转圈圈的视图,他是用两个 layer 层 使用 mask 和旋转造成的一种假象,可以说这个动画过程真是巧妙。

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 实际上是两个360°,下面解释为什么要画两圈。
        UIBezierPath* smoothedPath = [UIBezierPath bezierPathWithArcCenter:arcCenter radius:self.radius startAngle:(CGFloat) (M_PI*3/2) endAngle:(CGFloat) (M_PI/2+M_PI*5) clockwise:YES];
        
        //创建图层, 写到这里我们应该得到的如下图所示(特意放大了 HUD 的尺寸)
        _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 的展示应该像下图一样

无动画的view.png

紧接着就是给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 才会去渲染。

比如

layer.mask = maskLayer

那么 maskLayer之外的 layer 的部分默认是 clear 透明的,所以都不会被渲染。
maskLayer 和 layer 重叠部分的非透明部分才会被渲染。
例如: maskLayer 的背景颜色是 clear, 那么整个 layer 都不会被渲染。maskLayer 的 contents 设置成一张图片,但是这张图片有部分是透明的,那么 maskLayer 的非透明部分和 layer 的重叠部分才会被渲染, 例如 SVProgress 的遮罩图片(图片本身就是渐进透明的)

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

使用图片做一个 mask.png

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

mask 图片旋转.gif

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

线帽很丑.png

为了优化线条的 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"];
        
    }
    return _indefiniteAnimatedLayer;
}



线帽是弧形后会超出本来的界限.png

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

动画产生过程.gif

如果很难理解这个动画的过程,可以单独看下分别对 strokeEnd 和 strokeStart做动画的动画效果,会加深理解。

2. SVProgressAnimatedView显示进度的视图实现原理

相比较SVIndefiniteAnimatedView的实现来说,这个环形的视图实现起来要相对容易些。就是两个环形叠加在一起,这样就可以显示进度,如下图


progress.png

最后有一些疑惑,向大家请教,烦请告知, 不胜感激!

  1. SVProgress 使用 activityCount 来记录 HUD 的展示个数,同时提供了pop方法平衡 activityCount。我暂时没想到这些方法使用的一些场景,哪些场景下会使用到呢?
// decrease activity count, if activity count == 0 the HUD is  dismissed
+ (void)popActivity; 

例如这里,在-showProgress: status: 这个主要方法内,为什么要在progress 为 0 的时候 activityCount++ ?

 // Update the activity count
  if(progress == 0){
    strongSelf.activityCount++;
  }
  1. 在主要的 show 和 dismiss 方法的时候是用 block 执行动画和 dismiss 动画,但是在定义 block 的时候作者在前面加了__block修饰符,这个修饰符的作用是什么呢?
  __block void (^animationsBlock)(void) = ^{}
  __block void (^completionBlock)(void) = ^{}

上一篇下一篇

猜你喜欢

热点阅读