关于ZAKER 融媒体解决方案 合作 加入

iOS 核心动画高级技巧 -1

CocoaChina 11-19

图层的树状结构

巨妖有图层,洋葱也有图层,你有吗?我们都有图层 -- 史莱克

Core Animation 其实是一个令人误解的命名。你可能认为它只是用来做动画的,但实际上它是从一个叫做 Layer Kit 这么一个不怎么和动画有关的名字演变而来,所以做动画这只是 Core Animation 特性的冰山一角。

Core Animation 是一个复合引擎,它的职责就是尽可能快地组合屏幕上不同的可视内容,这个内容是被分解成独立的图层,存储在一个叫做图层树的体系之中。于是这个树形成了 UIKit 以及在 iOS 应用程序当中你所能在屏幕上看见的一切的基础。

在我们讨论动画之前,我们将从图层树开始,涉及一下 Core Animation 的静态组合以及布局特性。

1.1 图层与视图

图层与视图

如果你曾经在 iOS 或者 Mac OS 平台上写过应用程序,你可能会对视图的概念比较熟悉。一个视图就是在屏幕上显示的一个矩形块(比如图片,文字或者视频),它能够拦截类似于鼠标点击或者触摸手势等用户输入。视图在层级关系中可以互相嵌套,一个视图可以管理它的所有子视图的位置。图 1.1 显示了一种典型的视图层级关系

1.2 图层的能力

图层的能力

如果说 CALayer 是 UIView 内部实现细节,那我们为什么要全面地了解它呢?苹果当然为我们提供了优美简洁的 UIView 接口,那么我们是否就没必要直接去处理 Core Animation 的细节了呢?

一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的 iOS 交流群:1012951431, 分享 BAT, 阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!希望帮助开发者少走弯路。

某种意义上说的确是这样,对一些简单的需求来说,我们确实没必要处理 CALayer,因为苹果已经通过 UIView 的高级 API 间接地使得动画变得很简单。

但是这种简单会不可避免地带来一些灵活上的缺陷。如果你略微想在底层做一些改变,或者使用一些苹果没有在 UIView 上实现的接口功能,这时除了介入 Core Animation 底层之外别无选择。

我们已经证实了图层不能像视图那样处理触摸事件,那么他能做哪些视图不能做的呢?这里有一些 UIView 没有暴露出来的 CALayer 的功能:

阴影,圆角,带颜色的边框

3D 变换

非矩形范围

透明遮罩

多级非线性动画

我们将会在后续章节中探索这些功能,首先我们要关注一下在应用程序当中 CALayer 是怎样被利用起来的。

1.3 使用图层

使用图层

首先我们来创建一个简单的项目,来操纵一些 layer 的属性。打开 Xcode,使用 Single View Application 模板创建一个工程。

在屏幕中央创建一个小视图(大约 200 X 200 的尺寸),当然你可以手工编码,或者使用 Interface Builder(随你方便)。确保你的视图控制器要添加一个视图的属性以便可以直接访问它。我们把它称作 layerView。

运行项目,应该能在浅灰色屏幕背景中看见一个白色方块,如果没看见,可能需要调整一下背景 window 或者 view 的颜色

之后就可以在代码中直接引用 CALayer 的属性和方法。在清单 1.1 中,我们用创建了一个 CALayer,设置了它的 backgroundColor 属性,然后添加到 layerView 背后相关图层的子图层(这段代码的前提是通过 IB 创建了 layerView 并做好了连接),图 1.5 显示了结果。

清单 1.1 给视图添加一个蓝色子图层

#import "ViewController.h"#import @interface ViewController ( ) @property ( nonatomic, weak ) IBOutlet UIView *layerView;@end@implementation ViewController- ( void ) viewDidLoad{ [ super viewDidLoad ] ; //create sublayer CALayer *blueLayer = [ CALayer layer ] ; blueLayer.frame = CGRectMake ( 50.0f, 50.0f, 100.0f, 100.0f ) ; blueLayer.backgroundColor = [ UIColor blueColor ] .CGColor; //add it to our view [ self.layerView.layer addSublayer:blueLayer ] ;}@end

