一、显示效果

圆球加载 - 图1

二、原理分析

1、拆解动画

从效果图来看,动画可拆解成两部分:放大动画位移动画
放大动画 比较简单,这里主要来分析一下位移动画

(1)、先去掉缩放效果:

圆球加载 - 图2

(2)、去掉其中的一个圆球

圆球加载 - 图3

现在基本可以看出主要原理就是让其中一个圆球绕另一个球做圆弧运动,只要确定一个圆球的运动轨迹,另一个圆球和它左相对运动即可。下面咱们重点说一下这个圆弧运动的原理。

2、圆弧运动

为了方便观察我们先放慢一下这个动画,然后添加辅助线:
圆球加载 - 图4

从图中可以看出,蓝色球主要经过了三段轨迹

第一段:从左边缘逆时针运动180°到灰色球的右侧
第二段:从灰色球右侧贴着灰色球逆时针运动180°到其左侧
第三段:从灰色球左侧返回起始位置

既然分析出了运动轨迹,下面实现起来就方便了

第一段:蓝色球以A为起点,沿圆心O逆时针运动到B点
圆球加载 - 图5

第二段:蓝色球以B为起点绕圆心P运动到C点
圆球加载 - 图6

第三段:从C点返回原点
圆球加载 - 图7

三、实现代码

1、第一段运动:
确定起始点、圆心、半径,让蓝色小球绕大圆

  1. //动画容器的宽度
  2. CGFloat width = _ballContainer.bounds.size.width;
  3. //小圆半径
  4. CGFloat r = (_ball1.bounds.size.width)*ballScale/2.0f;
  5. //大圆半径
  6. CGFloat R = (width/2 + r)/2.0;
  7. UIBezierPath *path1 = [UIBezierPath bezierPath];
  8. //设置起始位置
  9. [path1 moveToPoint:_ball1.center];
  10. //画大圆(第一段的运动轨迹)
  11. [path1 addArcWithCenter:CGPointMake(R + r, width/2) radius:R startAngle:M_PI endAngle:M_PI*2 clockwise:NO];

2、第二段运动

以灰色小球中心为圆心,以其直径为半径绕小圆,并拼接两段曲线

  1. //画小圆
  2. UIBezierPath *path1_1 = [UIBezierPath bezierPath];
  3. //圆心为灰色小球的中心 半径为灰色小球的半径
  4. [path1_1 addArcWithCenter:CGPointMake(width/2, width/2) radius:r*2 startAngle:M_PI*2 endAngle:M_PI clockwise:NO];
  5. [path1 appendPath:path1_1];

3、第三段运动

  1. //回到原处
  2. [path1 addLineToPoint:_ball1.center];

4、位移动画

利用关键帧动画实现小球沿设置好的贝塞尔曲线移动

  1. //执行动画
  2. CAKeyframeAnimation *animation1 = [CAKeyframeAnimation animationWithKeyPath:@"position"];
  3. animation1.path = path1.CGPath;
  4. animation1.removedOnCompletion = YES;
  5. animation1.duration = [self animationDuration];
  6. animation1.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
  7. [_ball1.layer addAnimation:animation1 forKey:@"animation1"];

5、缩放动画

在每次位移动画开始时执行缩放动画

  1. -(void)animationDidStart:(CAAnimation *)anim{
  2. CGFloat delay = 0.3f;
  3. CGFloat duration = [self animationDuration]/2 - delay;
  4. [UIView animateWithDuration:duration delay:delay options:UIViewAnimationOptionCurveEaseOut| UIViewAnimationOptionBeginFromCurrentState animations:^{
  5. _ball1.transform = CGAffineTransformMakeScale(ballScale, ballScale);
  6. _ball2.transform = CGAffineTransformMakeScale(ballScale, ballScale);
  7. _ball3.transform = CGAffineTransformMakeScale(ballScale, ballScale);
  8. } completion:^(BOOL finished) {
  9. [UIView animateWithDuration:duration delay:delay options:UIViewAnimationOptionCurveEaseInOut| UIViewAnimationOptionBeginFromCurrentState animations:^{
  10. _ball1.transform = CGAffineTransformIdentity;
  11. _ball2.transform = CGAffineTransformIdentity;
  12. _ball3.transform = CGAffineTransformIdentity;
  13. } completion:nil];
  14. }];
  15. }

6、动画循环

在每次动画结束时从新执行动画

  1. -(void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag{
  2. if (_stopAnimationByUser) {return;}
  3. [self startPathAnimate];
  4. }

Github地址