iOS动画,从入门到放弃?

老规矩还是先上效果图视频效果点这里

效果1.gif

老规矩还是先吐槽

简书限制gif不超过10M我就忍了,我的图为什么还跑的这么快,又没人追你跑那么快干啥

然后外面怎么还在下雨,这都啥时候了还不晴天

然后怎么又下班了,时间怎么过得这么快

然后.......

前言

最近一直有朋友问我关于动画的相关问题,购物车加商品动画啊,水波纹啊,贝塞尔曲线啊种种,刚好前段时间项目中有相关功能,于是想着写篇博客,总结一下动画相关知识,也以免自己遗忘再去翻代码,毕竟作为高级工程师,封装的太高深,时间一长自己都看不懂了。

正文

iOS中常用的动画实现方式无非就是那几种,UIView动画,核心动画,帧动画,spring动画,json(Lottie)动画,自我总结了一下画了个动画合集思维导图如下所示。本来准备一项项的写,但是想了想网上随便哪个动画,一搜都是一大堆资料,再写也写不出来个花,于是另辟蹊径,直接分解效果图上的每一个动画,讲解相应的实现方式。


动画合集介绍.png

先介绍个属性

  • 在创建UIView对象时,UIView内部会自动创建一个层(即CALayer对象),UIView本身不具备显示的功能,是它内部的层才有显示功能。UIView是CALayer的管理器,CALayer的主要工作是为屏幕的绘制渲染提供所需的数据源,也就是说,你在屏幕上看到的内容,都是来源于CALayer。
  • 每一个UIView都有一个backing layer,UIView的UI属性跟CALayer的属性是一一对应的,设置UIView的UI属性实际上是设置CALayer对应的属性,即UIView的绘制渲染工作是由CALayer完成。
  • UIView对象之间存在着一定的层级关系,那么所以UIView的Backing Layer也相应的存在着一定的层级关系,这个层级关系叫做图层树(模型树)。
很重要的一点,layer不参与view的事件处理、不参与响应链,只负责显示,所以动画的操作,很大一部分程度上是对layer相关属性的操作。

1, 圆球扩大后再缩小 / 点击标签按钮抖动

CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"transform"];
NSMutableArray *values = [[NSMutableArray alloc]initWithCapacity:3];
[values addObject:[NSValue valueWithCATransform3D:CATransform3DMakeScale(0.1, 0.1, 1.0)]];
[values addObject:[NSValue valueWithCATransform3D:CATransform3DMakeScale(1.3, 1.3, 1.0)]];
[values addObject:[NSValue valueWithCATransform3D:CATransform3DMakeScale(0.9, 0.9, 1.0)]];
[values addObject:[NSValue valueWithCATransform3D:CATransform3DMakeScale(1.1, 1.1, 1.0)]];
[values addObject:[NSValue valueWithCATransform3D:CATransform3DMakeScale(1.0, 1.0, 1.0)]];
animation.values = values;
animation.duration = 0.5;
animation.removedOnCompletion = YES;
[self.layer addAnimation:animation forKey:nil];

2, 圆球中❤️❤️图标漂浮

介绍水球中❤️的浮动前,先来了解一个动画库Lottie,Lottie是一个为Android和iOS设备提供的一个开源框架,它能够解析通过Adobe After Effects 软件做出来的动画,动画文件通过Bodymovin导出json文件,然后由Lottie库解析json渲染动画,可以看官方示例网站,参考一下Lottie大概都能干些什么,比如说下图的这个开屏动画。

Lottie.gif

基本的用法如下

//从本地的json加载(json文件一般由设计师提供)
LOTAnimationView *hertView = [LOTAnimationView animationNamed:@"heart_inProgress"];
//从服务器的json加载
//LOTAnimationView *animationView = [[LOTAnimationView alloc] initWithContentsOfURL:[NSURL URLWithString:@""]];

hertView.cacheEnable = NO;

hertView.frame = frame;

hertView.contentMode = UIViewContentModeScaleToFill;

//设置播放速度
hertView.animationSpeed = 1.0;

//设置循环播放
hertView.loopAnimation = YES;

[self addSubview:hertView];

//播放     pause 暂停
[hertView play];

控制进度播放

//直接播放到指定进度
[animationView playToProgress:0.8 withCompletion:^(BOOL animationFinished) {
// do something
}];

//从进度A播放到进度B
[animationView playFromProgress:0 toProgress:0.8 withCompletion:^(BOOL animationFinished) {
// do something
}];

//直接设置当前进度
animationView.animationProgress = currentProgress;

Lottie同时还支持以下功能

  • 控制帧播放
  • 编辑某帧的动画对象的属性
  • 添加自定义的视图到指定的 Layer

总体来说功能还是蛮强大的,能够满足大部分动画需求,同时运行效率和也比数十张图片轮播实现某个系统函数实现不了的动画效率高的多。由于篇幅有限,对Lottie其余没介绍到的功能感兴趣的同学可以在评论区留言,共同探讨一二。

3, +1,-1图标飘出

