iOS 抖音专辑动画

今天给和大家分享的是抖音右小角底部的专辑动画

上图看下:


image.gif

再看下抖音的:


image.png

具体实现思路,首先需要3张素材

ContrainerView
Background Layer
Album (UIImageView)


image.png

1.我们首先写个 MusicAlbumView 继承自UIView

@interface MusicAlbumView : UIView

@property (nonatomic, strong) UIImageView  *album;
// 开始动画 rate 动画时间系数
- (void)startAnimation:(CGFloat)rate;
// 重置视图 删除所有已添加的动画组
- (void)resetView;

@end

(1)并提供两个接口,一个开始动画,一个重置动画
(2)album 成员变量 是为了给外部加载网络图片使用 所以暴露在.h中, 例如下面的调用

__weak __typeof(self) wself = self;
//加载网络图
[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.下面我们来看下内部如何封装
首先我们要创建背景

- (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)这里初始化的数组是为下面装动画layer使用 方便 Reset的时候 移除所有layer和动画
(2)一个产品背景容器UIView + 一个产品背景Layer + 一个人头像背景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];
//放在唱片内部的图片
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;

然后居中对齐。

4.专辑加旋转动画
给self.albumContainer.layer加旋转
我们在外部调用startAnimation:方法的时候 给self.albumContainer.layer添加旋转动画旋转

- (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.如何实现弧度动画
好 完成一半了 下面我们来说一下 弧度旋转.
先仔细观察一下动画的音符

image.png

这是一张音符动画 它的运动轨迹大概是这样的
image.png

我们其实用到的是贝塞尔曲线动画 (我画的不是很好 大家理解这个意思就好)
然后让音符的layer沿着 这个贝塞尔曲线做旋转… 其实是下面的一些列动作组合
这个需要一个动画组 包含如下动作

1.一个贝塞尔曲线运动的轨迹动画啊
2.旋转弧度 大概半圈 小一些 M_PI * 0.10 ~ M_PI * -0.10 之间旋转的动画
3.透明度 从0 到 1 在到 0 之间运动的透明度动画
4.缩放动画 从开始 1x 到 2x 之间变化

6.音符动画
(1)首先创建一个动画组(用来执行多个动画)

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是 动画组开始动画的延迟的时间 我们设置 delayTime 为0就是不延时 下面解释为什么这么写

(2)创建一个贝赛尔曲线动画(音符运动路径)

//bezier路径帧动画
CAKeyframeAnimation * pathAnimation = [CAKeyframeAnimation animationWithKeyPath:@"position"];

//然后把这坨代码加到 上面代码的底部
CGFloat sideXLength = 40.0f;  //X轴左右侧偏移量
CGFloat sideYLength = 100.0f; //Y轴上下偏移量

CGPoint beginPoint = CGPointMake(CGRectGetMidX(self.bounds) - 5,  //贝赛尔曲线开始点CGRectGetMaxY(self.bounds));
CGPoint endPoint = CGPointMake(beginPoint.x - sideXLength, beginPoint.y - sideYLength); //贝塞尔曲线结束点
NSInteger controlLength = 60; //贝塞尔曲线控制点长度
CGPoint controlPoint = CGPointMake(beginPoint.x - sideXLength/2.0f - controlLength, beginPoint.y - sideYLength/2.0f + controlLength); //贝塞尔曲线控制点

UIBezierPath *customPath = [UIBezierPath bezierPath]; //创建贝塞尔轨迹
[customPath moveToPoint:beginPoint];
[customPath addQuadCurveToPoint:endPoint controlPoint:controlPoint]; //核心代码 二次曲线方程式 可以google查一下

pathAnimation.path = customPath.CGPath; //让动画沿着轨迹运动

我来解释一下 关键变量

(1).beginPoint 开始点: 当前视图X坐标中心 向 左偏移 5dp (X轴是左右) Y的坐标是当前视图高度 就是最下面 (2).endPoint 结束点: 开始点的X 减去 40左侧偏移(就是距离左侧更远) Y也是 减去偏移之后 到了 视图的外部 左上方.
(3).controlPoint 控制点: 开始点 比如 X是 30 - 60/2.0 - 60 = -60,显然已经跑到最左边了 超出了视图范围, Y 后面是+ controlLength 说明是加大 Y坐标.

大家可以不用理解这些细节 看下面图就好了


image.png

customPath: 贝塞尔曲线对象

[customPath moveToPoint:beginPoint];
//核心代码 二次曲线方程式 可以google查一下
[customPath addQuadCurveToPoint:endPoint controlPoint:controlPoint];
//让动画沿着轨迹运动
pathAnimation.path = customPath.CGPath;

这就是 增加开始点 结束点 控制点之后的贝塞尔轨迹,然后 设置轨迹动画的path就完事了.
这一步搞完 然后 把pathAnimation放到动画组中,然后创建一个 音符的layer添加动画组

[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的时候删除相关layer和动画使用

我们来看下刚刚写好的音符沿着上述定义的贝塞尔曲线运动

image.png

(3)为音符加旋转 透明 缩放动画

//旋转帧动画
CAKeyframeAnimation * rotationAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform.rotation"];
//这里实际上是控制动画开始弧度和结束弧度 M_PI(180°) 就是半圆 * 0.10 或者 * -0.10j是为了关键点上下偏移的18°的间隙
[rotationAnimation setValues:@[
                               [NSNumber numberWithFloat:0],
                               [NSNumber numberWithFloat:M_PI * 0.10],
                               [NSNumber numberWithFloat:M_PI * -0.10]]];
//透明度帧动画, 注意一下: 为了让音符的图片更生动我们需要把layer.opacity = 0.0f; 这个音符透明 从而用透明度帧动画控制透明.
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]]];
//缩放帧动画
CABasicAnimation *scaleAnimation = [CABasicAnimation animation];
scaleAnimation.keyPath = @"transform.scale";
scaleAnimation.fromValue = @(1.0f);
scaleAnimation.toValue = @(2.0f);

