本人录制技术视频地址:https://edu.csdn.net/lecturer/1899 欢迎观看。
今天为大家介绍的是如何在执行动画的时候,完成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];
}