1.4 总结

总结

这一章阐述了图层的树状结构,说明了如何在 iOS 中由 UIView 的层级关系形成的一种平行的 CALayer 层级关系,在后面的实验中,我们创建了自己的 CALayer,并把它添加到图层树中。

在第二章," 图层关联的图片 ",我们将要研究一下 CALayer 关联的图片,以及 Core Animation 提供的操作显示的一些特性。

寄宿图

图片胜过千言万语,界面抵得上千图片 —— Ben Shneiderman

我们在第一章『图层树』中介绍了 CALayer 类并创建了一个简单的有蓝色背景的图层。背景颜色还好啦,但是如果它仅仅是展现了一个单调的颜色未免也太无聊了。事实上 CALayer 类能够包含一张你喜欢的图片,这一章节我们将来探索 CALayer 的寄宿图(即图层中包含的图)。

2.1 contents 属性

contents 属性

CALayer 有一个属性叫做 contents,这个属性的类型被定义为 id,意味着它可以是任何类型的对象。在这种情况下,你可以给 contents 属性赋任何值,你的 app 仍然能够编译通过。但是,在实践中,如果你给 contents 赋的不是 CGImage,那么你得到的图层将是空白的。

contents 这个奇怪的表现是由 Mac OS 的历史原因造成的。它之所以被定义为 id 类型,是因为在 Mac OS 系统上,这个属性对 CGImage 和 NSImage 类型的值都起作用。如果你试图在 iOS 平台上将 UIImage 的值赋给它,只能得到一个空白的图层。一些初识 Core Animation 的 iOS 开发者可能会对这个感到困惑。

头疼的不仅仅是我们刚才提到的这个问题。事实上,你真正要赋值的类型应该是 CGImageRef,它是一个指向 CGImage 结构的指针。UIImage 有一个 CGImage 属性,它返回一个 "CGImageRef", 如果你想把这个值直接赋值给 CALayer 的 contents,那你将会得到一个编译错误。因为 CGImageRef 并不是一个真正的 Cocoa 对象,而是一个 Core Foundation 类型。

尽管 Core Foundation 类型跟 Cocoa 对象在运行时貌似很像(被称作 toll-free bridging),他们并不是类型兼容的,不过你可以通过 bridged 关键字转换。如果要给图层的寄宿图赋值,你可以按照以下这个方法:

layer.contents = ( __bridge id ) image.CGImage;

如果你没有使用 ARC(自动引用计数),你就不需要 __bridge 这部分。但是,你干嘛不用 ARC?!

让我们来继续修改我们在第一章新建的工程,以便能够展示一张图片而不仅仅是一个背景色。我们已经用代码的方式建立一个图层,那我们就不需要额外的图层了。那么我们就直接把 layerView 的宿主图层的 contents 属性设置成图片。

清单 2.1 更新后的代码。

@implementation ViewController- ( void ) viewDidLoad{ [ super viewDidLoad ] ; //load an image UIImage *image = [ UIImage imageNamed:@"Snowman.png" ] ; //add it directly to our view's layer self.layerView.layer.contents = ( __bridge id ) image.CGImage;}@end

图表 2.1 在 UIView 的宿主图层中显示一张图片

我们用这些简单的代码做了一件很有趣的事情:我们利用 CALayer 在一个普通的 UIView 中显示了一张图片。这不是一个 UIImageView,它不是我们通常用来展示图片的方法。通过直接操作图层,我们使用了一些新的函数,使得 UIView 更加有趣了。

contentGravity

你可能已经注意到了我们的雪人看起来有点。。。胖 ==! 我们加载的图片并不刚好是一个方的,为了适应这个视图,它有一点点被拉伸了。在使用 UIImageView 的时候遇到过同样的问题,解决方法就是把 contentMode 属性设置成更合适的值,像这样:

view.contentMode = UIViewContentModeScaleAspectFit;

这个方法基本和我们遇到的情况的解决方法已经接近了(你可以试一下 : ) ),不过 UIView 大多数视觉相关的属性比如 contentMode,对这些属性的操作其实是对对应图层的操作。

