前言
此demo主要注重,按钮的动画并没有实现离线下载,如果对动画不感兴趣的老铁就可以不看了。
效果来源:网络搜索微操作的时候搜到的
分析
看到一个动画我们不要一拿到就开始做,首先多分析分析,这个怎么实现有哪些元素构成
开始做下载操作之前会有个竖线变成小圆点,然后网上抛出去的动画
然后是下载中
还有最后的完成状态
总的我们归整以下只有背景圆、竖线、箭头、进度圆、波浪(最后的勾也是波浪的一种状态)和显示文件大小的Label。这里对于图形的绘制我们都选择
CAShapeLayer
/**
背景圆
*/
@property(nonatomic,strong)CAShapeLayer *bgCircleShapeLayer;
/** 竖线*/
@property(nonatomic,strong)CAShapeLayer *pointShapeLayer;
/** 箭头*/
@property(nonatomic,strong)CAShapeLayer *arrowShapeLayer;
/** 进度*/
@property(nonatomic,strong)CAShapeLayer *progressShapeLayer;
/** 波浪*/
@property(nonatomic,strong)AIDownloadWaveLayer *waveLayer;
/** 文件大小*/
@property(nonatomic,weak)UILabel *progressLabel;
动画
点击了按钮到实际开始下载之间,我们是有个动画
1、竖线变成点
这种变路径代码我们选择基础动画中的path
属性
//变为点
UIBezierPath *pointPath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.ai_middleX, self.ai_middleY + 1) radius:.5 startAngle:0 endAngle:2*M_PI clockwise:NO];
CABasicAnimation *changeToPoint = [CABasicAnimation animationWithKeyPath:@"path"];
changeToPoint.toValue = (__bridge id)(pointPath.CGPath);
changeToPoint.fillMode = kCAFillModeForwards;
changeToPoint.removedOnCompletion = NO;
changeToPoint.duration = .2;
[self.pointShapeLayer addAnimation:changeToPoint forKey:nil];
2、箭头变成直线
在变成直线的时候纵坐标在网上移动一点,这样会有种圆点是被线弹上去的感觉
上下的位移肯定选择position.y
箭头变为线要有种把圆点弹出去的感觉动画选择CASpringAnimation
属性还是选择path
这里直线的路径要注意节点左右对称不然达不到左右一样的效果。
//箭头变为线
CABasicAnimation *lineAniamtion = [CABasicAnimation animationWithKeyPath:@"position.y"];
lineAniamtion.duration = .2;
lineAniamtion.fillMode = kCAFillModeBackwards;
lineAniamtion.toValue = @(self.arrowShapeLayer.y +10);
lineAniamtion.removedOnCompletion = NO;
UIBezierPath *linePath = [self linePath];
CASpringAnimation *lineSpringAnimation = [CASpringAnimation animationWithKeyPath:@"path"];
lineSpringAnimation.toValue = (__bridge id _Nullable)(linePath.CGPath);
lineSpringAnimation.duration = lineSpringAnimation.settlingDuration;
lineSpringAnimation.damping = 0;
lineSpringAnimation.mass = 30;
lineSpringAnimation.stiffness = 5;
lineSpringAnimation.initialVelocity = 30;
lineSpringAnimation.fillMode = kCAFillModeForwards;
lineSpringAnimation.beginTime = .2;
lineSpringAnimation.removedOnCompletion = NO;
CAAnimationGroup *groupAnimation = [CAAnimationGroup animation];
groupAnimation.duration = 1;
groupAnimation.fillMode = kCAFillModeForwards;
groupAnimation.removedOnCompletion = NO;
groupAnimation.animations = @[lineAniamtion,lineSpringAnimation];
[self.arrowShapeLayer addAnimation:groupAnimation forKey:nil];
//圆点起跳
CASpringAnimation *pointSpringAnimation = [CASpringAnimation animationWithKeyPath:@"position.y"];
pointSpringAnimation.delegate = self;
[pointSpringAnimation setValue:@"pointLayer" forKey:@"name"];
pointSpringAnimation.toValue = @(-self.ai_height*.5 - self.bgCircleShapeLayer.lineWidth * 0.5 + self.pointShapeLayer.lineWidth * 0.5 );
pointSpringAnimation.duration = pointSpringAnimation.settlingDuration;
pointSpringAnimation.fillMode = kCAFillModeForwards;
pointSpringAnimation.removedOnCompletion = NO;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(changeToPoint.duration * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self.pointShapeLayer addAnimation:pointSpringAnimation forKey:nil];
});
3、圆点起跳到背景圆上
这里我们还是选择CASpringAnimation
动画上下移动就选择position.y
属性,由于这个小圆点跳出完成后才开算下载,我们要在圆点起跳动画完成后回调,我们为动画添加key/value在动画结束的时候好找到。
[pointSpringAnimation setValue:@"pointLayer" forKey:@"name"];
//圆点起跳
CASpringAnimation *pointSpringAnimation = [CASpringAnimation animationWithKeyPath:@"position.y"];
pointSpringAnimation.delegate = self;
[pointSpringAnimation setValue:@"pointLayer" forKey:@"name"];
pointSpringAnimation.toValue = @(-self.ai_height*.5 - self.bgCircleShapeLayer.lineWidth * 0.5 + self.pointShapeLayer.lineWidth * 0.5 );
pointSpringAnimation.duration = pointSpringAnimation.settlingDuration;
pointSpringAnimation.fillMode = kCAFillModeForwards;
pointSpringAnimation.removedOnCompletion = NO;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(changeToPoint.duration * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self.pointShapeLayer addAnimation:pointSpringAnimation forKey:nil];
});
开始下载
下载的时候要做几件事
0、状态变更,然后回调
1、隐藏变为直线的箭头
[self opacityAnimationWithLayer:self.arrowShapeLayer fromValue:1. toValue:0.];
2、文件大小label显示并变大
//文件大小
[self scaleAnimationWithLayer:self.progressLabel.layer fromValue:.1 toValue:1.];
[self opacityAnimationWithLayer:self.progressLabel.layer fromValue:0. toValue:1.];
1和2的步骤都有不透明度的变化,所以这里直接把透明度提取出一个方法,我这里是使用pop动画,想多熟悉下当让用CABaseAnimation
也可以
/**
不透明度动画
@param layer 要执行动画的layer
@param from 从多少开始
@param to 到多少
*/
- (void)opacityAnimationWithLayer:(CALayer*)layer fromValue:(CGFloat)from toValue:(CGFloat)to {
POPBasicAnimation *opacityAnimation = [POPBasicAnimation animationWithPropertyNamed:kPOPLayerOpacity];
opacityAnimation.toValue = @(to);
opacityAnimation.fromValue = @(from);
opacityAnimation.duration = .3;
[layer pop_addAnimation:opacityAnimation forKey:nil];
}
然后待会下载完成后文件大小的label会有变小的动画,所以这里把,大小的变化也提取出一个方法
/**
缩放动画
@param layer 所要缩放的layer
@param from 从多少比例开始
@param to 到多少比例
*/
- (void)scaleAnimationWithLayer:(CALayer*)layer fromValue:(CGFloat)from toValue:(CGFloat)to {
//文件大小
POPBasicAnimation *scaleAnimation = [POPBasicAnimation animationWithPropertyNamed:kPOPLayerScaleXY];
scaleAnimation.fromValue = [NSValue valueWithCGPoint:CGPointMake(from, from)];
scaleAnimation.toValue = [NSValue valueWithCGPoint:CGPointMake(to, to)];
scaleAnimation.duration = .3;
[layer pop_addAnimation:scaleAnimation forKey:nil];
}
提取这些方法的好处不止于这里减少重复代码,而且以后有类似的动画也可以直接复制过去使用
3、添加波浪动画
这里我简单说下波浪动画原理
- 正弦函数: y =Asin(ωx+φ)+C
- A 表示振幅,也就是使用这个变量来调整波浪的高度
- ω表示周期,也就是使用这个变量来调整在屏幕内显示的波浪的数量
- φ表示波浪横向的偏移,也就是使用这个变量来调整波浪的流动
- C表示波浪纵向的位置,也就是使用这个变量来调整波浪在屏幕中竖直的位置。
在自己找到合适的A和ω后我们只需要改变φ我这里只做了四个波形,所以每个加π/2四个后刚好回到原点,波浪就是这这四个波形的切换然后就形成了一次波浪,在一次波浪完成后继续调用下次波浪。
下面是一次波浪动画的代码:
- (void)waveAnimateWithLayer:(CALayer*)layer {
// 1
CABasicAnimation *waveAnimationStart = [CABasicAnimation animationWithKeyPath:@"path"];
waveAnimationStart.fromValue = (__bridge id _Nullable)(self.wavePathStarting.CGPath);
waveAnimationStart.toValue = (__bridge id _Nullable)(self.wavePath1.CGPath);
waveAnimationStart.beginTime = 0.0;
waveAnimationStart.duration = KAnimationDuration;
// 2
CABasicAnimation *waveAnimation1 = [CABasicAnimation animationWithKeyPath:@"path"];
waveAnimation1.fromValue = (__bridge id _Nullable)(self.wavePath1.CGPath);
waveAnimation1.toValue = (__bridge id _Nullable)(self.wavePath2.CGPath);
waveAnimation1.beginTime = waveAnimationStart.beginTime + waveAnimationStart.duration;
waveAnimation1.duration = KAnimationDuration;
// 3
CABasicAnimation *waveAnimation2 = [CABasicAnimation animationWithKeyPath:@"path"];
waveAnimation2.fromValue = (__bridge id _Nullable)(self.wavePath2.CGPath);
waveAnimation2.toValue = (__bridge id _Nullable)(self.wavePath3.CGPath);
waveAnimation2.duration = KAnimationDuration;
waveAnimation2.beginTime = waveAnimation1.beginTime + waveAnimation1.duration;
// 4
CABasicAnimation *waveAnimationLow = [CABasicAnimation animationWithKeyPath:@"path"];
waveAnimationLow.fromValue = (__bridge id _Nullable)(self.wavePath3.CGPath);
waveAnimationLow.toValue = (__bridge id _Nullable)(self.wavePath4.CGPath);
waveAnimationLow.duration = KAnimationDuration;
waveAnimationLow.beginTime = waveAnimation2.beginTime + waveAnimation2.duration;
// 5
CABasicAnimation *waveAnimationCompelted = [CABasicAnimation animationWithKeyPath:@"path"];
waveAnimationCompelted.fromValue = (__bridge id _Nullable)(self.wavePath4.CGPath);
waveAnimationCompelted.toValue = (__bridge id _Nullable)(self.wavePathStarting.CGPath);
waveAnimationCompelted.duration = KAnimationDuration;
waveAnimationCompelted.beginTime = waveAnimationLow.beginTime + waveAnimationLow.duration;
// 6
CAAnimationGroup *animationGroup = [[CAAnimationGroup alloc] init];
animationGroup.delegate = self;
[animationGroup setValue:@"group" forKey:@"name"];
animationGroup.animations = \
@[waveAnimationStart,waveAnimation1,waveAnimation2,waveAnimationLow];
animationGroup.duration = waveAnimationLow.beginTime + waveAnimationLow.duration;
animationGroup.fillMode = kCAFillModeForwards;
animationGroup.removedOnCompletion = NO;
[layer addAnimation:animationGroup forKey:nil];
self.allAnimationDuration = animationGroup.duration;
}
接下来就是最实际的效果,让进度圆圈同步下载的进度,由于是逆时针所以设置strokeStart
-(void)setProgress:(CGFloat)progress {
_progress = progress;
self.progressShapeLayer.strokeStart = 1-progress;
if (progress >= 1) {
[self end];
}
}
复位
最后全部事情完成,如果想恢复到一开始的状态
1、进度条消失
进度条我这里并不是让他真的消失,而是让他宽度变为0
2、圆点变为竖线
3、箭头出现
4、移除波浪
/**
复位
*/
-(void)reset {
//变更状态
self.state = AIDownloadButtonNone;
[self.pointShapeLayer removeAllAnimations];
[self scaleAnimationWithLayer:self.progressLabel.layer fromValue:1. toValue:.1];
[self opacityAnimationWithLayer:self.progressLabel.layer fromValue:1. toValue:0];
self.progressShapeLayer.strokeStart = 1;
self.progress = 0.;
self.state = AIDownloadButtonNone;
//进度消失
POPBasicAnimation *progressAnimation = [POPBasicAnimation animationWithPropertyNamed:kPOPShapeLayerLineWidth];
progressAnimation.toValue = @0.;
progressAnimation.duration = .3;
[self.progressShapeLayer pop_addAnimation:progressAnimation forKey:nil];
//点变成竖线
UIBezierPath *pointPath = [UIBezierPath bezierPath];
[pointPath moveToPoint: CGPointMake(self.ai_middleX, self.ai_height *0.25)];
[pointPath addLineToPoint: CGPointMake(self.ai_middleX, self.ai_height *0.75 - self.arrowShapeLayer.lineWidth)];
CABasicAnimation *pointToLineAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
pointToLineAnimation.toValue = (__bridge id _Nullable)(pointPath.CGPath);
pointToLineAnimation.duration = .3;
pointToLineAnimation.removedOnCompletion = NO;
pointToLineAnimation.fillMode = kCAFillModeForwards;
[self.pointShapeLayer addAnimation:pointToLineAnimation forKey:nil];
//移除波浪
[self.waveLayer removeFromSuperlayer];
//箭头
self.arrowShapeLayer.opacity = 1.;
UIBezierPath *arrowPath = [UIBezierPath bezierPath];
[arrowPath moveToPoint: CGPointMake(self.ai_middleX * .75, self.ai_height *(0.25 + .5 * 0.6))];
[arrowPath addLineToPoint: CGPointMake(self.ai_middleX, self.ai_height *0.75)];
[arrowPath addLineToPoint: CGPointMake(self.ai_middleX * 1.25, self.ai_height *(0.25 + .5 * 0.6))];
CABasicAnimation *arrowAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
arrowAnimation.toValue = (__bridge id _Nullable)(arrowPath.CGPath);
arrowAnimation.duration = .3;
arrowAnimation.removedOnCompletion = NO;
arrowAnimation.fillMode = kCAFillModeForwards;
[self.arrowShapeLayer addAnimation:arrowAnimation forKey:nil];
}
如果对基础动画使用有不太明白的可以看下我另一篇文章登录动画也可以在GitHub上查看源码,你的star是我最大的支持