iOS動(dòng)畫篇:自定義動(dòng)畫

前言

在上一篇文章iOS動(dòng)畫篇:自定義View中講到了如何在view里畫一個(gè)圓,本文將在此基礎(chǔ)上給其加上弧度變化的動(dòng)畫,形成一個(gè)簡(jiǎn)單的Loading動(dòng)畫,呈現(xiàn)自定義動(dòng)畫的實(shí)現(xiàn)過(guò)程。

先來(lái)看看需要實(shí)現(xiàn)的Loading動(dòng)畫效果:

CustomAnimation - preview.gif

條條大路通羅馬:在UIView上實(shí)現(xiàn)

1、在自定義View時(shí)所提到的路徑方法只能畫整圓,現(xiàn)在我們使用下面的方法來(lái)畫一部分圓?。?/p>

 - (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];
 }

效果:半個(gè)圓弧

Circle - half.png

2、弧度總不能寫死吧,弧度得有變化才能形成動(dòng)畫效果。怎樣控制它變化呢,我們給它加上一個(gè)progress屬性來(lái)控制其弧度

@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、通過(guò)外部事件來(lái)改變它的弧度,并讓其重繪(這里的例子時(shí)當(dāng)點(diǎn)擊屏幕的時(shí)候改變其弧度屬性)

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

效果圖:

CustomAnimation - setNeedsDisplay.gif
小結(jié):
1)drawRect方法會(huì)執(zhí)行view的重繪,但是drawRect方法不能手動(dòng)調(diào)用(手動(dòng)調(diào)用了也無(wú)效),必須通過(guò)調(diào)用setNeedsDisplay讓系統(tǒng)自動(dòng)調(diào)該方法。
2)實(shí)現(xiàn)自定義動(dòng)畫可以通過(guò):O —>通過(guò)屬性控制view的形狀 —> 改變view的屬性 —> 調(diào)用重繪方法 —> view的形狀改變 —> O

下面我們創(chuàng)建slider來(lái)模擬進(jìn)度變化

    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

更優(yōu)雅的實(shí)現(xiàn)方式:在CALayer上實(shí)現(xiàn)

通過(guò)重載View的drawRect來(lái)實(shí)現(xiàn)自定義動(dòng)畫縱然可以,但是不夠優(yōu)雅(逼格),而且實(shí)現(xiàn)更復(fù)雜的界面時(shí)也顯得不夠方便,下面我們使用添加Layer的方式來(lái)實(shí)現(xiàn)。

1、新建CircleProgressLayer類

CircleProgressView.h
CircleProgressView.m

2、給其添加progress屬性

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

3、重載其繪圖方法 drawInContext,并在progress屬性變化時(shí)讓其重繪

- (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屬性變化時(shí)通知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;
}

這樣做可以達(dá)到跟上面例子一樣的效果,那么為什么推薦使用這種方式呢?

答案是:CALayer自帶動(dòng)畫效果(或者說(shuō)自帶自動(dòng)形成動(dòng)畫幀的天賦)

1)直接在View中繪圖可以形成動(dòng)畫效果,但前提是其變化幅度要求非常小,否則看起來(lái)就是一段一段的很生硬,比如上面的例子中,progress從0.2變化到0.5的時(shí)候,并沒(méi)有動(dòng)畫效果。
  2)對(duì)比起來(lái)在CALayer中繪圖可以使用CA動(dòng)畫讓其自定義的屬性變化也有動(dòng)畫效果,其原理是:給Layer的屬性提供初值、終值和動(dòng)畫時(shí)間,CA會(huì)自動(dòng)計(jì)算中間值,并生產(chǎn)關(guān)鍵幀,在非主線程中播放關(guān)鍵幀,這樣就形成了動(dòng)畫效果。

下面我們給創(chuàng)建的Layer添加動(dòng)畫效果:
1、新建CircleProgressLayer類

CircleProgressLayer.h
CircleProgressLayer.m

2、給其添加progress屬性

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

3、重載其繪圖方法 drawInContext,并在progress屬性變化時(shí)讓其重繪