CALayer 与 contentMode 对应的属性叫做 contentsGravity,但是它是一个 NSString 类型,而不是像对应的 UIKit 部分,那里面的值是枚举。contentsGravity 可选的常量值有以下一些:

kCAGravityCenter

kCAGravityTop

kCAGravityBottom

kCAGravityLeft

kCAGravityRight

kCAGravityTopLeft

kCAGravityTopRight

kCAGravityBottomLeft

kCAGravityBottomRight

kCAGravityResize

kCAGravityResizeAspect

kCAGravityResizeAspectFill

和 cotentMode 一样,contentsGravity 的目的是为了决定内容在图层的边界中怎么对齐,我们将使用 kCAGravityResizeAspect,它的效果等同于 UIViewContentModeScaleAspectFit, 同时它还能在图层中等比例拉伸以适应图层的边界。

self.layerView.layer.contentsGravity = kCAGravityResizeAspect;

图 2.2 可以看到结果

图 2.3 用错误的 contentsScale 属性显示 Retina 图片

如你所见,我们的雪人不仅有点大还有点像素的颗粒感。那是因为和 UIImage 不同,CGImage 没有拉伸的概念。当我们使用 UIImage 类去读取我们的雪人图片的时候,他读取了高质量的 Retina 版本的图片。但是当我们用 CGImage 来设置我们的图层的内容时,拉伸这个因素在转换的时候就丢失了。不过我们可以通过手动设置 contentsScale 来修复这个问题(如 2.2 清单),图 2.4 是结果

