好久没写动画了…最近扒了下以前没有写的动画效果,想想从最老的开始写吧,之前看到的版本是用Swift写的,没仔细找有没有OC版的,所以干脆自己练习一下吧,我们先来看看效果:

(这里三角形是旋转动画,但是Gif录出来看上去是抖了两下…)

可以直接run下代码,看下效果:https://github.com/Yuzeyang/GCLoadingAnimation/tree/master/GCLoadingAnimationOne

下面我来分析下过程

这个动画的实现只用到了UIBezierPathCABasicAnimationCALayer

从Gif里面可以看到这个动画分为以下几个步骤:

1.从无到圆

2.圆x轴方向拉伸和y轴方向拉伸

3.“长出”三角形的三个角

4.三角形旋转

5.画两条边框

6.水面上涨动画

7.中间矩形放大至全屏

8.中间logo跟着出现

0x00 从无到圆

这个比较简单,只要设定起始的size为0和设定默认圆半径大小,用+ bezierPathWithOvalInRect:方法画圆+ (instancetype)bezierPathWithRoundedRect:(CGRect)rect cornerRadius:(CGFloat)cornerRadius

1
2
UIBezierPath *startPath = [self circleStartPath];
UIBezierPath *endPath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(GCLoadingLayerCenterX - GCCircleRadius, GCLoadingLayerCenterY - GCCircleRadius, GCCircleRadius*2, GCCircleRadius*2)];

将最后圆的path设为circleLayerpath

1
2
3
4
self.circleLayer = [CAShapeLayer layer];
self.circleLayer.path = endPath.CGPath;
self.circleLayer.fillColor = [UIColor orangeColor].CGColor;
[self addSublayer:self.circleLayer];

然后加上动画,因为我们修改的是path,所以我们animationkeyPathpath(后面也是),设定起始值为startPath.CGPath

1
2
3
4
5
6
7
8
9
CABasicAnimation *circleAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
circleAnimation.fromValue = (__bridge id _Nullable)(startPath.CGPath);
circleAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
circleAnimation.duration = 0.2f;
circleAnimation.fillMode = kCAFillModeForwards;
circleAnimation.delegate = self;
circleAnimation.removedOnCompletion = NO;
[circleAnimation setValue:@"circleAnimation" forKey:@"animationName"];
[self.circleLayer addAnimation:circleAnimation forKey:nil];

0x01 圆x轴方向拉伸和y轴方向拉伸

这里我们的keyPath不用transform.scale.x/y,因为缩放之后,圆心会改变,看上去有偏移,这样动画写起来更复杂,所以我们干脆直接用拉伸后的path来做动画

创建x轴、y轴拉伸后的path,然后加到animation里面

1
2
3
4
5
6
7
8
UIBezierPath *scaleXPath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(GCLoadingLayerCenterX - GCCircleRadius*1.1, GCLoadingLayerCenterY - GCCircleRadius, GCCircleRadius*2.2, GCCircleRadius*2)];
UIBezierPath *scaleYPath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(GCLoadingLayerCenterX - GCCircleRadius, GCLoadingLayerCenterY - GCCircleRadius*1.1, GCCircleRadius*2, GCCircleRadius*2.2)];

CABasicAnimation *circleScaleXOneAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
circleScaleXOneAnimation.fromValue = (__bridge id _Nullable)(self.circleLayer.path);
circleScaleXOneAnimation.toValue = (__bridge id _Nullable)(scaleXPath.CGPath);
circleScaleXOneAnimation.duration = 0.2f;
circleScaleXOneAnimation.beginTime = 0.0;

一共四个CABasicAnimation对象,然后我们将这些动画加到CAAnimationGroup

1
2
3
4
5
6
CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
animationGroup.animations = @[circleScaleXOneAnimation,circleScaleXTwoAnimation,circleScaleYOneAnimation,circleScaleYTwoAnimation];
animationGroup.duration = circleScaleYTwoAnimation.beginTime + circleScaleYTwoAnimation.duration;
animationGroup.delegate = self;
[animationGroup setValue:@"circleScaleAnimation" forKey:@"animationName"];
[self.circleLayer addAnimation:animationGroup forKey:nil];

