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

接下來將更新常見動畫的解析及實現講解系列文章

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,521評論 25 708
  • 在iOS中隨處都可以看到絢麗的動畫效果,實現這些動畫的過程并不復雜,今天將帶大家一窺ios動畫全貌。在這里你可以看...
    每天刷兩次牙閱讀 8,572評論 6 30
  • 夏季,極端的很。要不烈日當空照,禪喘雷干。要不電閃雷鳴,暴雨嘩嘩,像天河決了口子。近日暴雨沖破了村莊上的水庫堤壩,...
    水漫漫漫思閱讀 541評論 0 1
  • 火炕竹席罐罐茶,大襟媳婦奶娃娃。 風吹敞院飄香氣,雨過空山跨彩霞。 牧犬沿坡追野兔,葫蘆上架瞅冬瓜。 小溪驚夢清音...
    詩人夏沐閱讀 621評論 10 5
  • 和劉吵了一架,原因是一直在說什么,你要嫁到我們家,應該多多了解我,有病吧!我還沒要求你了解我呢,你倒好事情這么多。...
    檸檬安然閱讀 112評論 0 0