关于Core Animation的一些初步探索
2013-04-09 17:15 三戒1993 阅读(149) 评论(0) 编辑 收藏 举报
所谓Core Animation,顾名思义就是用来做动画的,它包含了一些Objective-C类,这些类都在Quartz Core框架中。
用它的原因也无需多说,首先是性能很好,使用了GPU硬件加速;其次是接口易用,毕竟是Objective-C,不需要像OpenGL ES一样完全和C打交道。
不过要掌握它也很费劲,这2天就遇到了不少问题,于是记录在此。
首先说下起因吧,其实我主要是想模拟UITableViewCell中,对imageView赋值时的动画效果。即从nil更改为一个UIImage对象后,图像的大小会从0逐渐放大,而右侧的textLabel和detailTextLabel也会右移。
这个默认的实现应该是用UIView做的,先调用UIView的beginAnimations:context:方法,然后修改imageView、textLabel和detailTextLabel的frame,再调用UIView的commitAnimations方法,就会自动使用动画来表现frame的更改了。
不过使用UIView也就意味着绘制性能的下降,造成滚动时不太流畅,于是我想到了轻量级的CALayer。
CALayer就是一个层。和绘图软件中的图层一样,CALayer也可以叠加,最终混合成一个可见的视图。
实际上UIView对象就有个layer属性,那就是它的根层,添加到这个层上面的CALayer对象就会随这个UIView一起绘制出来了。
CALayer *layer = [[CALayer alloc] init];
layer.frame = imageRect;
layer.contents = (id)image.CGImage;
cell.imageLayer = layer;
[cell.contentView.layer addSublayer:layer];
[layer release];
上述代码中,把layer.contents赋值为一个CGImageRef对象后,就会在这个layer中显示这张图像了。
层还可以实现一些特效,例如圆角和阴影:
layer.cornerRadius = 10.0;
layer.masksToBounds = YES;
这段代码就给层加上了10像素半径的圆角,不过你会发现绘制性能显著下降了,所以如果不是必须的话,可以先在CGContextRef中画出圆角图像,再绘制到layer或view中。不过原文中没有设置scale,在iPhone 4下效果很差,因此最后创建时要改成[UIImage imageWithCGImage:imageMasked scale:[[UIScreen mainScreen] scale] orientation:UIImageOrientationUp]。
而如果要显示文字的话,可以继承CALayer,然后改写drawInContext:方法;或者实现delegate的drawLayer:inContext方法。代码可见Add text to CALayer。
不过当时我用的是CATextLayer这个子类,用法也很简单:
cell.textLayer = [CATextLayer layer];
textLayer.frame = textRect;
textLayer.truncationMode = kCATruncationEnd;
textLayer.foregroundColor = black;
textLayer.backgroundColor = white;
textLayer.wrapped = YES;
textLayer.fontSize = 17;
textLayer.string = text;
[cell.layer addSublayer:textLayer];
然而用了以后,我感觉被Apple坑了…
首先是啥都没看到,查了下文档,发现默认的字体颜色是白色,于是手动设置为黑色:
textLayer.foregroundColor = [UIColor blackColor].CGColor;
接着是文字过长时,会直接切断文字,而不显示省略号,并且有些文字可能只显示一半。如果把textLayer.wrapped设为NO,确实可以让它以省略号的形式截断,但是就只能显示一行了…貌似无解,除非自行计算长度并截断。
然后是在iPhone 4上测试时,发现文字没有抗锯齿。搜索了一下,发现需要手动设置contentsScale:
textLayer.contentsScale = [[UIScreen mainScreen] scale];
还有在滚动时,因为table cell是重用的,更改文字会出现短暂的残像。最后发现更改contents属性也会产生动画,因此需要去掉这个动画效果:
NSDictionary *actions = [[NSDictionary alloc] initWithObjectsAndKeys:[NSNull null], @"contents", nil];
textLayer.actions = actions;
[newActions release];
此外,采用相同字体和字号的时候,CATextLayer里的文本会比直接用NSString的drawInRect:withFont:lineBreakMode:方法粗一些,原因不明。
另外,我还发现CALayer默认是透明的,改成不透明后就显示成黑色了,而且改背景色也没用,原因亦不明。
于是最终我去掉了textLayer,而是直接调用NSString的drawInRect:withFont:lineBreakMode:方法。由于动画时间很短,倒也不影响视觉效果。
创建CALayer搞定了,接下来就该实现动画了。和UIView一样,修改CALayer的属性就会自动生成动画了。因此初始时可以把imageLayer的bounds的长宽都设为0,然后赋值为一个正数,它就会自动放大了。
不过默认的动画时间很短,如果要修改的话,就需要用到CATransaction了:
[CATransaction begin];
[CATransaction setAnimationDuration:1];
imageLayer.frame = imageRect;
[CATransaction commit];
CATransaction还有其他的作用,最重要的功能就是让其中的所有动画在commit时并行执行,以使它们同步。这样如果同时放大图像并缩小文字的话,就不会出现图像比文字先变化,而覆盖文字的情况。
此外还可以用下述语句来禁用一些动画,不过并非万能:
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
当然也可以自定义动画,例如使用CABasicAnimation:
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"];
animation.duration = 1;
CGPoint point = {100, 0};
animation.byValue=[NSValue valueWithCGPoint:point];
layer.position = position;
[layer addAnimation:animation forKey:@"positionAnimation"];
这段代码就让layer右移了100 point。不过执行完后,你会发现它又立刻回到原处了。
实际上Core Animation为layer维护了3种tree:
- 第一种是我们可以直接访问的layer tree,对CALayer的属性赋值后,就会立刻更改。
- 第二种是presentation tree,它存储了动画进行过程中的属性。因此在整个动画过程中,它会不断变化。我们可以通过它来得知layer当前的显示状态。
- 第三种是render tree,它根据presentation tree来计算,我们不能访问它。
知道这个后就清楚原因了。上例中我们执行了一个动画,它的presentation tree一直在变化,直到动画结束。接着它发现layer tree和presentation tree不一致,于是又自动更新了layer的位置,所以就还原了。
因此在执行完动画后,立刻更改CALayer的属性,应该就不会自动还原了,不过我懒得去试了。
现在就来实现一下drawRect:方法:
- (void)drawRect:(CGRect)rect {
[CATransaction begin];
if (shouldAnimatied) {
[CATransaction setAnimationDuration:.3];
} else {
[CATransaction setAnimationDuration:0];
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
}
if (image) {
if (imageLayer.hidden) {
imageLayer.hidden = NO;
imageLayer.frame = imageRect;
}
imageLayer.contents = (id)image.CGImage;
self.image = nil;
[textString drawInRect:textRect withFont:font lineBreakMode:UILineBreakModeTailTruncation];
} else {
imageLayer.hidden = YES;
imageLayer.frame = imageRect0;
[textString drawInRect:textRect0 withFont:font lineBreakMode:UILineBreakModeTailTruncation];
}
[CATransaction commit];
shouldAnimatied = NO;
}
其中shouldAnimatied是在drawRect:前设置的,只有当前cell可见,并且图像下载完时,才需要用动画的方式显示出来。
当没有图像时,就直接显示文字,并将imageLayer隐藏,长宽设为0;而有图像时,就将其显示出来,长宽增大,而文本则画在右侧。
目前这个版本在图像全部载入过的情况下,滚动时能稳定在55fps以上,但显示新图像时会降低到低于30fps,并明显感到很卡。而直接调用[image drawAtPoint:imagePoint]的话,无论何时基本都能维持在55fps以上。
所以考虑了另一个方案,即平时隐藏imageLayer,通过调用drawAtPoint:来显示图像。只有在动画更新时才显示imageLayer。
- (void)drawRect:(CGRect)rect {
if (shouldAnimatied) {
[CATransaction begin];
[CATransaction setAnimationDuration:.3];
imageLayer.hidden = NO;
imageLayer.frame = imageRect;
imageLayer.contents = (id)image.CGImage;
[CATransaction commit];
self.image = nil;
[textString drawInRect:textRect withFont:font lineBreakMode:UILineBreakModeTailTruncation];
shouldAnimatied = NO;
} else {
if (!imageLayer.hidden) {
[CATransaction begin];
[CATransaction setAnimationDuration:0];
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
imageLayer.hidden = YES;
imageLayer.frame = imageRect0;
[CATransaction commit];
}
if (image) {
[image drawAtPoint:imagePoint];
self.image = nil;
[textString drawInRect:textRect withFont:font lineBreakMode:UILineBreakModeTailTruncation];
} else {
[textString drawInRect:textRect0 withFont:font lineBreakMode:UILineBreakModeTailTruncation];
}
}
}
现在这个版本也基本上稳定到55fps以上了,总算大功告成了~