一、动画过程分析

1、拆分动画

正常动画效果如下:
优酷播放按钮动画原理解析 - 图1

操作 现象 结论
放慢动画 优酷播放按钮动画原理解析 - 图2 可以看出动画是由外侧的蓝色部分和中间的红色三角组成。
去掉旋转 优酷播放按钮动画原理解析 - 图3 可以看出核心的东西就是竖和弧线的缩进、三角形的透明度变化。
只保一侧留竖线和圆弧 优酷播放按钮动画原理解析 - 图4 可以看出竖线的动画时长大概是圆弧动动画时长的一半;弧线的运动角度是π/2,两个弧线正好组成一个完整的圆环。
只保留三角动画 优酷播放按钮动画原理解析 - 图5 三角动画比较简单,就是两条半透明直线组成的三角,交叉处颜色会变深。然后添加改变其透明度的动画。

2、总结

动画是由四部分组成:
1、直线的缩放
2、弧线的缩放
3、三角的透明度变化
4、整体的旋转。
三个部分中执行时间最长的是弧线的缩放,找到这个很重要,这样就可以确定其他动画的开始时间和持续时间了,这个会在下面解释。

二、动画开发

1、竖线动画

在开发这类动画的时候,一般是先在外面个框框和辅助线,帮助我们更好的完成开发。
这里设整个“容器”的边长是a,坐标系的原点为容器的左上角。
设从暂停—->播放的状态为正向,从播放—->暂停为逆向。
效果入下:
优酷播放按钮动画原理解析 - 图6

确定好坐标系后,我们来添加一条竖线。并且给竖线添加一个缩放的动画。
分析:这个动画的整体只不过是从暂停状态到播放状态的转换过程以及逆向过程。所以不能单单的通过bounds和position属性来绘制这个layer,需要用到的是CAShapeLayer+UIBezierPath来创建这个layer,并通过改变layer的strokeEnd属性进行对竖线的缩放操作。
设左侧直线的起点为:(a_0.2,a_0.9),终点为:(a_0.2,a_0.1)
(之所以这样设定是为了更贴近原版优酷按钮的效果,以及计算上的方便。)

  1. - (void)addLeftLineLayer {
  2. CGFloat a = self.layer.bounds.size.width;
  3. //创建竖线路径
  4. UIBezierPath *path = [UIBezierPath bezierPath];
  5. [path moveToPoint:CGPointMake(a*0.2,a*0.9)];
  6. [path addLineToPoint:CGPointMake(a*0.2,a*0.1)];
  7. //创建竖线显示层
  8. _leftLineLayer = [CAShapeLayer layer];
  9. _leftLineLayer.path = path.CGPath;
  10. _leftLineLayer.fillColor = [UIColor clearColor].CGColor;
  11. _leftLineLayer.strokeColor = BLueColor.CGColor;
  12. _leftLineLayer.lineWidth = [self lineWidth];
  13. //终点类型为圆形
  14. _leftLineLayer.lineCap = kCALineCapRound;
  15. //连接点为圆形
  16. _leftLineLayer.lineJoin = kCALineJoinRound;
  17. [self.layer addSublayer:_leftLineLayer];
  18. }

这样就添加了一条竖线,且竖线的起点在下,终点在上,是因为从暂停向播放转换时是向下缩放,逆向时是向上缩放。
为了代码的简洁性,写了一个单独的strokeEnd属性动画方法:

  1. /**
  2. 执行strokeEnd动画
  3. 参数为执行时间,起始值,被添加的layer
  4. */
  5. - (CABasicAnimation *)strokeEndAnimationFrom:(CGFloat)fromValue to:(CGFloat)toValue onLayer:(CALayer *)layer duration:(CGFloat)duration{
  6. CABasicAnimation *strokeEndAnimation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
  7. strokeEndAnimation.duration = duration;
  8. strokeEndAnimation.fromValue = @(fromValue);
  9. strokeEndAnimation.toValue = @(toValue);
  10. //这两个属性设定保证在动画执行之后不自动还原
  11. strokeEndAnimation.fillMode = kCAFillModeForwards;
  12. strokeEndAnimation.removedOnCompletion = NO;
  13. [layer addAnimation:strokeEndAnimation forKey:nil];
  14. return strokeEndAnimation;
  15. }

