iOS动画篇:自定义动画

前言

在上一篇文章iOS动画篇:自定义View中讲到了如何在view里画一个圆,本文将在此基础上给其加上弧度变化的动画,形成一个简单的Loading动画,呈现自定义动画的实现过程。

先来看看需要实现的Loading动画效果:

CustomAnimation - preview.gif

条条大路通罗马:在UIView上实现

1、在自定义View时所提到的路径方法只能画整圆,现在我们使用下面的方法来画一部分圆弧:

 - (void)drawRect:(CGRect)rect { CGFloat radius = self.bounds.size.width / 2; CGFloat lineWidth = 10.0;
   UIBezierPath * path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(radius, radius) radius:radius - lineWidth / 2 startAngle:0.f endAngle:M_PI clockwise:YES];
   [[UIColor colorWithRed:0.5 green:0.5 blue:0.9 alpha:1.0] setStroke]; 
   [path setLineWidth:lineWidth]; 
   [path stroke];
 }

效果:半个圆弧

Circle - half.png

2、弧度总不能写死吧,弧度得有变化才能形成动画效果。怎样控制它变化呢,我们给它加上一个progress属性来控制其弧度

@interface CircleProgressView : UIView
@property (nonatomic, assign) CGFloat progress;
@end
- (void)drawRect:(CGRect)rect {    
    CGFloat radius = self.bounds.size.width / 2;    
    CGFloat lineWidth = 10.0;    
    UIBezierPath * path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(radius, radius) radius:radius - lineWidth / 2 startAngle:0.f endAngle:M_PI * 2 * self.progress clockwise:YES];    
    [[UIColor colorWithRed:0.5 green:0.5 blue:0.9 alpha:1.0] setStroke];    
    [path setLineWidth:lineWidth];   
    [path stroke];
}

3、加到视图上

- (void)viewDidLoad {    
    [super viewDidLoad];      
    self.circleProgressView = [[CircleProgressView alloc]initWithFrame:CGRectMake(100, 100, 200, 200)];    
    self.circleProgressView.progress = 0.2;
    [self.view addSubview:self.circleProgressView];
}

4、通过外部事件来改变它的弧度,并让其重绘(这里的例子时当点击屏幕的时候改变其弧度属性)

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {    
    self.circleProgressView.progress = 0.5;    
    [self.circleProgressView setNeedsDisplay];
}

效果图:

CustomAnimation - setNeedsDisplay.gif
小结:
1)drawRect方法会执行view的重绘,但是drawRect方法不能手动调用(手动调用了也无效),必须通过调用setNeedsDisplay让系统自动调该方法。
2)实现自定义动画可以通过:O —>通过属性控制view的形状 —> 改变view的属性 —> 调用重绘方法 —> view的形状改变 —> O

下面我们创建slider来模拟进度变化

    UISlider * slider = [[UISlider alloc]initWithFrame:CGRectMake(50, 400, 275, 10)];    [slider addTarget:self action:@selector(changeProgress:) forControlEvents:UIControlEventValueChanged];    slider.maximumValue = 1.0;    slider.minimumValue = 0.f;    slider.value = self.circleProgressView.progress;
    [self.view addSubview:slider];
- (void)changeProgress:(UISlider *)slider {    self.circleProgressView.progress = slider.value;      
    [self.circleProgressView setNeedsDisplay];
}

效果图:

CustomAnimation - setNeedsDisplay - play.gif

更优雅的实现方式:在CALayer上实现

通过重载View的drawRect来实现自定义动画纵然可以,但是不够优雅(逼格),而且实现更复杂的界面时也显得不够方便,下面我们使用添加Layer的方式来实现。

1、新建CircleProgressLayer类

CircleProgressView.h
CircleProgressView.m

2、给其添加progress属性

@interface CircleProgressLayer : CALayer
@property (nonatomic, assign) CGFloat progress;
@end

3、重载其绘图方法 drawInContext,并在progress属性变化时让其重绘

- (void)drawInContext:(CGContextRef)ctx {    
    CGFloat radius = self.bounds.size.width / 2;    
    CGFloat lineWidth = 10.0;    
    UIBezierPath * path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(radius, radius) radius:radius - lineWidth / 2 startAngle:0.f endAngle:M_PI * 2 * self.progress clockwise:YES];    
    CGContextSetRGBStrokeColor(ctx, 0.5, 0.5, 0.9, 1.0);//笔颜色    
    CGContextSetLineWidth(ctx, 10);//线条宽度    
    CGContextAddPath(ctx, path.CGPath);    
    CGContextStrokePath(ctx);
}
- (void)setProgress:(CGFloat)progress {   
     _progress = progress;    
    [self setNeedsDisplay];
}

4、将layer添加到自定义的view中,并在progress属性变化时通知layer

- (id)initWithFrame:(CGRect)frame {    
    self = [super initWithFrame:frame];    
    if (self) {        
        self.circleProgressLayer = [CircleProgressLayer layer];        
        self.circleProgressLayer.frame = self.bounds;        //像素大小比例        
        self.circleProgressLayer.contentsScale = [UIScreen mainScreen].scale;        
        [self.layer addSublayer:self.circleProgressLayer];    
    }    
    return self;
}
- (void)setProgress:(CGFloat)progress {    
    self.circleProgressLayer.progress = progress;    
    _progress = progress;
}

这样做可以达到跟上面例子一样的效果,那么为什么推荐使用这种方式呢?

答案是:CALayer自带动画效果(或者说自带自动形成动画帧的天赋)

