前言
此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是我最大的支持