最后添把所有的音符动画添加到动画组中:

[animationGroup setAnimations:@[pathAnimation, scaleAnimation,  rotationAnimation,opacityAnimation]];

然后封装好方法 把上边我们做的贝塞尔曲线 透明 渐变 缩放 动画组都放在这个方法里面。

完整代码如下:

- (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路径帧动画
    CAKeyframeAnimation * pathAnimation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
    
    //X轴左右侧偏移量
    CGFloat sideXLength = 40.0f;
    //Y轴上下偏移量
    CGFloat sideYLength = 100.0f;
    
    //贝赛尔曲线开始点
    CGPoint beginPoint = CGPointMake(CGRectGetMidX(self.bounds) - 5, CGRectGetMaxY(self.bounds));
    //贝塞尔曲线结束点
    CGPoint endPoint = CGPointMake(beginPoint.x - sideXLength, beginPoint.y - sideYLength);
    //贝塞尔曲线控制点长度
    NSInteger controlLength = 60;
    //贝塞尔曲线控制点
    CGPoint controlPoint = CGPointMake(beginPoint.x - sideXLength/2.0f - controlLength, beginPoint.y - sideYLength/2.0f + controlLength);
    //创建贝塞尔轨迹
    UIBezierPath *customPath = [UIBezierPath bezierPath];
    [customPath moveToPoint:beginPoint];
    //核心代码 二次曲线方程式 可以google查一下
    [customPath addQuadCurveToPoint:endPoint controlPoint:controlPoint];
    //让动画沿着轨迹运动
    pathAnimation.path = customPath.CGPath;
    
    
    //旋转帧动画
    CAKeyframeAnimation * rotationAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform.rotation"];
    //这里实际上是控制动画开始弧度和结束弧度 M_PI(180°) 就是半圆 * 0.10 或者 * -0.10j是为了关键点上下偏移的18°的间隙
    [rotationAnimation setValues:@[
                                   [NSNumber numberWithFloat:0],
                                   [NSNumber numberWithFloat:M_PI * 0.10],
                                   [NSNumber numberWithFloat:M_PI * -0.10]]];
    //透明度帧动画
    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]]];
    //缩放帧动画
    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];
}

//在我们对外提供的startAnimation:方法中调用(.h中)
- (void)startAnimation:(CGFloat)rate {
    rate = fabs(rate);  //check 防止 rate输入为负值
    [self resetView];   //首先重置动画
    //这里调用一个音符的动画
    [self addNotoAnimation:@"icon_home_musicnote1" delayTime:0.0f rate:rate];
    //。。。封面的旋转动画    
}
//写到这里大概就完成了一个音符的动画 如果像做多个音符动画 就多调用几次 然后控制好开始时间的延时
[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];

写到这里可以看到我们实际上是 通过delayTime 延时(单位秒) 来控制 每个音符 距离上个音符的间隔时间,通过间隔时间来控制音符之间 交替 出现.

所以上面的动画组里面有这样一行代码

animationGroup.beginTime = CACurrentMediaTime() + delayTime;

就是基于当前的时间延迟1秒或者2秒来控制
完成之后 就是这样了


image.png

总结
首先感谢开源的小伙伴 的代码,我认真研读了几遍也写了一些代码,有些东西真是 天下大事必做于细 天下难事必做于易的感受.

这里的代码实现主要分开 专辑图旋转和音符动画组的实现即可

希望和大家分享 技术技巧.写的比较凌乱 我会逐渐提高这方面的能力.希望大家多多指教

原文地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容