iOS核心动画高级技巧-1
1. 图层树
图层的树状结构
巨妖有图层,洋葱也有图层,你有吗?我们都有图层 -- 史莱克
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的细节了呢?
某种意义上说的确是这样,对一些简单的需求来说,我们确实没必要处理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显示了结果。
一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的ios交流群:1012951431, 分享bat,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!希望帮助开发者少走弯路。
清单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提供的操作显示的一些特性。
2. 寄宿图
寄宿图
图片胜过千言万语,界面抵得上千图片 ——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来绘制图层内容。
在第三章,"图层几何学"中,我们将会探讨一下图层的几何,观察他们是如何放置和改变相互的尺寸的
上一篇: 如何增强前端代码的健壮性
下一篇: 实在不知道怎么写