前言
在上一篇文章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 *)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 *)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動畫來滿足需求。
應用場景:下載進度的變化、數字變化的效果
文/明仔Su(簡書作者)
原文鏈接:http://www.jianshu.com/p/673585164bd2