在正向动画的过程中左侧竖线strokeEnd属性从1到0:

  1. [self strokeEndAnimationFrom:1 to:0 onLayer:_leftLineLayer duration:animationDuration/2];

在逆向动画的过程中左侧竖线strokeEnd属性从0到1:

  1. [self strokeEndAnimationFrom:0 to:1 onLayer:_leftLineLayer duration:animationDuration/2];

效果如下:
优酷播放按钮动画原理解析 - 图7
同理添加右侧竖线,注意右侧竖线的终点在下,起点在上。并添加动画后效果如下:
优酷播放按钮动画原理解析 - 图8

2、圆弧动画

再确定两条直线后,弧线就比较容易添加了,在正向动画过程中:
左侧弧线是从左侧竖线的起点逆时针到右侧竖线的起点
右侧弧线是从右侧竖线的起点顺时针到左侧竖线的起点
如下图所示,添加左侧弧线效果
优酷播放按钮动画原理解析 - 图9
要绘制一个圆弧最好的方式是通过半径和起始角度+结束角度这种方式来确定的。
下面看一下示意图
优酷播放按钮动画原理解析 - 图10
如图所示,直角三角形的边长为0.3a、0.4a、0.5a,
所以根据反三角函数,角θ为acos(0.4a/0.5a)
通过这几点可以确定:
弧线的半径为0.5a
起始角度为:CGFloat startAngle = acos(4.0/5.0) + M_PI_2;
结束角度为:CGFloat endAngle = startAngle - M_PI;
添加弧线代码:

  1. - (void)addLeftCircle {
  2. CGFloat a = self.layer.bounds.size.width;
  3. UIBezierPath *path = [UIBezierPath bezierPath];
  4. [path moveToPoint:CGPointMake(a*0.2,a*0.9)];
  5. CGFloat startAngle = acos(4.0/5.0) + M_PI_2;
  6. CGFloat endAngle = startAngle - M_PI;
  7. [path addArcWithCenter:CGPointMake(a*0.5, a*0.5) radius:0.5*a startAngle:startAngle endAngle:endAngle clockwise:false];
  8. _leftCircle = [CAShapeLayer layer];
  9. _leftCircle.path = path.CGPath;
  10. _leftCircle.fillColor = [UIColor clearColor].CGColor;
  11. _leftCircle.strokeColor = LightBLueColor.CGColor;
  12. _leftCircle.lineWidth = [self lineWidth];
  13. _leftCircle.lineCap = kCALineCapRound;
  14. _leftCircle.lineJoin = kCALineJoinRound;
  15. _leftCircle.strokeEnd = 0;
  16. [self.layer addSublayer:_leftCircle];
  17. }

*这里注意弧线暂停时是隐藏的,所以strokeEnd为0;
正向过程执行strokeEnd动画:

  1. [self strokeEndAnimationFrom:0 to:1 onLayer:_leftLineLayer duration:animationDuration];

逆向过程:

  1. [self strokeEndAnimationFrom:1 to:0 onLayer:_leftCircle duration:animationDuration ];

*这里需要注意的是,弧线动画执行的时长是直线动画的二倍,即整个动画的执行时间就是弧线的动画时间animationDuration,所以直线的动画时间为:animationDuration/2。后面的旋转动画时长也是通过animationDuration确定的。
添加左右弧线后的动画效果:
优酷播放按钮动画原理解析 - 图11

3、三角动画

