前言
此demo主要注重,按钮的动画并没有实现离线下载,如果对动画不感兴趣的老铁就可以不看了。
效果来源:网络搜索微操作的时候搜到的
downloadButton.gif
分析
看到一个动画我们不要一拿到就开始做,首先多分析分析,这个怎么实现有哪些元素构成
开始状态开始做下载操作之前会有个竖线变成小圆点,然后网上抛出去的动画
抛出.png然后是下载中
下载中.png
还有最后的完成状态
完成.png
总的我们归整以下只有背景圆、竖线、箭头、进度圆、波浪(最后的勾也是波浪的一种状态)和显示文件大小的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];
}
源码位置
网友评论