您的位置:首页 > 移动开发 > IOS开发

iOS技术沙龙之 - 核心动画(1)

2015-11-20 22:04 489 查看

前言

上周末,非常开心的进行了我们**iOS技术沙龙-北京站**的第一次聚会。虽然因为是第一次聚会,准备的还不是很充分,但是大家还是讨论的很热烈,效果很不错。
第一次分享的内容并不是很多,今天开始,每个月我会把分享的内容以尽量精简的语言整理在blog上面,以供需要的朋友翻看。


CALayer

一、核心动画介绍

Core Animation 是跨平台的,支持iOS环境和Mac OS X环境,而CALayer是核心动画的基础,可以帮助开发者做圆角、阴影、边框等效果。我们学习核心动画之前,需要先理解CALayer,因为
核心动画操作的对象不是UIView,而是CALayer。
对于UIView控件每个内部都有一个Layer的属性。我们在实现核心动画时,本质上是将CALayer中的内容转换成位图,从而便于图形硬件的操纵。

二、图层和视图之间的关系

视图(UIView):直观上屏幕矩形块显示的内容,派生自UIView;可管理子视图的位置,能够接受输入、触摸、绘图和放射变换(缩放,位移)等一系列的操作行为.

图层(CALayer):用来在屏幕上显示和做动画,不能响应事件.

创建视图对象时,视图会自己创建一个层,视图在绘图(如drawRect:)时,会将内容画在自己的层上。当视图在层上完成绘图后,系统会将图层拷贝至屏幕。每个视图都有一个层,而每个图层又可以有多个子层。


  提示:

  1.Layer的设计目的不是为了取代视图,因此不能基于CALayer创建一个独立的可视化组件

  2.Layer的设计目的是提供视图的基本可视内容,从而提高动画的执行效率

  3.除提供可视内容外,Layer不负责视图的事件响应、内容绘制等工作,同时Layer不能参与到响应者链条中

图层树



图层树
包含每一层的对象模型值。他们就是你设定的图层的属性值。

呈现树
包含了当前动画发生时候将要显示的值,例如你要给图层背景颜色设置新的值的时候,它会立即修改图层树里面相应的值。但是在呈现树里面背景颜色值在将要显示给用户的时候才被更新为新值。

渲染树
在渲染图层的时候使用呈现树的值。渲染树负责执行独立于应用活动的复杂操作。渲染由一个单独的进程或线程来执行,使其对应用程序的运行循环影响最小。

三、CALayer的使用说明

通过UIView的layer属性可以拿到对应的根层,这个层不允许重新创建,但可以往层里面添加子层(调用CALayer的addSublayer)

要具体使用CALayer,需要引入
<QuartzCore/QuartzCore.h>


获取当前图层或使用静态方法layer初始化CALayer后,可以设置以下属性:

bounds:宽度和高度

position:位置(默认指中心点,具体由anchorPoint决定)

anchorPoint:锚点(x,y的范围都是0-1),决定了position的含义

backgroundColor: 背景颜色(CGColorRef类型)

borderColor:边框颜色(CGColorRef类型)

borderWidth:边框宽度

cornerRadius:圆角半径

contents: 内容(比如设置为图片CGImageRef)

注意:虽然CALayer可以使用frame,但最好还是使用bounds和position。为层设置动画时,用bounds和position会方便一点。

1、图层的几何属性

如图:



图层的 position 属性是一个 CGPoint 的值,它指定图层相当于它父图层的位置,该值基于父图层的坐标系。

图层的 bounds 属性是一个 CGRect 的值,指定图层的大小(bounds.size)和图层的原点(bounds.origin)。当你重写图层的重画方法的时候,bounds 的原点可以作为图形上下文的原点。

图层拥有一个隐式的 frame,它是 position,bounds,anchorPoint 和 transform 属性的一部分。设置新的 frame 将会相应的改变图层的 position 和 bounds 属性,但是 frame本身并没有被保存。但是设置新的 frame 时候,bounds 的原点不受干扰,bounds 的大小变为 frame 的大小,即 bounds.size=frame.size。图层的位置被设置为相对于锚点(anchor point)的适合位置。当你设置 frame 的值的时候,它的计算方式和 position、bounds、和 anchorPoint 的属性相关。

图层的 anchorPoint 属性是一个 CGPoint 值,它指定了一个基于图层 bounds 的符合位置坐标系的位置。锚点(anchor point)指定了 bounds 相对于 position 的值,同时也作为变换时候的支点。锚点使用单元空间坐标系表示,(0.0,0.0)点接近图层的原点,而(1.0,1.0)是原点的对角点。改变图层的父图层的变换属性(如果存在的话)将会影响到 anchorPoint 的方向,具体变化取决于父图层坐标系的 Y 轴。