1)直接在View中绘图可以形成动画效果,但前提是其变化幅度要求非常小,否则看起来就是一段一段的很生硬,比如上面的例子中,progress从0.2变化到0.5的时候,并没有动画效果。
  2)对比起来在CALayer中绘图可以使用CA动画让其自定义的属性变化也有动画效果,其原理是:给Layer的属性提供初值、终值和动画时间,CA会自动计算中间值,并生产关键帧,在非主线程中播放关键帧,这样就形成了动画效果。

下面我们给创建的Layer添加动画效果:
1、新建CircleProgressLayer类

CircleProgressLayer.h
CircleProgressLayer.m

2、给其添加progress属性

@interface CircleProgressLayer : CALayer
@property (nonatomic, assign) CGFloat progress;
@end

3、重载其绘图方法 drawInContext,并在progress属性变化时让其重绘

- (void)drawInContext:(CGContextRef)ctx {    
    CGFloat radius = self.bounds.size.width / 2;    
    CGFloat lineWidth = 10.0;    
    UIBezierPath * path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(radius, radius) radius:radius - lineWidth / 2 startAngle:0.f endAngle:M_PI * 2 * self.progress clockwise:YES];    
    CGContextSetRGBStrokeColor(ctx, 0.5, 0.5, 0.9, 1.0);//笔颜色    
    CGContextSetLineWidth(ctx, 10);//线条宽度    
    CGContextAddPath(ctx, path.CGPath);    
    CGContextStrokePath(ctx);
}

4、重载 needsDisplayForKey方法指定progress属性变化时进行重绘

+ (BOOL)needsDisplayForKey:(NSString *)key {    
    if ([key isEqualToString:@"progress"]) {        
        return YES;    
    }    
    return [super needsDisplayForKey:key];
}

5、重载initWithLayer方法

- (instancetype)initWithLayer:(CircleProgressLayer *)layer {    
    NSLog(@"initLayer");    
    if (self = [super initWithLayer:layer]) {        
        self.progress = layer.progress;    
    }    
    return self;
}

6、在View中,当progress属性变化时,给对应layer增加CA动画,并在动画结束时刷新layer的progress属性

- (id)initWithFrame:(CGRect)frame {    
    self = [super initWithFrame:frame];    
    if (self) {        
        self.circleProgressLayer = [CircleProgressLayer layer];        
        self.circleProgressLayer.frame = self.bounds;        //像素大小比例        
        self.circleProgressLayer.contentsScale = [UIScreen mainScreen].scale;        
        [self.layer addSublayer:self.circleProgressLayer];    
    }    
    return self;
}
- (void)setProgress:(CGFloat)progress {    
    CABasicAnimation * ani = [CABasicAnimation animationWithKeyPath:@"progress"];    
    ani.duration = 5.0 * fabs(progress - _progress);    
    ani.toValue = @(progress);    
    ani.removedOnCompletion = YES;    
    ani.fillMode = kCAFillModeForwards;    
    ani.delegate = self;    
    [self.circleProgressLayer addAnimation:ani forKey:@"progressAni"];    
    _progress = progress;
}
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {    
    self.circleProgressLayer.progress = self.progress;
}

7、添加到视图中,通过外部事件改变其进度(这里的测试例子是当点击屏幕时随机增加进度)

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {    
    self.circleProgressView.progress += (arc4random() % 4 + 1) * 0.1;
}

效果图:

CustomAnimation - layerAni.gif
小结:
1)needsDisplayForKey方法:CA动画生成需要指定对Layer的哪一个属性进行插值,Layer默认有许多带有动画效果的属性,如postion,backgroundColor等等,我们自定义的属性需要手动指定。
2)initWithLayer方法:CA生成关键帧是通过拷贝CALayer进行的,在拷贝时,只能拷贝原有的(系统的,非自定义的)属性,不能拷贝自定义的属性或持有的对象等等,因此需要重载initWithLayer来手动拷贝我们需要拷贝的东西。

·

蛋糕出炉加奶油:UIView和CALayer的结合

进度条动画已经具备了动画,再加上进度的显示,就完成了自定义的圆形进度条。

这里的进度使用了UILabel来展示,当可以满足需求的时候完全可以结合UIView来实现,当然如果有读者追求完美动画效果(例如进度数字的变化动画),可以继续思考如何实现,并完善之。

效果图:

CustomAnimation - preview.gif

本文例子的demo可以到我的GitHub点击我飞过去下载。

总结

至此,我们基本了解了自定义View动画的实现流程,大家可以根据不同情形选择其实现方式:

1)变化幅度小,变化速度快的情景,选用setNeedsDisplay进行重绘就可以满足需求。

应用场景:进度条的拖动、下拉刷新的动画、等等

2)变化幅度大、变化速度慢的情景,选用给属性添加CA动画来满足需求。

应用场景:下载进度的变化、数字变化的效果

next

接下来将更新常见动画的解析及实现讲解系列文章

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,569评论 25 707
  • 在iOS中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥ios动画全貌。在这里你可以看...
    每天刷两次牙阅读 8,321评论 6 30
  • 夏季,极端的很。要不烈日当空照,禅喘雷干。要不电闪雷鸣,暴雨哗哗,像天河决了口子。近日暴雨冲破了村庄上的水库堤坝,...
    水漫漫漫思阅读 489评论 0 1
  • 火炕竹席罐罐茶,大襟媳妇奶娃娃。 风吹敞院飘香气,雨过空山跨彩霞。 牧犬沿坡追野兔,葫芦上架瞅冬瓜。 小溪惊梦清音...
    诗人夏沐阅读 564评论 10 5
  • 和刘吵了一架,原因是一直在说什么,你要嫁到我们家,应该多多了解我,有病吧!我还没要求你了解我呢,你倒好事情这么多。...
    柠檬安然阅读 91评论 0 0