CALayer的needsDisplayForKey方法使用说明

来源:互联网 发布:卖家如何查看淘宝客 编辑:程序博客网 时间:2024/06/04 19:14

今天为大家介绍的是如何在执行动画的时候,完成Core Graphics 图形的绘制工作。 主要把重心放在needsDisplayForKey 方法上面。先看看实现绘制的动画效果图。


一、理论基础

首先了解下layer自己的属性如何实现动画。

1. layer首次加载时会调用 +(BOOL)needsDisplayForKey:(NSString *)key方法来判断当前指定的属性key改变是否需要重新绘制。

2. Core Animartion中的key或者keypath等于+(BOOL)needsDisplayForKey:(NSString *)key 方法中指定的key便会自动调用setNeedsDisplay方法,这样就会触发重绘,达到我们想要的效果。


layer方法响应链有两种:

1. [layer setNeedDisplay] -> [layer displayIfNeed] -> [layer display] -> [layerDelegate displayLayer:]

2. [layer setNeedDisplay] -> [layer displayIfNeed] -> [layer display] -> [layer drawInContext:] -> [layerDelegate drawLayer: inContext:]

说明一下,如果layerDelegate实现了displayLayer:协议,之后layer就不会再调用自身的重绘代码。

这里使用第二种方式来实现圆形进度条,将代码集成到layer中,降低耦合。


二、代码实现

1. 自定义一个类CircleLayer,其继承自CALayer。

.h 文件代码如下:

@interface CircleLayer : CALayer@end
在 .m文件中定义一个progress属性,可以通过对这个属性的监听来完成绘制操作。
@interface CircleLayer()@property (nonatomic, assign) CGFloat progress;@end

下面所述的代码均是.m文件中的代码 。


2. 重写父类CALayer的 needsDisplayForKey方法,也就是 "理论基础" 的第一点。
+ (BOOL)needsDisplayForKey:(NSString *)key {    BOOL result;    if ([key isEqualToString:@"progress"]) {        result = YES;    } else {        result = [super needsDisplayForKey:key];    }    return result;}
需要说明的有以下几点:

2.1 此方法只会在图层初始化的时候被调用一次。

2.2 代码中通过判断图层的属性名称来决定是否需要对对应的Core Animation动画执行UI重绘工作(本例中就是对自定义的progress属性进行处理)。

2.3 [super needsDisplayForKey:key]; 这个父类方法默认的返回值是NO。


3. 自定义动画,完成绘制工作。

首先在.h文件中定义执行动画的方法