当你设置图层的 frame 属性的时候,position 会根据锚点(anchorPoint)相应的改变,而当你设置图层的 position 属性的时候,bounds 会根据锚点(anchorPoint)做相应的改变。

举个例子来说明这几者的关系:
如果你新创建一个图层,设置图层的 frame 为(40.0,60.0,120.0,80.0),那么相应的 position 属性值将会自动设置为(100.0,100.0),而 bounds 会自动设置为(0.0,0.0,120.0,80.0)。
那么这其中是怎么变化的呢?
原来,当我们设置frame属性为(40.0,60.0,120.0,80.0),系统则会自动处理bounds属性为(0.0,0.0,120.0,80.0)。而这个时候,默认的anchorPoint的值为(0.5,0.5),所以系统计算到图层的相对位置为(120 * 0.5,80 * 0.5),也就是(60,40)。加上本身基于frame的(40,60),就会自动生成position的属性为(40+60,60+40),也就是(100,100)。
所以,在我们看来的frame,其实只是为了方便开发而提供开发者的一套隐式的东西,并没有实际意义。


因此,当我们去操作CALayer的时候,建议操作bounds和position,也就是在我们平时开发中使用的bounds和center属性。

2、图层的其他属性

contents属性

CALayer的一个属性,它真正要赋值的类型应该是CGImageRef ,可以给这个属性赋予一个CGImage就能让一个普通的UIView显示图像;

contentGravity属性

contentsGravity与UIView的contentMode类似,但是它是一个NSString类型 ;

self.layerView.layer.contentsGravity = kCAGravityResizeAspect;


maskToBounds属性

UIView有一个叫做clipsToBounds的属性可以用来决定是否显示超出边界的内容,CALayer对应的属性叫做masksToBounds

contentsRect属性

CALayer的contentsRect属性允许我们在图层边框里显示寄宿图的一个子域,默认的contentsRect是{0, 0, 1, 1}

图片的拼接

图片拼合后可以打包整合到一张大图上一次性载入。相比多次载入不同的图片,这样做能够带来很多方面的好处:内存使用,载入时间,渲染性能等等

关于设置layer的内容

上面说到了contents属性,其实你可以通过以下任何一种方法指定 CALayer 实例的内容:

使用包含图片内容的 CGImageRef 来显式的设置图层的 contents 的属性。

self.layerView.layer.contents = (__bridge id _Nullable)(contentImage.CGImage);


指定一个委托,它提供或者重绘内容。

你可以绘制图层的内容,或更好的封装图层的内容图片,通过创建一个委托类实现下列方法之一:

displayLayer:或 drawLayer:inContext:


要显式的告诉一个图层实例来重新缓存内容,通过发送以下任何一个方法

setNeedsDisplay
或 者
setNeedsDisplayInRect:
的 消 息 , 或 者 把 图 层 的
needsDisplayOnBoundsChange
属性值设置为 YES。

通过委托实现方法
displayLayer:
可以根据特定的图层决定显示什么图片,还可以更加需要设置图层的
contents
属性值。下面的例子是“图层的坐标系”部分的,它实现
displayerLayer:
方法根据
state
的值设置
theLayer
contents
属性。子类不需要存储
state
的值,因为 CALayer 的实例是一个
键-值编码
容器。

