iOS动画篇:自定义动画

作者: 明仔Su | 来源:发表于2016-05-17 16:05 被阅读3349次

    前言

    在上一篇文章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

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

    相关文章

      网友评论

      • 江海寄余生12138:=====>>>>0.647349
        2017-06-12 23:03:43.555556 a.b.art[2272:720210] drawInContext =====>>>>0.652507
        2017-06-12 23:03:43.578829 a.b.art[2272:720210] drawInContext =====>>>>0.664061
        2017-06-12 23:03:43.588927 a.b.art[2272:720210] drawInContext =====>>>>0.669193
        2017-06-12 23:03:43.612375 a.b.art[2272:720210] drawInContext =====>>>>0.680836
        2017-06-12 23:03:43.622575 a.b.art[2272:720210] drawInContext =====>>>>0.685925
        2017-06-12 23:03:43.645611 a.b.art[2272:720210] drawInContext =====>>>>0.697448
        2017-06-12 23:03:43.655595 a.b.art[2272:720210] drawInContext =====>>>>0.702528
        2017-06-12 23:03:43.678969 a.b.art[2272:720210] drawInContext =====>>>>0.714140
        2017-06-12 23:03:43.690113 a.b.art[2272:720210] drawInContext =====>>>>0.714555
        2017-06-12 23:03:43.708412 a.b.art[2272:720210] drawInContext =====>>>>0.714555
        2017-06-12 23:03:43.714725 a.b.art[2272:720210] drawInContext =====>>>>0.454222
        2017-06-12 23:03:43.717088 a.b.art[2272:720210] stop ==0.714556
      • 江海寄余生12138:2017-06-12 23:03:43.274974 a.b.art[2272:720210] drawInContext =====>>>>0.512235
        2017-06-12 23:03:43.288301 a.b.art[2272:720210] drawInContext =====>>>>0.518923
        2017-06-12 23:03:43.308914 a.b.art[2272:720210] drawInContext =====>>>>0.529218
        2017-06-12 23:03:43.321767 a.b.art[2272:720210] drawInContext =====>>>>0.535640
        2017-06-12 23:03:43.342891 a.b.art[2272:720210] drawInContext =====>>>>0.546203
        2017-06-12 23:03:43.355186 a.b.art[2272:720210] drawInContext =====>>>>0.552305
        2017-06-12 23:03:43.376204 a.b.art[2272:720210] drawInContext =====>>>>0.562860
        2017-06-12 23:03:43.388595 a.b.art[2272:720210] drawInContext =====>>>>0.569052
        2017-06-12 23:03:43.410050 a.b.art[2272:720210] drawInContext =====>>>>0.579650
        2017-06-12 23:03:43.422239 a.b.art[2272:720210] drawInContext =====>>>>0.585848
        2017-06-12 23:03:43.445548 a.b.art[2272:720210] drawInContext =====>>>>0.597506
        2017-06-12 23:03:43.455583 a.b.art[2272:720210] drawInContext =====>>>>0.602520
        2017-06-12 23:03:43.478492 a.b.art[2272:720210] drawInContext =====>>>>0.613944
        2017-06-12 23:03:43.489149 a.b.art[2272:720210] drawInContext =====>>>>0.619277
        2017-06-12 23:03:43.512386 a.b.art[2272:720210] drawInContext =====>>>>0.630904
        2017-06-12 23:03:43.522275 a.b.art[2272:720210] drawInContext =====>>>>0.635868
        2017-06-12 23:03:43.545309 a.b.art[2272:720210] drawInContext
      • 江海寄余生12138:用楼主的最后一个方法实现的进度条,出现了一个Bug,当动画完成时,绘制的圈会返回初始的地方,只是偶尔出现,下面是对drawInContext 的progress打印:
        2017-06-12 23:03:43.146329 a.b.art[2272:720210] progress cir======= 0.71
        2017-06-12 23:03:43.146705 a.b.art[2272:720210] previous = 4088,now = 6431
        2017-06-12 23:03:43.187449 a.b.art[2272:720210] drawInContext =====>>>>0.468485
        2017-06-12 23:03:43.192295 a.b.art[2272:720210] drawInContext =====>>>>0.470930
        2017-06-12 23:03:43.212216 a.b.art[2272:720210] drawInContext =====>>>>0.480857
        2017-06-12 23:03:43.215083 a.b.art[2272:720210] drawInContext =====>>>>0.482329
        2017-06-12 23:03:43.226346 a.b.art[2272:720210] drawInContext =====>>>>0.487940
        2017-06-12 23:03:43.247269 a.b.art[2272:720210] drawInContext =====>>>>0.498410
        2017-06-12 23:03:43.254835 a.b.art[2272:720210] drawInContext =====>>>>0.502194
      • 饭饭男:不错不错
      • 常义:思路很清晰,想问下你写的时候是用的什么工具编写的?
      • 川农鉴黄师:Poping三方动画不错,可以研究研究
      • 神的仆人iOS:没必要这么繁琐,通过 CAShapeLayer的strokeEnd和strokeStart就可以了。
      • chen文:赞,写的很好,问下博主怎么实现 数字UILabel由0变成70%这样的动画
        TommyYaphetS:@上海灬一人 你可以试试POP动画库,可以非常简单的实现这个效果,如果自己写的话,代码量比较大

      本文标题:iOS动画篇:自定义动画

      本文链接:https://www.haomeiwen.com/subject/bwyurttx.html