今天为大家介绍的是如何在执行动画的时候,完成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文件中的代码 。
+ (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]; }