今天給和大家分享的是抖音右小角底部的專輯動(dòng)畫
上圖看下:

再看下抖音的:

具體實(shí)現(xiàn)思路,首先需要3張素材
ContrainerView
Background Layer
Album (UIImageView)
image.png
1.我們首先寫個(gè) MusicAlbumView 繼承自UIView
@interface MusicAlbumView : UIView
@property (nonatomic, strong) UIImageView *album;
// 開始動(dòng)畫 rate 動(dòng)畫時(shí)間系數(shù)
- (void)startAnimation:(CGFloat)rate;
// 重置視圖 刪除所有已添加的動(dòng)畫組
- (void)resetView;
@end
(1)并提供兩個(gè)接口,一個(gè)開始動(dòng)畫,一個(gè)重置動(dòng)畫
(2)album 成員變量 是為了給外部加載網(wǎng)絡(luò)圖片使用 所以暴露在.h中, 例如下面的調(diào)用
__weak __typeof(self) wself = self;
//加載網(wǎng)絡(luò)圖
[self.musicAlbum.album sd_setImageWithURL:[NSURL URLWithString:@"https://www.sunyazhou.com/images/logo2.jpg"] completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
if(!error) {
wself.musicAlbum.album.image = image;
}
}];
2.下面我們來看下內(nèi)部如何封裝
首先我們要?jiǎng)?chuàng)建背景
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.noteLayers = [NSMutableArray array];
//專輯背景容器視圖
self.albumContainer =[[UIView alloc]initWithFrame:self.bounds];
[self addSubview:self.albumContainer];
}
return self;
}
(1)這里初始化的數(shù)組是為下面裝動(dòng)畫layer使用 方便 Reset的時(shí)候 移除所有l(wèi)ayer和動(dòng)畫
(2)一個(gè)產(chǎn)品背景容器UIView + 一個(gè)產(chǎn)品背景Layer + 一個(gè)人頭像背景UIImageView,我們依次把下面代碼寫在[self addSubview:self.albumContainer]底部
3.添加唱片背景
//添加唱片icon的layer
CALayer *backgroudLayer = [CALayer layer];
backgroudLayer.frame = self.bounds;
backgroudLayer.contents = (id)[UIImage imageNamed:@"music_cover"].CGImage;
[self.albumContainer.layer addSublayer:backgroudLayer];
//放在唱片內(nèi)部的圖片
CGFloat w = CGRectGetWidth(frame) / 2.0f;
CGFloat h = CGRectGetHeight(frame) / 2.0f;
CGRect albumFrame = CGRectMake(w / 2.0f, h / 2.0f, w, h);
self.album = [[UIImageView alloc]initWithFrame:albumFrame];
self.album.contentMode = UIViewContentModeScaleAspectFill;
[self.albumContainer addSubview:self.album];
self.album.layer.cornerRadius = h / 2.0f;
self.album.layer.masksToBounds = YES;
然后居中對(duì)齊。
4.專輯加旋轉(zhuǎn)動(dòng)畫
給self.albumContainer.layer加旋轉(zhuǎn)
我們?cè)谕獠空{(diào)用startAnimation:方法的時(shí)候 給self.albumContainer.layer添加旋轉(zhuǎn)動(dòng)畫旋轉(zhuǎn)
- (void)startAnimation:(CGFloat)rate {
CABasicAnimation* rotationAnimation;
rotationAnimation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
rotationAnimation.toValue = [NSNumber numberWithFloat: M_PI * 2.0];
rotationAnimation.duration = 3.0f;
rotationAnimation.cumulative = YES;
rotationAnimation.repeatCount = MAXFLOAT;
[self.albumContainer.layer addAnimation:rotationAnimation forKey:@"rotationAnimation"];
}
加完效果是這樣的

5.如何實(shí)現(xiàn)弧度動(dòng)畫
好 完成一半了 下面我們來說一下 弧度旋轉(zhuǎn).
先仔細(xì)觀察一下動(dòng)畫的音符

這是一張音符動(dòng)畫 它的運(yùn)動(dòng)軌跡大概是這樣的

我們其實(shí)用到的是貝塞爾曲線動(dòng)畫 (我畫的不是很好 大家理解這個(gè)意思就好)
然后讓音符的layer沿著 這個(gè)貝塞爾曲線做旋轉(zhuǎn)… 其實(shí)是下面的一些列動(dòng)作組合
這個(gè)需要一個(gè)動(dòng)畫組 包含如下動(dòng)作
1.一個(gè)貝塞爾曲線運(yùn)動(dòng)的軌跡動(dòng)畫啊
2.旋轉(zhuǎn)弧度 大概半圈 小一些 M_PI * 0.10 ~ M_PI * -0.10 之間旋轉(zhuǎn)的動(dòng)畫
3.透明度 從0 到 1 在到 0 之間運(yùn)動(dòng)的透明度動(dòng)畫
4.縮放動(dòng)畫 從開始 1x 到 2x 之間變化
6.音符動(dòng)畫
(1)首先創(chuàng)建一個(gè)動(dòng)畫組(用來執(zhí)行多個(gè)動(dòng)畫)
CAAnimationGroup *animationGroup = [[CAAnimationGroup alloc]init];
animationGroup.duration = rate/4.0f;
animationGroup.beginTime = CACurrentMediaTime() + delayTime;
animationGroup.repeatCount = MAXFLOAT;
animationGroup.removedOnCompletion = NO;
animationGroup.fillMode = kCAFillModeForwards;
animationGroup.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
rate 外部傳入 delayTime是 動(dòng)畫組開始動(dòng)畫的延遲的時(shí)間 我們?cè)O(shè)置 delayTime 為0就是不延時(shí) 下面解釋為什么這么寫
(2)創(chuàng)建一個(gè)貝賽爾曲線動(dòng)畫(音符運(yùn)動(dòng)路徑)
//bezier路徑幀動(dòng)畫
CAKeyframeAnimation * pathAnimation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
//然后把這坨代碼加到 上面代碼的底部
CGFloat sideXLength = 40.0f; //X軸左右側(cè)偏移量
CGFloat sideYLength = 100.0f; //Y軸上下偏移量
CGPoint beginPoint = CGPointMake(CGRectGetMidX(self.bounds) - 5, //貝賽爾曲線開始點(diǎn)CGRectGetMaxY(self.bounds));
CGPoint endPoint = CGPointMake(beginPoint.x - sideXLength, beginPoint.y - sideYLength); //貝塞爾曲線結(jié)束點(diǎn)
NSInteger controlLength = 60; //貝塞爾曲線控制點(diǎn)長度
CGPoint controlPoint = CGPointMake(beginPoint.x - sideXLength/2.0f - controlLength, beginPoint.y - sideYLength/2.0f + controlLength); //貝塞爾曲線控制點(diǎn)
UIBezierPath *customPath = [UIBezierPath bezierPath]; //創(chuàng)建貝塞爾軌跡
[customPath moveToPoint:beginPoint];
[customPath addQuadCurveToPoint:endPoint controlPoint:controlPoint]; //核心代碼 二次曲線方程式 可以google查一下
pathAnimation.path = customPath.CGPath; //讓動(dòng)畫沿著軌跡運(yùn)動(dòng)
我來解釋一下 關(guān)鍵變量
(1).beginPoint 開始點(diǎn): 當(dāng)前視圖X坐標(biāo)中心 向 左偏移 5dp (X軸是左右) Y的坐標(biāo)是當(dāng)前視圖高度 就是最下面 (2).endPoint 結(jié)束點(diǎn): 開始點(diǎn)的X 減去 40左側(cè)偏移(就是距離左側(cè)更遠(yuǎn)) Y也是 減去偏移之后 到了 視圖的外部 左上方.
(3).controlPoint 控制點(diǎn): 開始點(diǎn) 比如 X是 30 - 60/2.0 - 60 = -60,顯然已經(jīng)跑到最左邊了 超出了視圖范圍, Y 后面是+ controlLength 說明是加大 Y坐標(biāo).
大家可以不用理解這些細(xì)節(jié) 看下面圖就好了

customPath: 貝塞爾曲線對(duì)象
[customPath moveToPoint:beginPoint];
//核心代碼 二次曲線方程式 可以google查一下
[customPath addQuadCurveToPoint:endPoint controlPoint:controlPoint];
//讓動(dòng)畫沿著軌跡運(yùn)動(dòng)
pathAnimation.path = customPath.CGPath;
這就是 增加開始點(diǎn) 結(jié)束點(diǎn) 控制點(diǎn)之后的貝塞爾軌跡,然后 設(shè)置軌跡動(dòng)畫的path就完事了.
這一步搞完 然后 把pathAnimation放到動(dòng)畫組中,然后創(chuàng)建一個(gè) 音符的layer添加動(dòng)畫組
[animationGroup setAnimations:@[pathAnimation]];
CAShapeLayer *layer = [CAShapeLayer layer];
layer.contents = (__bridge id _Nullable)([UIImage imageNamed:imageName].CGImage);
layer.frame = CGRectMake(beginPoint.x, beginPoint.y, 10, 10);
[self.layer addSublayer:layer];
[self.noteLayers addObject:layer];
[layer addAnimation:animationGroup forKey:nil];
[self.noteLayers addObject:layer];這行代碼是我們前面聲明的全局變量 存layer,reset的時(shí)候刪除相關(guān)layer和動(dòng)畫使用
我們來看下剛剛寫好的音符沿著上述定義的貝塞爾曲線運(yùn)動(dòng)

(3)為音符加旋轉(zhuǎn) 透明 縮放動(dòng)畫
//旋轉(zhuǎn)幀動(dòng)畫
CAKeyframeAnimation * rotationAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform.rotation"];
//這里實(shí)際上是控制動(dòng)畫開始弧度和結(jié)束弧度 M_PI(180°) 就是半圓 * 0.10 或者 * -0.10j是為了關(guān)鍵點(diǎn)上下偏移的18°的間隙
[rotationAnimation setValues:@[
[NSNumber numberWithFloat:0],
[NSNumber numberWithFloat:M_PI * 0.10],
[NSNumber numberWithFloat:M_PI * -0.10]]];
//透明度幀動(dòng)畫, 注意一下: 為了讓音符的圖片更生動(dòng)我們需要把layer.opacity = 0.0f; 這個(gè)音符透明 從而用透明度幀動(dòng)畫控制透明.
CAKeyframeAnimation * opacityAnimation = [CAKeyframeAnimation animationWithKeyPath:@"opacity"];
[opacityAnimation setValues:@[
[NSNumber numberWithFloat:0],
[NSNumber numberWithFloat:0.2f],
[NSNumber numberWithFloat:0.7f],
[NSNumber numberWithFloat:0.2f],
[NSNumber numberWithFloat:0]]];
//縮放幀動(dòng)畫
CABasicAnimation *scaleAnimation = [CABasicAnimation animation];
scaleAnimation.keyPath = @"transform.scale";
scaleAnimation.fromValue = @(1.0f);
scaleAnimation.toValue = @(2.0f);
最后添把所有的音符動(dòng)畫添加到動(dòng)畫組中:
[animationGroup setAnimations:@[pathAnimation, scaleAnimation, rotationAnimation,opacityAnimation]];
然后封裝好方法 把上邊我們做的貝塞爾曲線 透明 漸變 縮放 動(dòng)畫組都放在這個(gè)方法里面。
完整代碼如下:
- (void)addNotoAnimation:(NSString *)imageName
delayTime:(NSTimeInterval)delayTime
rate:(CGFloat)rate{
CAAnimationGroup *animationGroup = [[CAAnimationGroup alloc]init];
animationGroup.duration = rate/4.0f;
animationGroup.beginTime = CACurrentMediaTime() + delayTime;
animationGroup.repeatCount = MAXFLOAT;
animationGroup.removedOnCompletion = NO;
animationGroup.fillMode = kCAFillModeForwards;
animationGroup.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
//bezier路徑幀動(dòng)畫
CAKeyframeAnimation * pathAnimation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
//X軸左右側(cè)偏移量
CGFloat sideXLength = 40.0f;
//Y軸上下偏移量
CGFloat sideYLength = 100.0f;
//貝賽爾曲線開始點(diǎn)
CGPoint beginPoint = CGPointMake(CGRectGetMidX(self.bounds) - 5, CGRectGetMaxY(self.bounds));
//貝塞爾曲線結(jié)束點(diǎn)
CGPoint endPoint = CGPointMake(beginPoint.x - sideXLength, beginPoint.y - sideYLength);
//貝塞爾曲線控制點(diǎn)長度
NSInteger controlLength = 60;
//貝塞爾曲線控制點(diǎn)
CGPoint controlPoint = CGPointMake(beginPoint.x - sideXLength/2.0f - controlLength, beginPoint.y - sideYLength/2.0f + controlLength);
//創(chuàng)建貝塞爾軌跡
UIBezierPath *customPath = [UIBezierPath bezierPath];
[customPath moveToPoint:beginPoint];
//核心代碼 二次曲線方程式 可以google查一下
[customPath addQuadCurveToPoint:endPoint controlPoint:controlPoint];
//讓動(dòng)畫沿著軌跡運(yùn)動(dòng)
pathAnimation.path = customPath.CGPath;
//旋轉(zhuǎn)幀動(dòng)畫
CAKeyframeAnimation * rotationAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform.rotation"];
//這里實(shí)際上是控制動(dòng)畫開始弧度和結(jié)束弧度 M_PI(180°) 就是半圓 * 0.10 或者 * -0.10j是為了關(guān)鍵點(diǎn)上下偏移的18°的間隙
[rotationAnimation setValues:@[
[NSNumber numberWithFloat:0],
[NSNumber numberWithFloat:M_PI * 0.10],
[NSNumber numberWithFloat:M_PI * -0.10]]];
//透明度幀動(dòng)畫
CAKeyframeAnimation * opacityAnimation = [CAKeyframeAnimation animationWithKeyPath:@"opacity"];
[opacityAnimation setValues:@[
[NSNumber numberWithFloat:0],
[NSNumber numberWithFloat:0.2f],
[NSNumber numberWithFloat:0.7f],
[NSNumber numberWithFloat:0.2f],
[NSNumber numberWithFloat:0]]];
//縮放幀動(dòng)畫
CABasicAnimation *scaleAnimation = [CABasicAnimation animation];
scaleAnimation.keyPath = @"transform.scale";
scaleAnimation.fromValue = @(1.0f);
scaleAnimation.toValue = @(2.0f);
[animationGroup setAnimations:@[pathAnimation, scaleAnimation, rotationAnimation,opacityAnimation]];
CAShapeLayer *layer = [CAShapeLayer layer];
layer.opacity = 0.0f;
layer.contents = (__bridge id _Nullable)([UIImage imageNamed:imageName].CGImage);
layer.frame = CGRectMake(beginPoint.x, beginPoint.y, 10, 10);
[self.layer addSublayer:layer];
[self.noteLayers addObject:layer];
[layer addAnimation:animationGroup forKey:nil];
}
//在我們對(duì)外提供的startAnimation:方法中調(diào)用(.h中)
- (void)startAnimation:(CGFloat)rate {
rate = fabs(rate); //check 防止 rate輸入為負(fù)值
[self resetView]; //首先重置動(dòng)畫
//這里調(diào)用一個(gè)音符的動(dòng)畫
[self addNotoAnimation:@"icon_home_musicnote1" delayTime:0.0f rate:rate];
//。。。封面的旋轉(zhuǎn)動(dòng)畫
}
//寫到這里大概就完成了一個(gè)音符的動(dòng)畫 如果像做多個(gè)音符動(dòng)畫 就多調(diào)用幾次 然后控制好開始時(shí)間的延時(shí)
[self addNotoAnimation:@"icon_home_musicnote1" delayTime:0.0f rate:rate];
[self addNotoAnimation:@"icon_home_musicnote2" delayTime:1.0f rate:rate];
[self addNotoAnimation:@"icon_home_musicnote3" delayTime:2.0f rate:rate];
寫到這里可以看到我們實(shí)際上是 通過delayTime 延時(shí)(單位秒) 來控制 每個(gè)音符 距離上個(gè)音符的間隔時(shí)間,通過間隔時(shí)間來控制音符之間 交替 出現(xiàn).
所以上面的動(dòng)畫組里面有這樣一行代碼
animationGroup.beginTime = CACurrentMediaTime() + delayTime;
就是基于當(dāng)前的時(shí)間延遲1秒或者2秒來控制
完成之后 就是這樣了

總結(jié)
首先感謝開源的小伙伴 的代碼,我認(rèn)真研讀了幾遍也寫了一些代碼,有些東西真是 天下大事必做于細(xì) 天下難事必做于易的感受.
這里的代碼實(shí)現(xiàn)主要分開 專輯圖旋轉(zhuǎn)和音符動(dòng)畫組的實(shí)現(xiàn)即可
希望和大家分享 技術(shù)技巧.寫的比較凌亂 我會(huì)逐漸提高這方面的能力.希望大家多多指教
附原文地址
