iOS 动画技巧 (一)

1,591 阅读7分钟

欢迎大家关注我的公众号,我会定期分享一些我在项目中遇到问题的解决办法和一些iOS实用的技巧,现阶段主要是整理出一些基础的知识记录下来

文章也会同步更新到我的博客:
ppsheep.com

关于动画,在iOS中我们见的太多了,基本上现在每个APP或多或少都会加上一些动画,在这个动画系列中,我并不会实现很多很炫的动画,很炫的动画,现在开源的已经有很多了,成熟的也已经有很多,在这里,我主要是讲一些对于动画的理解,对于动画的由来以及实现原理等等。

动画的由来

在iOS中所有的视图都是从UIView的基类派生而来,UIView可以处理触摸事件,可以支持基于Core Graphics绘图,也能够做一些简单的动画,比如旋转、缩放或者其他一些滑动渐变的动画。但是实际上,这是苹果为我们封装了一层,真正实现动画的其实是一个叫做图层的玩意儿(CALayer),UIView所做的一切动画,都是苹果从CALayer封装而来。

CALayer

CALayer类在概念上和UIView类似,也可以像View一样添加一些子layer(图片,文本等等),也能够像View一样,管理子图层的位置大小等等,并且,CALayer有一些非常重要的属性和方法,iOS中的动画就是通过这些来做动画和变换,CALayer和UIView最大的不同就是CALayer不处理用户的交互。

UIView和CALayer的关系

每一个UIView都有一个CALayer实例属性,UIView的职责就是创建并创建这个图层,以确保在子视图被创建时,子图层也能够被创建,子视图被添加和移除的时候,子图层也能够做相对的添加移除操作。 他们的关系是一一对应的。

实际上,我们在屏幕上看到的视图或者动画,其实都是图层。UIView只是苹果为我们封装的高级API

这个就有个历史原因了,主要呢 是要在Mac上也使用CALayer,但是iOS设备的触摸和Mac的鼠标点击又不一样,在Mac上,高级API就叫做NSView了,更多了,就不在这里讲了。

哪里能用到CALayer

一般的,我们在处理一些简单的动画时,都用不到CALayer,既然苹果为我们封装好了,干嘛不用呢?但是如果需要再处理一些高级的动画,那UIView可能就不能满足我们的需求了。

没有暴露出来的CALayer功能:

  • 阴影,圆角,带颜色的边框
  • 3D变换
  • 非矩形范围
  • 透明遮罩
  • 多级非线性动画

使用图层

我们先来感受一下图层

新建一个工程

在view中添加一个view

UIView *layerView = [[UIView alloc] initWithFrame:CGRectMake(10, 10, 200, 200)];
layerView.backgroundColor = [UIColor redColor];
layerView.center = self.view.center;
[self.view addSubview:layerView];

然后我们想要在这个小红方框的中间添加上一个蓝色的小方框。当然,我们肯定知道这很简单,直接加上一个子view就行了,但是这样做的,就失去了我们学习CALayer的意义。

我的想法是这样,既然layer像view一样,那我们是否可以在layerView的layer上加上一个蓝色方框样式的layer 我们的做法是这样

CALayer *blueLayer = [CALayer layer];
blueLayer.frame = CGRectMake(50, 50, 100, 100);
blueLayer.backgroundColor = [UIColor blueColor].CGColor;
[layerView.layer addSublayer:blueLayer];

效果就出来了 我们来看看3D图

我们可以看到,这样的效果,我们只能看到一个图层,一个view 并没有向layerView上添加子view

当然这里我们只是做一个layer的介绍,并不是说你之后添加视图这样添加,这样肯定是错误的,我们之前讲过,layer不能处理用户的交互,这个很重要。

但是在什么情况下,我们需要这样来使用CALayer呢?

  • 开发同时可以在macOS上运行的跨平台应用
  • 使用多种CALayer,并不想创建额外的UIView去封装他们(这个后面会讲到)
  • 做一些对性能要求较高的工作,但是遇到这种情况,我们很多时候都直接使用OpenGL绘图了

总的来说呢,处理视图肯定比处理图层简单多了