0x02 “长出”三角形的三个角

实际上三角形在等到圆形出现或者圆形拉伸完之后就已经在那了,“长出角”的感觉实际上只是改变了绘制的三个点的位置,首先我们根据圆的半径画出三角形

1
2
3
4
5
6
7
8
9
10
UIBezierPath *originTrianglePath = [UIBezierPath bezierPath];
[originTrianglePath moveToPoint:[self triangleLeftPointWithScale:1.0]];
[originTrianglePath addLineToPoint:[self triangleRightPointWithScale:1.0]];
[originTrianglePath addLineToPoint:[self triangleTopPointWithScale:1.0]];
[originTrianglePath closePath];

self.triangleLayer = [CAShapeLayer layer];
self.triangleLayer.path = originTrianglePath.CGPath;
self.triangleLayer.fillColor = [UIColor orangeColor].CGColor;
[self addSublayer:self.triangleLayer];

然后改变左边点的位置

1
2
3
4
5
UIBezierPath *blowUpLeftTrianglePath = [UIBezierPath bezierPath];
[blowUpLeftTrianglePath moveToPoint:[self triangleLeftPointWithScale:1.2]];
[blowUpLeftTrianglePath addLineToPoint:[self triangleRightPointWithScale:1.0]];
[blowUpLeftTrianglePath addLineToPoint:[self triangleTopPointWithScale:1.0]];
[blowUpLeftTrianglePath closePath];

也加上path的动画

1
2
3
4
5
CABasicAnimation *blowUpLeftAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
blowUpLeftAnimation.fromValue = (__bridge id _Nullable)(self.triangleLayer.path);
blowUpLeftAnimation.toValue = (__bridge id _Nullable)(blowUpLeftTrianglePath.CGPath);
blowUpLeftAnimation.duration = 0.2f;
blowUpLeftAnimation.beginTime = 0.0;

右边和上边的点同理,然后也一起加到CAAnimationGroup

0x03 三角形旋转

旋转就比较简单了,只要根据z轴旋转设定的角度即可

1
2
3
4
5
6
7
8
CABasicAnimation *rotationAniamtion = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
rotationAniamtion.toValue = @(M_PI*2);
rotationAniamtion.duration = 0.4;
rotationAniamtion.fillMode = kCAFillModeForwards;
rotationAniamtion.delegate = self;
rotationAniamtion.beginTime = CACurrentMediaTime();
[rotationAniamtion setValue:@"rotationAniamtion" forKey:@"animationName"];
[self.triangleLayer addAnimation:rotationAniamtion forKey:nil];

0x04 画两条边框

这两个边框绘制方法是一模一样的,只是中间有个时间间隔而已

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
- (CABasicAnimation *)drawRectWithLineColor:(CGColorRef)color animationValue:(NSString *)animationValue {
CGPoint startPoint = [self triangleLeftPointWithScale:1.2];
UIBezierPath *rectPath = [UIBezierPath bezierPath];
[rectPath moveToPoint:startPoint];
[rectPath addLineToPoint:CGPointMake(startPoint.x, startPoint.y - GCCircleRadius*2.4)];
[rectPath addLineToPoint:CGPointMake(startPoint.x + powf(3, 0.5)*GCCircleRadius*1.2, startPoint.y - GCCircleRadius*2.4)];
[rectPath addLineToPoint:CGPointMake(startPoint.x + powf(3, 0.5)*GCCircleRadius*1.2, startPoint.y - 2)];
[rectPath addLineToPoint:CGPointMake(startPoint.x - 2.5, startPoint.y - 2)];

CAShapeLayer *layer = [CAShapeLayer layer];
layer.path = rectPath.CGPath;
layer.lineWidth = 5;
layer.strokeColor = color;
layer.fillColor = [UIColor clearColor].CGColor;
[self addSublayer:layer];

CABasicAnimation *rectAniamtion = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
rectAniamtion.fromValue = @(0.0);
rectAniamtion.toValue = @(1.0);
rectAniamtion.duration = 0.8;
rectAniamtion.delegate = self;
if (animationValue.length) {
[rectAniamtion setValue:@"rectAniamtion" forKey:@"animationName"];
}
[layer addAnimation:rectAniamtion forKey:nil];

return rectAniamtion;
}

