SVProgressHUD 源码解析

最近看了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) = ^{}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容