- (void)displayLayer:(CALayer *)theLayer
{
// check the value of the layer's state key
if ([[theLayer valueForKey:@"state"] boolValue])
{
// display the yes image
theLayer.contents=[someHelperObject loadStateYesImage];
}else {
// display the no image
theLayer.contents=[someHelperObject loadStateNoImage];
}


如 果 你 必 须 重 绘 图 层 的 内 容 , 而 不 是 通 过 加 载 图 片 , 那你 需 要 实 现
drawLayer:inContext:
方 法 。 通 过 委 托 可 以 决 定 哪 些 内 容 是 需 要 的 并 使 用
CGContextRef
来重绘内容。

下面的例子是“指定图层的几何”部分内容,它实现
drawLayer:inContext:
方法使用
lineWidth
键值来重绘一个路径(path),返回
therLayer


- (void)drawLayer:(CALayer *)theLayer
inContext:(CGContextRef)theContext
{
CGMutablePathRef thePath = CGPathCreateMutable();
CGPathMoveToPoint(thePath,NULL,15.0f,15.f);
CGPathAddCurveToPoint(thePath,
NULL,
15.f,250.0f,
295.0f,250.0f,
295.0f,15.0f);
CGContextBeginPath(theContext);
CGContextAddPath(theContext, thePath );
CGContextSetLineWidth(theContext,
[[theLayer valueForKey:@"lineWidth"] floatValue]);
CGContextStrokePath(theContext);
// release the path
CFRelease(thePath);
}


继承 CALayer 类重载显示的函数。

虽然通常情况不需要这样做,但是你仍可以继承 CALayer 直接重载重绘和显示方法。这个通常发生在你的图层需要定制行为而委托又无法满足需求的时候。

子类可以重载 CALayer 的显示方法,设置图层的内容为适当的图片。下面的例子是“变换图层的几何”部分的内容,它提供了和“图层的坐标系”例子相同的功能。不同的是子类定义
state
为实例的属性,而不是根据 CALayer 的键-值编码容器获取。

- (void)drawInContext:(CGContextRef)theContext
{
CGMutablePathRef thePath = CGPathCreateMutable();
CGPathMoveToPoint(thePath,NULL,15.0f,15.f);
CGPathAddCurveToPoint(thePath,
NULL,
15.f,250.0f,
295.0f,250.0f,
295.0f,15.0f);
CGContextBeginPath(theContext);
CGContextAddPath(theContext, thePath );
CGContextSetLineWidth(theContext,self.lineWidth);
CGContextSetStrokeColorWithColor(theContext,self.lineColor);
CGContextStrokePath(theContext);
CFRelease(thePath);
}


CALayer、UIView以及上下文的之间的关系

  (1)当UIView收到setNeedsDisplay消息时,CALayer会准备好一个CGContextRef,然后向它的delegate即UIView发送消息,并且传入已经准备好的CGContextRef对象。UIView在drawLayer:inContext:方法中会调用自己的drarRect:方法。

  (2)平时在drawRect:中通过UIGraphicsGetCurrentContext()获取的就是有CALayer传入的CGContextRef对象,在drawRect:中完成的所有绘图都会填入CALayer的CGContextRef中,然后被拷贝至屏幕

  (3)CALayer的CGContextRef用的是位图上下文(Bitmap Grahpics Context)。


3、图层的视觉效果

圆角的设置

CALayer有一个叫做conrnerRadius的属性控制着图层角的曲率,是一个浮点类型,把masksToBounds设置成YES的话,才能实现效果

self.layerView.layer.cornerRadius = 1.0f;
self.layerView.layer.maskToBounds = YES;
//或者直接通过clipsToBounds
//self.layerView.clipsToBounds = YES;


图层边框

CALayer另外两个非常有用属性就是borderWidth和borderColor。二者共同定义了图层边的绘制样式。

边框是绘制在图层边界里面的,而且在所有子内容之前,也在子图层之前.

self.layerView.layer.borderWidth = 2.0f;
self.layerView.layer.borderColor = [UIColor blackColor].CGColor;


阴影

给shadowOpacity属性一个大于默认值(也就是0)的值,阴影就可以显示在任意图层之下

shadowOpacity是一个必须在0.0(不可见)和1.0(完全不透明)之间的浮点数

layer.shadowColor = [UIColor redColor].CGColor; //shadowColor阴影颜色
layer.shadowOffset = CGSizeMake(2.0f , 2.0f); //shadowOffset阴影偏移x,y向(上/下)偏移(-/+)2
layer.shadowOpacity = 0.5f;//阴影透明度,默认0
layer.shadowRadius = 4.0f;//阴影半径,值越大阴影越模糊


阴影剪裁

阴影通常就是在Layer的边界之外,如果你开启了masksToBounds属性,所有从图层中突出来的内容都会被裁剪掉

这个可以使用两个图层来解决

shadowPath属性

shadowPath是一个CGPathRef类型(一个指向CGPath的指针)。CGPath是一个Core Graphics对象,用来指定任意的一个矢量图形

self.layerView.layer.shadowOpacity   = 0.5f;//设置阴影显示
CGMutablePathRef squarePath = CGPathCreateMutable();
CGPathAddRect(squarePath, NULL, self.layerView.bounds);
self.layerView.layer.shadowPath = squarePath;
self.layerView.layer.shadowOffset = CGSizeMake(0, 3);
CGPathRelease(squarePath);


图层蒙版

图层+图层蒙版 = 图层蒙版在图层上的轮廓

CALayer *maskLayer = [CALayer layer];
maskLayer.frame = self.layerView.bounds;
UIImage *maskImage = [UIImage imageNamed:@"Cone.png"];
maskLayer.contents = (__bridge id)maskImage.CGImage;
self.imageView.layer.mask = maskLayer;


图片拉伸

minification(缩小图片)和magnification(放大图片)默认的过滤器都是kCAFilterLinear,这个过滤器采用双线性滤波算法,它在大多数情况下都表现良好。双线性滤波算法通过对多个像素取样最终生成新的值,得到一个平滑的表现不错的拉伸。但是当放大倍数比较大的时候图片就模糊不清了。

组透明度(透明度叠加问题)

一个控件有子控件的情况下,设置透明度会导致透明度的混合增加

- (UIButton *)customButton
{
//create button
CGRect frame = CGRectMake(0, 0, 150, 50);
UIButton *button = [[UIButton alloc] initWithFrame:frame];
button.backgroundColor = [UIColor whiteColor];
button.layer.cornerRadius = 10;
//add label
frame = CGRectMake(20, 10, 110, 30);
UILabel *label = [[UILabel alloc] initWithFrame:frame];
label.text = @"Hello World";
label.textAlignment = NSTextAlignmentCenter;
[button addSubview:label];
return button;
}
- (void)viewDidLoad
{
[super viewDidLoad];
//create opaque button
UIButton *button1 = [self customButton];
button1.center = CGPointMake(50, 150);
[self.containerView addSubview:button1];
//create translucent button
UIButton *button2 = [self customButton];
button2.center = CGPointMake(250, 150);
button2.alpha = 0.5;
[self.containerView addSubview:button2];

//设置下面两个属性就会解决透明度叠加的问题
button2.layer.shouldRasterize = YES;
button2.layer.rasterizationScale = [UIScreen mainScreen].scale;
}


4、CALayer的隐式动画属性

每一个UIView内部都默认关联着一个CALayer,称这个Layer为Root Layer。所有的非Root Layer都存在着隐式动画,隐式动画的默认时长为1/4秒。

当修改非Root Layer的部分属性时,相应的修改会自动产生动画效果,能执行隐式动画的属性被称为“可动画属性”,诸如:

bounds: 缩放动画

position: 平移动画

opacity: 淡入淡出动画(改变透明度)

在文档中搜素animatable可以找到所有可动画属性

如果要关闭默认的动画效果,可以通过动画事务方法实现:

  [CATransaction begin];//开启事务
  [CATransaction setDisableActions:YES];//取消隐式动画
  [CATransaction commit];//事务提交


关于图层变换

仿射变换

UIView可以通过设置transform属性做变换,transform是一CGAffineTransform类型,CGAffineTransform是一个可以和二维空间向量(例如CGPoint)做乘法的3X2的矩阵



我们使用的旋转常量是M_PI,而不是你想象的180°,因为iOS的变换函数使用弧度而不是角度作为单位。弧度用数学常量pi的倍数表示,一个pi代表180度,所以四分之一的pi就是45度

CGAffineTransform transform = CGAffineTransformMakeRotation(M_PI);
self.layerView.layer.affineTransform = transform;
//或者通过UIView
//self.layerView.transform = transform;


混合变换

Core Graphics提供了一系列的函数可以在一个变换的基础上做更深层次的变换,如果做一个既要缩放又要旋转的变换

-(void)viewDidLoad
{
[super viewDidLoad]; //create a new transform
CGAffineTransform transform = CGAffineTransformIdentity; //scale by 50%
transform = CGAffineTransformScale(transform, 0.5, 0.5); //rotate by 30 degrees
transform = CGAffineTransformRotate(transform, M_PI / 180.0 * 30.0); //translate by 200 points
transform = CGAffineTransformTranslate(transform, 200, 0);
//apply transform to layer
self.layerView.layer.affineTransform = transform;
}


3D转换

CALayer有一个zPosition属性,可以用来让图层靠近或者远离(用户视角),transform属性(CATransform3D类型)可以真正做到这点,即让图层在3D空间内移动或者旋转。和CGAffineTransform类似,CATransform3D也是一个矩阵,但是和2x3的矩阵不同,CATransform3D是一个可以在3维空间内做变换的4x4的矩阵,做过游戏或者VR的同仁们,应该都知道这个,下面是一个常见的向量与矩阵相乘的计算公式。



下面这个是CATransform3D的结构体定义:



-(void)viewDidLoad
{
[super viewDidLoad];
//效果:看起来图层并没有被旋转,而是仅仅在水平方向上的一个压缩
CATransform3D transform = CATransform3DMakeRotation(M_PI_4, 0, 1, 0);
self.layerView.layer.transform = transform;
}


透视投影

CATransform3D的透视效果通过一个矩阵中一个很简单的元素来控制:m34;

34的默认值是0,我们可以通过设置m34

为-1.0 / d来应用透视效果,d代表了想象中视角相机和屏幕之间的距离,以像素为单位,那应该如何计算这个距离呢?实际上并不需要,大概估算一个就好了

- (void)viewDidLoad
{
[super viewDidLoad];
//create a new transform
CATransform3D transform = CATransform3DIdentity;
//apply perspective
transform.m34 = - 1.0 / 500.0;// 500-1000均可
//rotate by 45 degrees along the Y axis
transform = CATransform3DRotate(transform, M_PI_4, 0, 1, 0);
//apply to layer
self.layerView.layer.transform = transform;
}


sublayerTransform属性

CALayer有一个属性叫做sublayerTransform。它也是CATransform3D类型,但和对一个图层的变换不同,它影响到所有的子图层。这意味着你可以一次性对包含这些图层的容器做变换,于是所有的子图层都自动继承了这个变换方法

- (void)viewDidLoad
{
[super viewDidLoad];
//apply perspective transform to container
CATransform3D perspective = CATransform3DIdentity;
perspective.m34 = - 1.0 / 500.0;
self.containerView.layer.sublayerTransform = perspective;
//rotate layerView1 by 45 degrees along the Y axis
CATransform3D transform1 = CATransform3DMakeRotation(M_PI_4, 0, 1, 0);
self.layerView1.layer.transform = transform1;
//rotate layerView2 by 45 degrees along the Y axis
CATransform3D transform2 = CATransform3DMakeRotation(-M_PI_4, 0, 1, 0);
self.layerView2.layer.transform = transform2;
//containerView 是layerView1、layerView2的父视图
}


小结

上面就是对于核心动画的前置内容CALayer的介绍,下面通过一些比较通俗的方式再对CALayer和UIView之间的区别做个小结。下一章开始进入CoreAnimation的介绍。

UIView是iOS系统中界面元素的基础,所有的界面元素都继承自它。它本身完全是由CoreAnimation来实现的(Mac下似乎不是这样)。它真正的绘图部分,是由一个叫CALayer(Core Animation Layer)的类来管理。UIView本身,更像是一个CALayer的管理器,访问它的跟绘图和跟坐标有关的属性,例如frame,bounds等等,实际上内部都是在访问它所包含的CALayer的相关属性。

UIView有个layer属性,可以返回它的主CALayer实例,UIView有一个layerClass方法,返回主layer所使用的类,UIView的子类,可以通过重载这个方法,来让UIView使用不同的CALayer来显示,例如通过

- (class) layerClass {
return ([CAEAGLLayer class]);
}


使某个UIView的子类使用GL来进行绘制。

UIView的CALayer类似UIView的子View树形结构,也可以向它的layer上添加子layer,来完成某些特殊的表示。例如下面的代码

grayCover = [[CALayer alloc] init];
grayCover.backgroundColor = [[[UIColor blackColor] colorWithAlphaComponent:0.2] CGColor];
[self.layer addSubLayer: grayCover];


会在目标View上敷上一层黑色的透明薄膜(图层蒙版)。

UIView的layer树形在系统内部,被系统维护着三份copy。

第一份,逻辑树,就是代码里可以操纵的,例如更改layer的属性等等就在这一份。

第二份,呈现树,这是一个中间层,系统正在这一层上更新属性。

第三份,渲染树,顾名思义,最后渲染图层内容。

动画的运作

UIView的主layer以外,对它的subLayer,也就是子layer的属性进行更改,系统将自动进行动画生成,动画持续时间有个缺省时间,可以通过
[UIApplication sharedApplication].statusBarOrientationAnimationDuration
获取当前系统的动画时常。在动画时间里,系统自动判定哪些属性更改了,自动对更改的属性进行动画插值,生成中间帧然后连续显示产生动画效果。

坐标系系统(对position和anchorPoint的关系还是犯晕)

CALayer的坐标系系统和UIView有点不一样,它多了一个叫anchorPoint的属性,它使用CGPoint结构,但是值域是0~1,也就是按照比例来设置。这个点是各种图形变换的坐标原点,同时会更改layer的position的位置,它的缺省值是{0.5 * w, 0.5 * h},也就是在layer的中央。

某layer.anchorPoint = CGPointMake(0.f, 0.f);

如果这么设置,layer的左上角就会被挪到原来的中间的位置,

加上这样一句就好了

某layer.position = CGPointMake(0.f, 0.f);
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息