间隔的话,我们直接调用- performSelector: withObject: afterDelay:来延迟执行第二条边框的绘制就好

0x05 水面上涨动画

这个动画的关键就是用- addCurveToPoint: controlPoint1: controlPoint2:方法来画出水波的线,这个方法主要是利用controlPoint1controlPoint2这两个点来控制弧度方向,如图:

然后我们只需要交叉改变controlPoint1controlPoint2这两个点在上下的位置和startPointendPoint的位置,就能感觉水面上涨的感觉

1
2
3
4
5
NSMutableArray <UIBezierPath *> *waterPathArray = [NSMutableArray array];
for (NSInteger i = 0; i < 11; i++) {
UIBezierPath *water = [self water:i % 2 == 0 ? YES : NO withProgress:0.1*i];
[waterPathArray addObject:water];
}

创建完毕path之后,将anmations放到CAAnimationGroup里面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (void)addWaterAnimation:(NSMutableArray <UIBezierPath *> *)waterArray {
NSMutableArray <CABasicAnimation *> *animationArray = [NSMutableArray array];
for (NSInteger i = 0; i < waterArray.count - 1; i++) {
CABasicAnimation *waterAniamtion = [CABasicAnimation animationWithKeyPath:@"path"];
waterAniamtion.fromValue = (__bridge id _Nullable)(waterArray[i].CGPath);
waterAniamtion.toValue = (__bridge id _Nullable)(waterArray[i + 1].CGPath);
waterAniamtion.duration = 0.2;
waterAniamtion.beginTime = i == 0 ? 0.0 : animationArray[i - 1].beginTime + animationArray[i - 1].duration;
[animationArray addObject:waterAniamtion];
}

CAAnimationGroup *group = [CAAnimationGroup animation];
group.animations = animationArray;
group.duration = [animationArray lastObject].beginTime + [animationArray lastObject].duration;
group.fillMode = kCAFillModeForwards;
group.removedOnCompletion = NO;
group.delegate = self;
[group setValue:@"waterAnimation" forKey:@"animationName"];
[self.waterLayer addAnimation:group forKey:nil];
}

0x06 中间矩形放大至全屏

和前面一样,创建好全屏大小的path之后,然后加上动画即可

0x07 中间logo跟着出现

这个改变bounds即可

1
2
3
4
5
6
7
8
9
10
11
12
13
CALayer *logoLayer = [CALayer layer];
logoLayer.contents = (__bridge id _Nullable)([UIImage imageNamed:@"logo.jpg"].CGImage);

logoLayer.frame = CGRectMake(GCLoadingLayerCenterX, GCLoadingLayerCenterY, 0, 0);
[self addSublayer:logoLayer];

CABasicAnimation *logoAnimation = [CABasicAnimation animationWithKeyPath:@"bounds"];
logoAnimation.toValue = [NSValue valueWithCGRect:CGRectMake(GCLoadingLayerCenterX, GCLoadingLayerCenterY, 100, 120)];
logoAnimation.duration = 0.2;
logoAnimation.beginTime = 0.0;
logoAnimation.removedOnCompletion = NO;
logoAnimation.fillMode = kCAFillModeForwards;
[logoLayer addAnimation:logoAnimation forKey:nil];

这个加载动画的缺点就是在加载时没有可定制化的形状,只能修改圆形等的颜色,如果要改变形状,可能会涉及到动效的改动,所以这个动画只能作为学习分析参考