我们这里创建这一个例子,只是为了来介绍,图层的树状结构,和视图的一一关联关系。

寄宿图

寄宿图是什么意思呢?其实呢,就是图层中包含图

contents属性

在CALayer中有一个属性,叫做contents,这个属性的类型被定义为id,看上去好像这个属性能够接收任意类型的值,如果给contents赋予了任意一个类型的值,你的APP也能够编译成功,但是得到的图层确实一个空白的图层,事实上,这个contents在iOS下,是需要一个CGImage的值。

那为什么这个又要写作一个id类型呢,这个又是一个历史原因了 ,明显的这个是因为macOS的原因,因为在macOS下,这个是接收NSImage类型。

在UIImage中 有一个CGImage属性,他返回的是一个CGImageRef(指向CGImage的指针),如果你直接把这个赋给contents,那是会编译出错的。CGImageRef是一个Core Foundation对象,并不是一个cocoa对象,但是我们可以通过bridged来进行转换,我们来向刚刚创建的layerView的图层赋予一张图片

UIImage *image = [UIImage imageNamed:@"plane"];
layerView.layer.contents = (__bridge id _Nullable)(image.CGImage);

这样我们就避开了UIImageView,直接向UIView的图层设置一张图片

contentsGravity属性

但是我们看到中间的图片明显被拉伸了,我们想要展示他原有的效果,怎么做呢?

在我们使用UIImageView时,我们处理这种拉伸,一般是使用UIimageView的一个属性

imageView.contentModel = UIViewContentModeScaleAspectFit;

在CALayer中有一个和contentMode相似的属性,叫做contentsGravity,不同的是他是一个NSString类型。

contentsGravity值的类型包含:

  • kCAGravityCenter
  • kCAGravityTop
  • kCAGravityBottom
  • kCAGravityLeft
  • kCAGravityRight
  • kCAGravityTopLeft
  • kCAGravityTopRight
  • kCAGravityBottomLeft
  • kCAGravityBottomRight
  • kCAGravityResize
  • kCAGravityResizeAspect
  • kCAGravityResizeAspectFill

这其中的类型与contentMode都有一一对应关系的,其中kCAGravityResizeAspect相当于视图中contentMode类型的UIViewContentModeScaleAspectFit

当我们将layerView的contentsGravity设置成kCAGravityResizeAspect

layerView.layer.contentsGravity = kCAGravityResizeAspect;

效果就不一样了

contentsScale属性(主要针对Mac)

contentsScale定义了寄宿图的像素尺寸和视图大小的比例,默认情况下是一个值为1.0的浮点数。那么我们一般怎么使用这个属性呢?

这个属性其实是为了支持高分辨率屏幕机制而出现的,他用来判断在绘制图层的时候,应该为寄宿图创建创建的空间大小和需要显示图片的拉伸度。

简单来说,如果我们将contentsScale设置为1.0 那么寄宿图创建出来的图片将会以每个点一个像素来绘制图片,如果设置为2,那么将会以每个点2个像素来绘制图片。这个就是我们熟知的retina屏幕。

在我们之前使用contentsGravity = kCAGravityResizeAspect这个属性时,默认是将图片等比例拉伸至适应图层大小,但是,如果我们将contentsGravity设置成kCAGravityCenter,我们看一下,效果会是什么样?

整个图片直接放大,将原图层盖住,这是因为kCAGravityCenter属性值,默认是不会对图片进行拉伸,所以将图片的原始大小展示了出来,这时候,我们的contentsScale就起到了作用

layerView.layer.contentsScale = image.scale;

这时,我们看到,现在使用了正确的图片来进行绘制

注意,当我们使用代码来设置寄宿图时,我们一定要手动设置contentsScale

layerView.layer.contentsScale = [UIScreen mainScreen].scale;

这样,我们的图片在retina设备上,才会显示正常。

maskToBounds属性

不知大家有没有注意到,在上面的图中,我们的图片已经超过了图层的边界,默认情况下,在UIView中,也会绘制超过边界的内容或者子视图。

在UIView中,控制是否超出边界的属性是clipsToBounds,在CALayer中,控制的属性是masksToBounds,将他设置成yes

未完待续。。。