一、隐式动画
隐式是因为我们并没有指定任何动画的类型,我们仅仅改变了一个属性,然后Core Animation来决定如何并且何时去做动画。
但当你改变一个属性,Core Animation是如何判断动画类型和持续时间的呢?实际上动画执行的时间取决于当前事务的设置,动画类型取决于图层行为。
事务实际上是Core Animation用来包含一系列属性动画集合的机制,任何用指定事务去改变可以做动画的图层属性都不会立刻发生变化,而是当事务一旦提交的时候开始用一个动画过渡到新值。
事务是通过CATransaction类来做管理,这个类的设计有些奇怪,不像你从它的命名预期的那样去管理一个简单的事务,而是管理了一叠你不能访问的事务。CATransaction没有属性或者实例方法,并且也不能用+alloc和-init方法创建它。但是可以用+begin和+commit分别来入栈或者出栈。
任何可以做动画的图层属性都会被添加到栈顶的事务,你可以通过+setAnimationDuration:方法设置当前事务的动画时间,或者通过+animationDuration方法来获取值(默认0.25秒)。
Core Animation在每个runloop周期中自动开始一次新的事务,即使你不显式的用[CATransaction begin]开始一次事务,任何在一次runloop循环中属性的改变都会被击中起来,然后做一次0.25秒的动画。明白了这些之后,我们就可以轻松修改变色动画的时间了。我们当然可以用当前事务的+setAnimationDuration:方法来修改动画时间。
- (IBAction)changeColor
{
//begin a new transaction
[CATransaction begin];
//set the animation duration to 1 second
[CATransaction setAnimationDuration:1.0];
//randomize the layer background color
CGFloat red = arc4random() / (CGFloat)INT_MAX;
CGFloat green = arc4random() / (CGFloat)INT_MAX;
CGFloat blue = arc4random() / (CGFloat)INT_MAX;
self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;
//commit the transaction
[CATransaction commit];
}
基于UIView的block的动画允许你在动画结束的时候提供一个完成的动作。CATranscation接口提供的+setCompletionBlock:方法也有同样的功能。
- (IBAction)changeColor
{
//begin a new transaction
[CATransaction begin];
//set the animation duration to 1 second
[CATransaction setAnimationDuration:1.0];
//add the spin animation on completion
[CATransaction setCompletionBlock:^{
//rotate the layer 90 degrees
CGAffineTransform transform = self.colorLayer.affineTransform;
transform = CGAffineTransformRotate(transform, M_PI_2);
self.colorLayer.affineTransform = transform;
}];
//randomize the layer background color
CGFloat red = arc4random() / (CGFloat)INT_MAX;
CGFloat green = arc4random() / (CGFloat)INT_MAX;
CGFloat blue = arc4random() / (CGFloat)INT_MAX;
self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;
//commit the transaction
[CATransaction commit];
}
Core Animation通常对CALayer的所有属性做动画,但是UIView把它关联的图层的这个特性关闭了。
我们把改变属性时CALayer自动应用的动画称作行为,当CALayer的属性被修改时,它会调用-actionForKey:方法,传递属性的名称。剩下的操作在CALayer的头文件中有详细的说明,实质上是如下几步:
-
图层首先检测它是否有委托,并且是否实现CALayerDelegate协议指定的-actionForLayer:forKey方法。如果有,直接调用并返回结果。
-
如果没有委托,或者委托没有实现-actionForLayer:forKey方法,图层接着检查包含属性名称对应行为映射的actions字典。
-
如果actions字典没有包含对应的属性,那么图层接着在它的style字典接着搜索属性名。
-
最后,如果在style里面也找不到对应的行为,那么图层将会直接调用定义了每个属性的标准行为的-defaultActionForKey:方法
所以一轮完整的搜索结束之后,-actionForKey:要么返回空(这种情况将不会有动画发生),要么是CAAction协议对应的对象,最后CALayer拿这个结果去对先前和当前的值做动画。
于是这就解释了UIKit是如何禁用隐式动画的:每个UIView对它关联的图层都扮演了一个委托,并且提供了-actionForLayer:forKey的实现方法。当不在一个动画块的实现中,UIView对所有图层行为返回nil,但是在动画block范围之内,它就返回一个非空值。
返回nil并不是禁用隐式动画唯一的办法,
[CATransaction setDisableActions:YES];
我们来对颜色渐变的例子使用一个不同的行为,通过给colorLayer设置一个自定义的actions字典。
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *layerView;
@property (nonatomic, weak) IBOutlet CALayer *colorLayer;/*热心人发现这里应该改为@property (nonatomic, strong) CALayer *colorLayer;否则运行结果不正确。
*/
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//create sublayer
self.colorLayer = [CALayer layer];
self.colorLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f);
self.colorLayer.backgroundColor = [UIColor blueColor].CGColor;
//add a custom action
CATransition *transition = [CATransition animation];
transition.type = kCATransitionPush;
transition.subtype = kCATransitionFromLeft;
self.colorLayer.actions = @{@"backgroundColor": transition};
//add it to our view
[self.layerView.layer addSublayer:self.colorLayer];
}
- (IBAction)changeColor
{
//randomize the layer background color
CGFloat red = arc4random() / (CGFloat)INT_MAX;
CGFloat green = arc4random() / (CGFloat)INT_MAX;
CGFloat blue = arc4random() / (CGFloat)INT_MAX;
self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;
}
@end
CALayer是一个连接用户界面(就是MVC中的view)虚构的类,但是在界面本身这个场景下,CALayer的行为更像是存储了视图如何显示和动画的数据模型。
每个图层属性的显示值都被存储在一个叫做呈现图层的独立图层当中,它可以通过-presentationLayer方法来访问。这个呈现图层的属性值代表了在任何指定时刻当前外观效果。
呈现树通过图层树中所有图层的呈现图层所形成。注意呈现图层仅仅当图层首次被提交(就是首次第一次在屏幕上显示)的时候创建,所以在那之前调用-presentationLayer将会返回nil。
你可能注意到有一个叫做–modelLayer的方法。在呈现图层上调用–modelLayer将会返回它正在呈现所依赖的CALayer。通常在一个图层上调用-modelLayer会返回–self(实际上我们已经创建的原始图层就是一种数据模型)。
使用presentationLayer图层来判断当前图层位置
@interface ViewController ()
@property (nonatomic, strong) CALayer *colorLayer;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//create a red layer
self.colorLayer = [CALayer layer];
self.colorLayer.frame = CGRectMake(0, 0, 100, 100);
self.colorLayer.position = CGPointMake(self.view.bounds.size.width / 2, self.view.bounds.size.height / 2);
self.colorLayer.backgroundColor = [UIColor redColor].CGColor;
[self.view.layer addSublayer:self.colorLayer];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
//get the touch point
CGPoint point = [[touches anyObject] locationInView:self.view];
//check if we've tapped the moving layer
if ([self.colorLayer.presentationLayer hitTest:point]) {
//randomize the layer background color
CGFloat red = arc4random() / (CGFloat)INT_MAX;
CGFloat green = arc4random() / (CGFloat)INT_MAX;
CGFloat blue = arc4random() / (CGFloat)INT_MAX;
self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;
} else {
//otherwise (slowly) move the layer to new position
[CATransaction begin];
[CATransaction setAnimationDuration:4.0];
self.colorLayer.position = point;
[CATransaction commit];
}
}
@end
二、显式动画
https://www.jianshu.com/p/79278ec6950c
我们证实了过渡是一种对那些不太好做平滑动画属性的强大工具,但是CATransition的提供的动画类型太少了。
更奇怪的是苹果通过UIView +transitionFromView:toView:duration:options:completion:和+transitionWithView:duration:options:animations:方法提供了Core Animation的过渡特性。但是这里的可用的过渡选项和CATransition的type属性提供的常量完全不同。UIView过渡方法中options参数可以由如下常量指定:
UIViewAnimationOptionTransitionFlipFromLeft
UIViewAnimationOptionTransitionFlipFromRight UIViewAnimationOptionTransitionCurlUp UIViewAnimationOptionTransitionCurlDown UIViewAnimationOptionTransitionCrossDissolve UIViewAnimationOptionTransitionFlipFromTop UIViewAnimationOptionTransitionFlipFromBottom
使用UIKit提供的方法来做过渡动画
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIImageView *imageView;
@property (nonatomic, copy) NSArray *images;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad]; //set up images
self.images = @[[UIImage imageNamed:@"Anchor.png"],
[UIImage imageNamed:@"Cone.png"],
[UIImage imageNamed:@"Igloo.png"],
[UIImage imageNamed:@"Spaceship.png"]];
- (IBAction)switchImage
{
[UIView transitionWithView:self.imageView duration:1.0
options:UIViewAnimationOptionTransitionFlipFromLeft
animations:^{
//cycle to next image
UIImage *currentImage = self.imageView.image;
NSUInteger index = [self.images indexOfObject:currentImage];
index = (index + 1) % [self.images count];
self.imageView.image = self.images[index];
}
completion:NULL];
}
@end
文档暗示过在iOS5(带来了Core Image框架)之后,可以通过CATransition的filter属性,用CIFilter来创建其它的过渡效果。然是直到iOS6都做不到这点。试图对CATransition使用Core Image的滤镜完全没效果(但是在Mac OS中是可行的,也许文档是想表达这个意思)。
因此,根据要实现的效果,你只用关心是用CATransition还是用UIView的过渡方法就可以了。希望下个版本的iOS系统可以通过CATransition很好的支持Core Image的过渡滤镜效果(或许甚至会有新的方法)。
但这并不意味着在iOS上就不能实现自定义的过渡效果了。这只是意味着你需要做一些额外的工作。就像之前提到的那样,过渡动画做基础的原则就是对原始的图层外观截图,然后添加一段动画,平滑过渡到图层改变之后那个截图的效果。如果我们知道如何对图层截图,我们就可以使用属性动画来代替CATransition或者是UIKit的过渡方法来实现动画。
事实证明,对图层做截图还是很简单的。CALayer有一个-renderInContext:方法,可以通过把它绘制到Core Graphics的上下文中捕获当前内容的图片,然后在另外的视图中显示出来。如果我们把这个截屏视图置于原始视图之上,就可以遮住真实视图的所有变化,于是重新创建了一个简单的过渡效果。
renderInContext:创建自定义过渡效果
我们对当前视图状态截图,然后在我们改变原始视图的背景色的时候对截图快速转动并且淡出,图8.5展示了我们自定义的过渡效果。
为了让事情更简单,我们用UIView -animateWithDuration:completion:方法来实现。虽然用CABasicAnimation可以达到同样的效果,但是那样的话我们就需要对图层的变换和不透明属性创建单独的动画,然后当动画结束的是哦户在CAAnimationDelegate中把coverView从屏幕中移除。
@implementation ViewController
- (IBAction)performTransition
{
//preserve the current view snapshot
UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, YES, 0.0);
[self.view.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *coverImage = UIGraphicsGetImageFromCurrentImageContext();
//insert snapshot view in front of this one
UIView *coverView = [[UIImageView alloc] initWithImage:coverImage];
coverView.frame = self.view.bounds;
[self.view addSubview:coverView];
//update the view (we'll simply randomize the layer background color)
CGFloat red = arc4random() / (CGFloat)INT_MAX;
CGFloat green = arc4random() / (CGFloat)INT_MAX;
CGFloat blue = arc4random() / (CGFloat)INT_MAX;
self.view.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0];
//perform animation (anything you like)
[UIView animateWithDuration:1.0 animations:^{
//scale, rotate and fade the view
CGAffineTransform transform = CGAffineTransformMakeScale(0.01, 0.01);
transform = CGAffineTransformRotate(transform, M_PI_2);
coverView.transform = transform;
coverView.alpha = 0.0;
} completion:^(BOOL finished) {
//remove the cover view now we're finished with it
[coverView removeFromSuperview];
}];
}
@end
三、核心动画性能调优
1.CPU所做的工作都在软件层面,而GPU在硬件层面。
总的来说,我们可以用软件(使用CPU做任何事情),但是对于图像处理,通常用硬件会更快,因为GPU使用图像对高度并行浮点运算做了优化。
2.动画和屏幕上组合的图层实际上被一个单独的进程管理,而不是你的应用程序。这个进程就是所谓的渲染服务。在iOS5和之前的版本是SpringBoard进程(同时管理着iOS的主屏)。在iOS之后的版本中叫做BackBoard。
当运行一段动画时候,这个过程会被四个分离的阶段被打破:
- 布局。这是准备你的视图/图层的层级关系,以及设置图层属性(位置、背景色、边框等等)的阶段。
- 显示。这是图层的寄宿图片被绘制的阶段。绘制有可能涉及你的-drawRect:和-drawLayer:inContext:方法的调用。
- 准备。这是Core Animation准备发送动画数据到渲染服务的阶段。这同时也是Core Animation将要执行一些别的事务例如解码动画过程中将要显示的图片的时间点。
- 提交。这是最后的阶段,Core Animation打包所有图层和动画属性,然后通过IPC(内部处理通信)发送到渲染服务进行显示。
一旦打包的图层和动画到达渲染服务进程,他们会被反序列化来形成另一个叫做渲染树的图层数。使用这个树状结构,渲染服务队动画的每一帧做出如下工作: - 对所有的图层属性计算中间值,设置OpenGL几何形状(纹理化的三角形)来进行渲染。
- 在屏幕上渲染可见的三角形。
最后两个阶段在动画中不停的重复。前五个阶段都在软件层面处理,只有最后一个被GPU执行。而且,你真正只能控制前两个阶段。
四、脏矩形
有时候用 CAShapeLayer 或者其他矢量图形图层替代Core Graphics并不是那么 切实可行。比如我们的绘图应用:我们用线条完美地完成了矢量绘制。但是设想一 下如果我们能进一步提高应用的性能,让它就像一个黑板一样工作,然后用『粉 笔』来绘制线条。模拟粉笔最简单的方法就是用一个『线刷』图片然后将它粘贴到 用户手指碰触的地方,但是这个方法用 CAShapeLayer没办法实现。
我们可以给每个『线刷』创建一个独立的图层,但是实现起来有很大的问题。屏 幕上允许同时出现图层上线数量大约是几百,那样我们很快就会超出的。这种情况 下我们没什么办法,就用Core Graphics吧(除非你想用OpenGL做一些更复杂的事 情)。
我们的『黑板』应用的最初实现如上面代码,我们更改了之前版本的DrawingView ,用一个画刷位置的数组代替 UIBezierPath。
#import "DrawingView.h"
#define BRUSH_SIZE 32
@interface DrawingView()
@property (nonatomic, strong) NSMutableArray *strokes;
@end
@implementation DrawingView
- (void)awakeFromNib{
[super awakeFromNib];
self.strokes = [NSMutableArray array];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
CGPoint point = [[touches anyObject] locationInView: self];
[self addBrushStrokeAtPoint:point];
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
CGPoint point = [[touches anyObject] locationInView: self];
[self addBrushStrokeAtPoint:point];
}
- (void)addBrushStrokeAtPoint:(CGPoint)point{
[self.strokes addObject:[NSValue valueWithCGPoint:point]];
[self setNeedsDisplay];
}
- (void)drawRect:(CGRect)rect {
for (NSValue *value in self.strokes) {
CGPoint point = [value CGPointValue];
CGRect brushRect = CGRectMake(point.x / BRUSH_SIZE, point.y / BRUSH_SIZE, BRUSH_SIZE - 10, BUFSIZ - 10);
[[UIImage imageNamed:@"ball"] drawInRect:brushRect];
}
}
@end
这个实现在模拟器上表现还不错,但是在真实设备上就没那么好了。问题在于每 次手指移动的时候我们就会重绘之前的线刷,即使场景的大部分并没有改变。我们 绘制地越多,就会越慢。随着时间的增加每次重绘需要更多的时间,帧数也会下降 ,如何提高性能呢?
为了减少不必要的绘制,Mac OS和iOS设备将会把屏幕区分为需要重绘的区域和 不需要重绘的区域。那些需要重绘的部分被称作『脏区域』。在实际应用中,鉴于 非矩形区域边界裁剪和混合的复杂性,通常会区分出包含指定视图的矩形位置,而 这个位置就是『脏矩形』。
当一个视图被改动过了,TA可能需要重绘。但是很多情况下,只是这个视图的一 部分被改变了,所以重绘整个寄宿图就太浪费了。但是Core Animation通常并不了 解你的自定义绘图代码,它也不能自己计算出脏区域的位置。然而,你的确可以提供这些信息。
当你检测到指定视图或图层的指定部分需要被重绘,你直接调用 - setNeedsDisplayInRect:来标记它,然后将影响到的矩形作为参数传入。这样就 会在一次视图刷新时调用视图的-drawRect: (或图层代理的- drawLayer:inContext:方法)。
传入-drawLayer:inContext:的 CGContext参数会自动被裁切以适应对应的 矩形。为了确定矩形的尺寸大小,你可以用CGContextGetClipBoundlingBox() 方法来从上下文获得大小。调用 - drawRect()会更简单,因为CGRect会作为参数直接传入。
你应该将你的绘制工作限制在这个矩形中。任何在此区域之外的绘制都将被自动 无视,但是这样CPU花在计算和抛弃上的时间就浪费了,实在是太不值得了。
相比依赖于Core Graphics为你重绘,裁剪出自己的绘制区域可能会让你避免不 必要的操作。那就是说,如果你的裁剪逻辑相当复杂,那还是让Core Graphics来代劳吧,记住:当你能高效完成的时候才这样做。
下面代码:展示了一个-addBrushStrokeAtPoint:方法的升级版,它只重绘当前线刷的附近区域。另外也会刷新之前线刷的附近区域,我们也可以用 CGRectIntersectsRect() 来避免重绘任何旧的线刷以不至于覆盖已更新过的区域。这样做会显著地提高绘制效率
#import "DrawingView.h"
#define BRUSH_SIZE 32
@interface DrawingView()
@property (nonatomic, strong) NSMutableArray *strokes;
@end
@implementation DrawingView
- (void)awakeFromNib{
[super awakeFromNib];
self.strokes = [NSMutableArray array];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
CGPoint point = [[touches anyObject] locationInView: self];
[self addBrushStrokeAtPoint:point];
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
CGPoint point = [[touches anyObject] locationInView: self];
[self addBrushStrokeAtPoint:point];
}
- (void)addBrushStrokeAtPoint:(CGPoint)point{
[self.strokes addObject:[NSValue valueWithCGPoint:point]];
[self setNeedsDisplayInRect:[self burshRectForPoint:point]];
}
- (CGRect)burshRectForPoint:(CGPoint)point{
return CGRectMake(point.x / BRUSH_SIZE / 2, point.y / BRUSH_SIZE / 2, BUFSIZ, BUFSIZ);
}
- (void)drawRect:(CGRect)rect {
for (NSValue *value in self.strokes) {
CGPoint point = [value CGPointValue];
CGRect brushRect = [self burshRectForPoint:point];
if (CGRectIntersectsRect(rect, brushRect)) {
[[UIImage imageNamed:@"ball"] drawInRect:brushRect];
}
}
}
@end
五、异步绘制
UIKit的单线程天性意味着寄宿图通畅要在主线程上更新,这意味着绘制会打断用 户交互,甚至让整个app看起来处于无响应状态。我们对此无能为力,但是如果能 避免用户等待绘制完成就好多了。
针对这个问题,有一些方法可以用到:一些情况下,我们可以推测性地提前在另外一个线程上绘制内容,然后将由此绘出的图片直接设置为图层的内容。这实现起 来可能不是很方便,但是在特定情况下是可行的。Core Animation提供了一些选 择: CATiledLayer和 drawsAsynchronously属性。
CATiledLayer
我们在第六章简单探索了一下CATiledLayer. 除了将图层再次分割成独立更新的小块(类似于脏矩形自动更新的概念),CATiledlayer还有一个有趣的特性:在多个线程中为每个小块同时调用- drawLayer: inContext:方法. 这就避免了阻塞用户交互而且能够利用多核心芯片来更快地绘制。只有一个小块 的 CATiledLayer是实现异步更新图片视图的简单方法。
drawsAsynchronously
iOS 6中,苹果为CALayer引入了这个令人好奇的属性,drawsAsynchronously 属性对传入-drawLayer:inContext: 的 CGContext进行改动,允许CGContext延缓绘制命令的执行以至于不阻塞用户交互。
它与CATiledLayer使用的异步绘制并不相同。它自己的- drawLayer: inContext:方法只会在主线程调用,但是CGContext并不等待每个绘制命令的结束。相反地,它会将命令加入队列,当方法返回时,在后台线程逐个执行真正的绘制。
根据苹果的说法。这个特性在需要频繁重绘的视图上效果最好(比如我们的绘图 应用,或者诸如 UITableViewCell 之类的),对那些只绘制一次或很少重绘的图 层内容来说没什么太大的帮助。
网友评论