- (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屬性變化時(shí)進(jìn)行重繪

+ (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中,當(dāng)progress屬性變化時(shí),給對(duì)應(yīng)layer增加CA動(dòng)畫,并在動(dòng)畫結(jié)束時(shí)刷新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、添加到視圖中,通過(guò)外部事件改變其進(jìn)度(這里的測(cè)試?yán)邮钱?dāng)點(diǎn)擊屏幕時(shí)隨機(jī)增加進(jìn)度)

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

效果圖:

CustomAnimation - layerAni.gif
小結(jié):
1)needsDisplayForKey方法:CA動(dòng)畫生成需要指定對(duì)Layer的哪一個(gè)屬性進(jìn)行插值,Layer默認(rèn)有許多帶有動(dòng)畫效果的屬性,如postion,backgroundColor等等,我們自定義的屬性需要手動(dòng)指定。
2)initWithLayer方法:CA生成關(guān)鍵幀是通過(guò)拷貝CALayer進(jìn)行的,在拷貝時(shí),只能拷貝原有的(系統(tǒng)的,非自定義的)屬性,不能拷貝自定義的屬性或持有的對(duì)象等等,因此需要重載initWithLayer來(lái)手動(dòng)拷貝我們需要拷貝的東西。

·

蛋糕出爐加奶油:UIView和CALayer的結(jié)合

進(jìn)度條動(dòng)畫已經(jīng)具備了動(dòng)畫,再加上進(jìn)度的顯示,就完成了自定義的圓形進(jìn)度條。

這里的進(jìn)度使用了UILabel來(lái)展示,當(dāng)可以滿足需求的時(shí)候完全可以結(jié)合UIView來(lái)實(shí)現(xiàn),當(dāng)然如果有讀者追求完美動(dòng)畫效果(例如進(jìn)度數(shù)字的變化動(dòng)畫),可以繼續(xù)思考如何實(shí)現(xiàn),并完善之。

效果圖:

CustomAnimation - preview.gif

本文例子的demo可以到我的GitHub點(diǎn)擊我飛過(guò)去下載。

總結(jié)

至此,我們基本了解了自定義View動(dòng)畫的實(shí)現(xiàn)流程,大家可以根據(jù)不同情形選擇其實(shí)現(xiàn)方式:

1)變化幅度小,變化速度快的情景,選用setNeedsDisplay進(jìn)行重繪就可以滿足需求。

應(yīng)用場(chǎng)景:進(jìn)度條的拖動(dòng)、下拉刷新的動(dòng)畫、等等

2)變化幅度大、變化速度慢的情景,選用給屬性添加CA動(dòng)畫來(lái)滿足需求。

應(yīng)用場(chǎng)景:下載進(jìn)度的變化、數(shù)字變化的效果

next

接下來(lái)將更新常見(jiàn)動(dòng)畫的解析及實(shí)現(xiàn)講解系列文章

最后編輯于
?著作權(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),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,094評(píng)論 25 709
  • 在iOS中隨處都可以看到絢麗的動(dòng)畫效果,實(shí)現(xiàn)這些動(dòng)畫的過(guò)程并不復(fù)雜,今天將帶大家一窺ios動(dòng)畫全貌。在這里你可以看...
    每天刷兩次牙閱讀 8,694評(píng)論 6 30
  • 火炕竹席罐罐茶,大襟媳婦奶娃娃。 風(fēng)吹敞院飄香氣,雨過(guò)空山跨彩霞。 牧犬沿坡追野兔,葫蘆上架瞅冬瓜。 小溪驚夢(mèng)清音...
    詩(shī)人夏沐閱讀 687評(píng)論 10 5
  • 和劉吵了一架,原因是一直在說(shuō)什么,你要嫁到我們家,應(yīng)該多多了解我,有病吧!我還沒(méi)要求你了解我呢,你倒好事情這么多。...
    檸檬安然閱讀 128評(píng)論 0 0
  • 去年,和朋友一起乘著移動(dòng)互聯(lián)之風(fēng),也開通了一個(gè)公眾號(hào),主要針對(duì)自己所在行業(yè),提供一些便捷信息查詢服務(wù),間歇性的寫一...
    spring2000閱讀 402評(píng)論 0 5

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