欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

iOS核心动画高级技巧-1

程序员文章站 2022-05-31 19:47:02
1. 图层树 图层的树状结构 巨妖有图层,洋葱也有图层,你有吗?我们都有图层 -- 史莱克 Core Animation其实是一个令人误解的命名。你可能认为它只是用来做动画的,但实际上它是从一个叫做Layer Kit这么一个不怎么和动画有关的名字演变而来,所以做动画这只是Core Animation ......

 

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的宿主图层中显示一张图片

 
iOS核心动画高级技巧-1
image

我们用这些简单的代码做了一件很有趣的事情:我们利用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灵活多了和boundsframe不同,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创建了一个合适尺寸的空寄宿图(尺寸由boundscontentsscale决定)和一个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来绘制图层内容。

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