iOS 抖音專輯動(dòng)畫

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

上圖看下:


image.gif

再看下抖音的:


image.png

具體實(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"];
}

加完效果是這樣的


image.png

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

image.png

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

我們其實(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é) 看下面圖就好了


image.png

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)

image.png

(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秒來控制
完成之后 就是這樣了


image.png

總結(jié)
首先感謝開源的小伙伴 的代碼,我認(rèn)真研讀了幾遍也寫了一些代碼,有些東西真是 天下大事必做于細(xì) 天下難事必做于易的感受.

這里的代碼實(shí)現(xiàn)主要分開 專輯圖旋轉(zhuǎn)和音符動(dòng)畫組的實(shí)現(xiàn)即可

希望和大家分享 技術(shù)技巧.寫的比較凌亂 我會(huì)逐漸提高這方面的能力.希望大家多多指教

原文地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容