产品经理给的需求是,取下方点击按钮中心区域为飘出标签的起点,做一个路径为S型的动画,如果点击按钮位于屏幕左侧,往右侧做S动画,同理按钮位于右侧,往左侧做S动画,标签透明度渐变,到达屏幕顶部中心时,完全透明消失。下图黑色虚线为大概路径。


标签飘出.png
分析一下需求实现方式
  • 确定动画的路径,可以画一条三次贝塞尔曲线,在每次点击事件中对贝塞尔曲线端点重新赋值以达到左右飘出的需求,然后将此路径赋值给layer的path。
  • 透明度渐变的话,假设动画时间为1秒,维护一个定时器,每隔1/10秒改变一次标签的透明度,当到动画完成到达顶部时标签刚好透明
  • 动画结束之后,在animationDidStop方法中可以直接销毁标签,也可以回收起来存到数组里,将属性全部清空,下一次点击取出再次利用,减少cpu频繁的alloc操作
问题

1次点击,屏幕中出现一个标签(UIImageView对象),如果点击迅速,屏幕中会出现数十个标签同时做动画,如何捕捉到每一个标签操作其透明度,难道要同时维护数十个定时器?而且animationDidStop方法也会被不停的调用,怎么做到准确的销毁每一个该被销毁的标签。

解决办法

维护一个全局数组与一个全局字典,每初始化一个标签(UIImageView对象),将此标签跟透明度存入一个局部字典dic,同时取当前的时间戳为key,局部字典dic对象为value。全局数组的作用相当于一个队列,后初始化的标签总是排在前一个标签的后面,动画结束代理方法调用时,每次默认删除第0个元素,以达到每次准确取出需要操作的标签的目的。

NSString *nowtime = [self getCurrentTime];
    
NSMutableDictionary *dic = [[NSMutableDictionary alloc] initWithCapacity:0];

[dic setObject:coverImgView forKey:@"btn"];
    
[dic setObject:@"1" forKey:@"alph"];
    
[self.btnDic setObject:dic forKey:nowtime];
    
[self.btnArry addObject:nowtime];

页面初始化时,init方法里只维护一个定时器,每个时间间隔内遍历字典,将按钮取出,改变其透明度。