三角动画相对于之前的竖线和圆弧的动画比较简单,就是通过两条直线确定一个三角形
添加这个三角形前,还是先创建一个放置三角形的“容器”,然后通过这个容器确定三角形两条边的起点和终点,如图所示:
优酷播放按钮动画原理解析 - 图12
如图可以看出,三角两条边的起点为“容器”的底边中点,两条边的终点分别为“容器”的左上角及右上角。
代码如下:

  1. - (void)addCenterTriangleLayer {
  2. CGFloat a = self.layer.bounds.size.width;
  3. //初始化容器
  4. _triangleCotainer = [CALayer layer];
  5. _triangleCotainer.bounds = CGRectMake(0, 0, 0.4*a, 0.35*a);
  6. _triangleCotainer.position = CGPointMake(a*0.5, a*0.55);
  7. _triangleCotainer.opacity = 0;
  8. _triangleCotainer.borderWidth = 1;
  9. [self.layer addSublayer:_triangleCotainer];
  10. //容器宽高
  11. CGFloat b = _triangleCotainer.bounds.size.width;
  12. CGFloat c = _triangleCotainer.bounds.size.height;
  13. //第一条边
  14. UIBezierPath *path1 = [UIBezierPath bezierPath];
  15. [path1 moveToPoint:CGPointMake(0,0)];
  16. [path1 addLineToPoint:CGPointMake(b/2,c)];
  17. CAShapeLayer *layer1 = [CAShapeLayer layer];
  18. layer1.path = path1.CGPath;
  19. layer1.fillColor = [UIColor clearColor].CGColor;
  20. layer1.strokeColor = RedColor.CGColor;
  21. layer1.lineWidth = [self lineWidth];
  22. layer1.lineCap = kCALineCapRound;
  23. layer1.lineJoin = kCALineJoinRound;
  24. layer1.strokeEnd = 1;
  25. [_triangleCotainer addSublayer:layer1];
  26. //第二条边
  27. UIBezierPath *path2 = [UIBezierPath bezierPath];
  28. [path2 moveToPoint:CGPointMake(b,0)];
  29. [path2 addLineToPoint:CGPointMake(b/2,c)];
  30. CAShapeLayer *layer2 = [CAShapeLayer layer];
  31. layer2.path = path2.CGPath;
  32. layer2.fillColor = [UIColor clearColor].CGColor;
  33. layer2.strokeColor = RedColor.CGColor;
  34. layer2.lineWidth = [self lineWidth];
  35. layer2.lineCap = kCALineCapRound;
  36. layer2.lineJoin = kCALineJoinRound;
  37. layer2.strokeEnd = 1;
  38. [_triangleCotainer addSublayer:layer2];
  39. }

添加透明度动画

  1. - (void)actionTriangleOpacityAnimationFrom:(CGFloat)from to:(CGFloat)to duration:(CGFloat)duration{
  2. CABasicAnimation *alphaAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"];
  3. alphaAnimation.duration = duration; // 持续时间
  4. alphaAnimation.fromValue = @(from);
  5. alphaAnimation.toValue = @(to);
  6. alphaAnimation.fillMode = kCAFillModeForwards;
  7. alphaAnimation.removedOnCompletion = NO;
  8. [alphaAnimation setValue:@"alphaAnimation" forKey:@"animationName"];
  9. [_triangleCotainer addAnimation:alphaAnimation forKey:nil];
  10. }

效果如下:
优酷播放按钮动画原理解析 - 图13

4、旋转动画

  1. - (void)actionRotateAnimationClockwise:(BOOL)clockwise {
  2. //逆时针旋转
  3. CGFloat startAngle = 0.0f;
  4. CGFloat endAngle = -M_PI_2;
  5. CGFloat duration = 0.75 * animationDuration;
  6. //顺时针旋转
  7. if (clockwise) {
  8. startAngle = -M_PI_2;
  9. endAngle = 0.0;
  10. duration = animationDuration;
  11. }
  12. CABasicAnimation *roateAnimation = [CABasicAnimation animationWithKeyPath:@"transform.rotation"];
  13. roateAnimation.duration = duration; // 持续时间
  14. roateAnimation.fromValue = [NSNumber numberWithFloat:startAngle];
  15. roateAnimation.toValue = [NSNumber numberWithFloat:endAngle];
  16. roateAnimation.fillMode = kCAFillModeForwards;
  17. roateAnimation.removedOnCompletion = NO;
  18. [roateAnimation setValue:@"roateAnimation" forKey:@"animationName"];
  19. [self.layer addAnimation:roateAnimation forKey:nil];
  20. }

最终效果如下:
优酷播放按钮动画原理解析 - 图14

三、小结

所有的看起来复杂的动画只要拆分成各个模块都是比较简单的,只要把各个模块做好在拼凑到一起就可以了。

四、代码

Github:https://github.com/mengxianliang/XLPlayButton

五、延伸

爱奇艺播放按钮动画解析