@implementation ViewController- ( void ) viewDidLoad{ [ super viewDidLoad ] ; //load an image UIImage *image = [ UIImage imageNamed:@"Snowman.png" ] ; //add it directly to our view's layer self.layerView.layer.contents = ( __bridge id ) image.CGImage; //center the image self.layerView.layer.contentsGravity = kCAGravityCenter; //set the contentsScale to match image self.layerView.layer.contentsScale = image.scale;}@end

图 2.5 使用 masksToBounds 来修建图层内容

contentsRect

CALayer 的 contentsRect 属性允许我们在图层边框里显示寄宿图的一个子域。这涉及到图片是如何显示和拉伸的,所以要比 contentsGravity 灵活多了和 bounds,frame 不同,contentsRect 不是按点来计算的,它使用了单位坐标,单位坐标指定在 0 到 1 之间,是一个相对值(像素和点就是绝对值)。所以他们是相对与寄宿图的尺寸的。iOS 使用了以下的坐标系统:

点 —— 在 iOS 和 Mac OS 中最常见的坐标体系。点就像是虚拟的像素,也被称作逻辑像素。在标准设备上,一个点就是一个像素,但是在 Retina 设备上,一个点等于 2*2 个像素。iOS 用点作为屏幕的坐标测算体系就是为了在 Retina 设备和普通设备上能有一致的视觉效果。

像素 —— 物理像素坐标并不会用来屏幕布局,但是仍然与图片有相对关系。UIImage 是一个屏幕分辨率解决方案,所以指定点来度量大小。但是一些底层的图片表示如 CGImage 就会使用像素,所以你要清楚在 Retina 设备和普通设备上,他们表现出来了不同的大小。

单位 —— 对于与图片大小或是图层边界相关的显示,单位坐标是一个方便的度量方式, 当大小改变的时候,也不需要再次调整。单位坐标在 OpenGL 这种纹理坐标系统中用得很多,Core Animation 中也用到了单位坐标。

默认的 contentsRect 是 {0, 0, 1, 1},这意味着整个寄宿图默认都是可见的,如果我们指定一个小一点的矩形,图片就会被裁剪(如图 2.6)

2.2 Custom Drawing

Custom Drawing

给 contents 赋 CGImage 的值不是唯一的设置寄宿图的方法。我们也可以直接用 Core Graphics 直接绘制寄宿图。能够通过继承 UIView 并实现 -drawRect: 方法来自定义绘制。

-drawRect: 方法没有默认的实现,因为对 UIView 来说,寄宿图并不是必须的,它不在意那到底是单调的颜色还是有一个图片的实例。如果 UIView 检测到 -drawRect: 方法被调用了,它就会为视图分配一个寄宿图,这个寄宿图的像素尺寸等于视图大小乘以 contentsScale 的值。

如果你不需要寄宿图,那就不要创建这个方法了,这会造成 CPU 资源和内存的浪费,这也是为什么苹果建议:如果没有自定义绘制的任务就不要在子类中写一个空的 -drawRect: 方法。

当视图在屏幕上出现的时候 -drawRect: 方法就会被自动调用。-drawRect: 方法里面的代码利用 Core Graphics 去绘制一个寄宿图,然后内容就会被缓存起来直到它需要被更新(通常是因为开发者调用了 -setNeedsDisplay 方法,尽管影响到表现效果的性值被更改时,一些视图类型会被自动重绘,如 bounds 属性)。虽然 -drawRect: 方法是一个 UIView 方法,事实上都是底层的 CALayer 安排了重绘工作和保存了因此产生的图片。

CALayer 有一个可选的 delegate 属性,实现了 CALayerDelegate 协议,当 CALayer 需要一个内容特定的信息时,就会从协议中请求。CALayerDelegate 是一个非正式协议,其实就是说没有 CALayerDelegate @protocol 可以让你在类里面引用啦。你只需要调用你想调用的方法,CALayer 会帮你做剩下的。(delegate 属性被声明为 id 类型,所有的代理方法都是可选的)。

当需要被重绘时,CALayer 会请求它的代理给他一个寄宿图来显示。它通过调用下面这个方法做到的 :

( void ) displayLayer: ( CALayerCALayer * ) layer;

趁着这个机会,如果代理想直接设置 contents 属性的话,它就可以这么做,不然没有别的方法可以调用了。如果代理不实现 -displayLayer: 方法,CALayer 就会转而尝试调用下面这个方法:

- ( void ) drawLayer: ( CALayer * ) layer inContext: ( CGContextRef ) ctx;

在调用这个方法之前,CALayer 创建了一个合适尺寸的空寄宿图(尺寸由 bounds 和 contentsScale 决定)和一个 Core Graphics 的绘制上下文环境,为绘制寄宿图做准备,他作为 ctx 参数传入。

让我们来继续第一章的项目让它实现 CALayerDelegate 并做一些绘图工作吧(见清单 2.5). 图 2.12 是他的结果

清单 2.5 实现 CALayerDelegate

@implementation ViewController- ( void ) viewDidLoad{ [ super viewDidLoad ] ; //create sublayer CALayer *blueLayer = [ CALayer layer ] ; blueLayer.frame = CGRectMake ( 50.0f, 50.0f, 100.0f, 100.0f ) ; blueLayer.backgroundColor = [ UIColor blueColor ] .CGColor; //set controller as layer delegate blueLayer.delegate = self; //ensure that layer backing image uses correct scale blueLayer.contentsScale = [ UIScreen mainScreen ] .scale; //add layer to our view [ self.layerView.layer addSublayer:blueLayer ] ; //force layer to redraw [ blueLayer display ] ;}- ( void ) drawLayer: ( CALayer * ) layer inContext: ( CGContextRef ) ctx{ //draw a thick red circle CGContextSetLineWidth ( ctx, 10.0f ) ; CGContextSetStrokeColorWithColor ( ctx, [ UIColor redColor ] .CGColor ) ; CGContextStrokeEllipseInRect ( ctx, layer.bounds ) ;}@end

2.3 总结

总结

本章介绍了寄宿图和一些相关的属性。你学到了如何显示和放置图片, 使用拼合技术来显示, 以及用 CALayerDelegate 和 Core Graphics 来绘制图层内容。

在第三章," 图层几何学 " 中,我们将会探讨一下图层的几何,观察他们是如何放置和改变相互的尺寸的

以上内容由"CocoaChina"上传发布 查看原文

觉得文章不错,微信扫描分享好友

扫码分享