- (void)animateCircle;
在.m文件中实现这个方法
- (void)animateCircle {    CAKeyframeAnimation *anim = [CAKeyframeAnimation animationWithKeyPath:@"progress"];    anim.values = [self valuesListWithAnimationDuration: 3];    anim.duration = 3.0;    anim.fillMode = kCAFillModeForwards;    anim.removedOnCompletion = NO;    anim.delegate = self;    [self addAnimation:anim forKey:@"circle"];}- (NSMutableArray *)valuesListWithAnimationDuration:(CGFloat)duration {    NSInteger numberOfFrames = duration * 60;    NSMutableArray *values = [NSMutableArray array];    // 注意这里的 fromValue和toValue是针对的progress的值的大小。    CGFloat fromValue = 0.0;    CGFloat toValue = 1.0;    CGFloat diff = toValue - fromValue;    for (NSInteger frame = 1; frame <= numberOfFrames; frame++) {        CGFloat piece = (CGFloat)frame / (CGFloat)numberOfFrames;        CGFloat currentValue = fromValue + diff * piece;        [values addObject:@(currentValue)];    }    return values;}
3.1 animationWithKeyPath 中指定的属性是progress,这里就是 "理论基础" 中说明的第二点。

因为在needsDisplayForKey方法中指定了key的值是progress,所以这里的animationWithKeyPath动画操作会在动画执行期间,不停的促发Core Graphics的重绘工作,即不停的调用 - (void)drawInContext:(CGContextRef)ctx方法进行绘制。

3.2 - (NSMutableArray *)valuesListWithAnimationDuration:(CGFloat)duration 方法完成绘制点的 "收集" 工作。由于默认情况下,Core Graphics 绘制的帧率为一秒钟60次,所以可以根据绘制时间计算出绘制帧数: numberOfFrame = duration * 60。然后在fromValue 和 toValue之间根据帧率取点,依次放入到一个数组中。

3.3 fillMode 和 removeOnCompletion 两个属性指定动画在绘制完成后,对应的动画对象不会从内存中移除掉。如果对这两个属性有什么不了解的地方,请参照:Core Animation 基本动画效果汇总。


4. 绘制图形。

- (void)drawInContext:(CGContextRef)ctx {    NSLog(@"progress: %f", self.progress);    CGContextSetLineWidth(ctx, 5.0f);    CGContextSetStrokeColorWithColor(ctx, [UIColor blackColor].CGColor);    CGContextAddArc(ctx, CGRectGetWidth(self.bounds) * 0.5, CGRectGetHeight(self.bounds) * 0.5, CGRectGetWidth(self.bounds) * 0.5 - 6, 0, 2 * M_PI * self.progress, 0);    CGContextStrokePath(ctx);}
这里就是简单的绘制一个圆形,注意到绘制的终止点是 2 * M_PI * self.progress, 因为在第三点中的动画处理中已经指定了绘制的KeyPath为progress,并且指定了动画的values数组中的每一个值是0~1之间的浮点值,所以self.progress在绘制过程中会对应的从0递增到1。这样就实现了在动画执行过程中,完成图形的绘制。


5. 在第四点中,我特意加了一句self.progress 值的打印,可以发现,在绘制过程中,self.progress的值的确是递增打印的。动画执行完毕后,self.progress的值是1.000, 但是动画执行完毕后,会一直打印,而且不会停止下来!!! 那是因为在绘制过程中,设置了fillMode为kCAFillModeForwards 和 removeOnCompletion为NO,即动画执行完毕后,动画对象依旧驻留在内存中,所以会一直打印,而且会一直调用的 -(void)drawInContext:(CGContextRef)ctx这个方法不停的进行绘制,虽然最后会不停的绘制self.progress为1的时候的那个点,但是会非常的消耗性能,导致内存直线上升!!! 所以在动画执行完毕后,必须移除掉动画对象。因此需要实现动画执行完毕后的代理方法:

- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {    [self removeAnimationForKey:@"circle"];    self.progress = 1.0;    [self setNeedsDisplay];}
5.1    动画执行完毕后,通过removeAnimationForKey 移除掉动画对象。

5.2 设置self.progress为1.0, 准备绘制。

5.3 通过调用[self setNeedsDisplay]立刻绘制圆形。

需要注意的是:
第三点中的绘制,是通过CAKeyframeAnimation动画逐点依次绘制出来的; 

而这里的[self setNeedsDisplay]是一次性绘制出来的。

即实现思想是,先通过CAKeyframeAnimation动画从开始"慢慢"的绘制到最后。当动画执行完毕,也就是一个圆正好绘制完毕的时候,立刻移除掉动画对象,然后通过[self setNeedsDisplay]一次性绘制出来。移除动画对象的瞬间,其实绘制出来的圆会消失的,但随即调用[self setNeedsDisplay]绘制圆。由于这两步之间的时间极短,所以感觉还是完整的绘制了一个圆。


6. 主控制中调用代码:

- (void)viewDidLoad {    [super viewDidLoad];        // 实现方式一: 直接添加操作图层处理    self.layer = [[CircleLayer alloc] init];    self.layer.frame = CGRectMake(50, 100, 100, 100);    self.layer.backgroundColor = [UIColor redColor].CGColor;    [self.view.layer addSublayer:self.layer];        UITapGestureRecognizer* tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(move:)];    [self.view addGestureRecognizer:tap];}- (void)move:(UIGestureRecognizer*)tap{    [self.layer animateCircle];}

0 0
原创粉丝点击