// 设置回调
dispatch_source_set_event_handler(self.timer, ^{

    for (NSString *keyStr in [weakself.btnDic allKeys]) {
            
        NSDictionary *fistdic = [weakself.btnDic objectForKey:keyStr];

        UIButton *fistbtn = [fistdic objectForKey:@"btn"];
                
        if (fistbtn.alpha > 0) {
            istbtn.alpha = fistbtn.alpha - 0.15;
            [fistdic setValue:[NSString stringWithFormat:@"%f",fistbtn.alpha] forKey:@"alph"];
        }
 }

动画结束的代理方法中,将数组中第0个元素取出来,去字典中将此按钮取出删除,最后删除数据中第0个元素。记得删除动画,不然会多次调用addAnimation会引起内存泄漏。

- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {

    UIButton *resultbtn =  [[self.btnDic objectForKey:self.btnArry[0]] objectForKey:@"btn"];;
         
    [resultbtn removeFromSuperview];
         
    [self.btnDic removeObjectForKey:self.btnArry[0]];
         
    //每次删除动画 不然coverImgView多次调用addAnimation 会导致内存泄漏
    [_coverImgView.layer removeAnimationForKey:self.btnArry[0]];
         
    [self.btnArry removeObjectAtIndex:0];
}
关键问题解决后,就是简单的动画实现了,方式如下

1,初始化标签动画的路径

- (void)drawRect:(CGRect)rect {
    UIBezierPath bepath = [UIBezierPath bezierPath];
    bepath.lineWidth = 1.0;
    bepath.lineCapStyle = kCGLineCapRound;
    bepath.lineJoinStyle = kCGLineJoinRound;
    [bepath stroke];
}

2,初始化一个关于位移的关键帧动画

//初始化一个关于位移的关键帧动画 
CAKeyframeAnimation *anima = [CAKeyframeAnimation animationWithKeyPath:@"position"];
anima.delegate = self;
anima.path = bepath.CGPath;

//默认为YES,代表动画执行完毕后就从图层上移除,图形会恢复到动画执行前的状态。如果想让图层保持显示动画执行后的状态,那就设置为NO,不过还要设置fillMode为kCAFillModeForwards
anima.removedOnCompletion = NO;
anima.fillMode = kCAFillModeForwards;

anima.duration = 1.0;

3,按钮的点击事件里,动态的画每一条路径线

- (void)addTagAnimationWithBtn:(UIButton *)button {

    / /初始化一个tagImgView标签步骤省略

    NSString *nowtime = [self getCurrentTime];
    
    NSMutableDictionary *dic = [[NSMutableDictionary alloc] initWithCapacity:0];

    [dic setObject:tagImgView forKey:@"btn"];
    
    [dic setObject:@"1" forKey:@"alph"];
    
    [self.btnDic setObject:dic forKey:nowtime];
    
    [self.btnArry addObject:nowtime];

    [bepath removeAllPoints];

    //起点
    [bepath moveToPoint:CGPointMake(CGRectGetMidX(button.frame),CGRectGetMidY(button.frame)  + self.baseScrollview.frame.origin.y - self.scrollviewDeviationY)];

    //终点
    CGFloat endPointX = SCREEN_WIDTH / 2.0 - exceedWidth / 2.0 + arc4random() % exceedWidth;
    
    [bepath addCurveToPoint:CGPointMake(endPointX , 10) controlPoint1:CGPointMake(pointx, 200) controlPoint2:CGPointMake(pointx2, 200)];
    
    anima.path = bepath.CGPath;
    
    //+1图标是一个UIImageView对象  以
    [tagImgView.layer addAnimation:anima forKey:nowtime];
    
    [bepath closePath];
}

4, 圆球中水波纹

水波纹实现方式大同小异,基本都要用到正弦曲线或者余弦曲线,y=Asin(ωx+φ)+h,先画一条静态的三角函数。


静态.jpeg
- (void)setCurrentWaveLayerPath {
    // 通过正弦曲线来绘制波浪形状
    CGMutablePathRef path = CGPathCreateMutable();

    CGFloat y = self.currentWavePointY;

    CGPathMoveToPoint(path, nil, 0, y);

    CGFloat width = CGRectGetWidth(self.frame);

    for (float x = 0.0f; x <= width; x++){

        // 正弦波浪公式
        y = 2 * self.waveAmplitude * sin(1.5 * self.waveCycle * x + self.offsetX) + self.currentWavePointY;

        CGPathAddLineToPoint(path, nil, x, y);
    }

    CGPathAddLineToPoint(path, nil, width, CGRectGetHeight(self.frame));

    CGPathAddLineToPoint(path, nil, 0, CGRectGetHeight(self.frame));

    CGPathCloseSubpath(path);

    self.waveLayer.path = path;

    CGPathRelease(path);
}

要让曲线动起来,当不断改变x的值,曲线就会发生偏移,以达到动画效果,所以需要一个定时器。

//是用于同步屏幕刷新频率的计时器,调用屏幕会根据不同iphone设备自动调整,比普通定时器用于动画效果更自然
@property (nonatomic, strong) CADisplayLink *displayLink;

@property (nonatomic, strong) CAShapeLayer *waveLayer;  // 绘制第一条波形

@property (nonatomic, strong) CAShapeLayer *waveTwoLayer;  // 绘制第二条波形

@property (nonatomic, strong) CAShapeLayer *waveThreeLayer;  // 绘制第三条波形

// 绘制波形的变量定义,使用波形曲线y=Asin(ωx+φ)+k进行绘制
@property (nonatomic, assign) CGFloat waveAmplitude;  // 波纹振幅,A

@property (nonatomic, assign) CGFloat waveCycle;   // 波纹周期,T = 2π/ω

@property (nonatomic, assign) CGFloat offsetX;   // 波浪x位移,φ

@property (nonatomic, assign) CGFloat waveSpeed;  // 波纹速度,用来累加到相位φ上,达到波纹水平移动的效果

@property (nonatomic, assign) CGFloat waveGrowth;  // 波纹上升速度,累加到k上,达到波浪高度上升的效果

//用来计算波峰一定范围内的波动值
@property (nonatomic, assign) BOOL increase;
@property (nonatomic, assign) CGFloat variable; //波峰 

初始化渲染定时器

//启动同步渲染绘制波纹
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(setCurrentWave:)];
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];

在定时器的每次回调方法中,做水波纹上升动画跟水平方向上波动动画

- (void)setCurrentWave:(CADisplayLink *)displayLink {
    if ([self waveFinished])  {
        // 波峰在一定范围之内进行轻微波动
    
        // 波峰该继续增大或减小
        if (self.increase) {
            self.variable += 0.01;
        }else{
            self.variable -= 0.01;
        }
    
        // 变化的范围
        if (self.variable <= 1){
            self.increase = YES;
        }
        if (self.variable >= 1.6) {
            self.increase = NO;
        }
    
        // 根据variable值来决定波峰
        self.waveAmplitude = self.variable * 5;
    }
    else
    {
        // 波浪高度未到指定高度 继续上涨
        [self amplitudeChanged];
        
        if (self.percent > 0.5) {
            self.currentWavePointY -= self.waveGrowth;
        }else{
            self.currentWavePointY += self.waveGrowth;
        }

        //圆球中分数增加
        CGFloat resultcount = self.percent > 0.5 ? self.lableScore + self.perScore : self.lableScore - self.perScore;
        
        self.lableScore = resultcount;
        
        self.scoreLable.text = [NSString stringWithFormat:@"%d",(int)resultcount];
    }
    
    self.offsetX += self.waveSpeed;

    [self setCurrentWaveLayerPath];
}

效果gif中有三条水波纹,是设计需求,为了减少篇幅此处只贴一条水波纹代码,三条的话错开相位φ即可达到效果。

结语

好久没更新博客了,工作中总能遇到各种各样的问题,各种突发奇想,不知所措。以及某一个问题的全部解决思路。

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

推荐阅读更多精彩内容