前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >iOS动画-CALayer基础知识

iOS动画-CALayer基础知识

作者头像
梧雨北辰
发布2019-04-22 10:32:28
1.9K0
发布2019-04-22 10:32:28
举报
文章被收录于专栏:梧雨北辰的开发录

核心动画Core Animation,其实是由Layer Kit这样一个名字演变而来。它实际上是一个复合引擎,可以将存储在图层树体系中的不同独立图层,尽可能快地组合成不同的可视内容呈现于屏幕上;所以做动画只是Core Animation的特性之一;

Core Animation直接作用于CALayer上,而图层树又是形成了UIKit以及我们在iOS应用程序所能在屏幕上看见一切的基础。因此,在讨论动画之前,我们有必要对于图层这一概念进行深入的理解。

本篇主要内容: 1.理解视图与图层 2.CALyer寄宿图与contents属性 3.UIView方法绘制自定义寄宿图 4.CALyer方法绘制自定义寄宿图 5.Frame与Bounds的区别 6.中心点(position)与锚点(anchorPoint) 7.视图与图层的坐标系

一、理解视图与图层

UIView我们都非常熟悉, 但它其实是对于CALayer的一层封装,我们在创建UIView时,其内部会自动创建CALayer图层对象(即UIView的关联图层),UIView调用drawRect:方法进行绘图,并且将所有的内容绘制到自己的图层上,绘制完毕后,系统会将图层拷贝到屏幕上,于是就完成了UIView显示。

视图的的职责就是创建并管理这个图层,以确保子视图在层级关系中添加或者被移除的时候,它们的关联图层也同样对应在层级关系树当中有相同的操作。我们在访问UIView的frame,bounds等属性又或者设置动画,其实也都是在操作其关联图层CALayer的特性。

但是,UIView因为继承了UIResponder而具备响应事件的能力;而CALayer并不清楚具体的响应者链(iOS通过视图等级关系用来传送触摸事件的机制),于是它并不能响应事件,即使它也提供一些方法来判断是否一个触点在图层的范围之内。

最后,总结UIView(视图)与CALayer(图层)的关系:UIView = CALayer(负责绘制显示内容的功能) + 处理用户交互的功能。

1.图层与视图的底层关系

下面的图示很好的展示了UIView与CALayer的底层上的区别:

图层与视图的底层关系.png

UIView、UIColor、UIImage都定义于UIKit框架中; CALayer定义在QuartzCore框架中的CoreAnimation中; CGImageRef、CGColorRef两种数据类型是定义在Core Graphics框架中;

QuartzCore框架和CoreGraphics框架可以跨平台使用,在iOS和Mac OS上都能使用 ,但是UIKit却只能在iOS中使用;为了保证可移植性,QuartzCore是不能直接使用UIImage和UIColor的,如果使用需要将其转化为CGImageRef、CGColorRef

2.使用图层

使用图层十分简单,区别在于图层必须添加到图层上,具体代码如下:

代码语言:javascript
复制
- (void)viewDidLoad {
    [super viewDidLoad];

    CALayer *colorLayer = [CALayer new];
    colorLayer.backgroundColor = [UIColor orangeColor].CGColor;
    colorLayer.frame = CGRectMake(30, 30, kDeviceWidth -60,  200);
    [self.view.layer addSublayer:colorLayer];
}
3.图层的能力

苹果为我们提供了简洁方便的UIView的接口,而且为UIView增加了处理触摸事件的能力,但这种简单的设计也不可避免带来灵活上的缺陷,如果我们需要在底层做一些改变,或者使用一些没有在UIView上实现的接口功能,此时就需要我们介入Core Amimation底层了。 下面是一些UIView没有暴露出来的CALayer的功能:

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

二、CALyer寄宿图与contents属性

CALayer具有和UIView一样的层级关系树,可用于显示一个矩形块。但事实上它还通过contents属性包含并显示一张图片,称之为CALayer的寄宿图。CALayer的contents属性虽被定义为id,但是真正可以被赋值的类型是CGImageRef,指向的是一个CGImage结构的指针。

在Mac OS系统上,contents属性对于CGIamge和NSImage类型的值都起作用;而对于iOS平台,虽然UIImage的CGImage属性也返回一个CGImageRef,但如果将这个值直接赋值给CALayer的contents,却会得到一个编译错误。这是因为CGImageRef并不是一个真正的Cocoa对象,而是一个Core Foundation类型;

具体解决方法就是使用bridged关键字,下面是用于演示的代码:

代码语言:javascript
复制
- (void)viewDidLoad {
    [super viewDidLoad];

    UIView *colorView = [UIView new];
    colorView.backgroundColor = [UIColor orangeColor];
    colorView.frame = CGRectMake(30, 30, kDeviceWidth -60,  200);
    [self.view addSubview:colorView];
    
    UIImage *headerImage = [UIImage imageNamed:@"header"];
    colorView.layer.contents = (__bridge id)headerImage.CGImage;
}

效果图如下:

测试CALayer寄宿图1.png

我们没有通过UIImageView的方法,而是直接利用CALaye显示了一张图片。这似乎很酷,但惊喜之余,我们也发现了仍然存在的小缺憾,那就是此时的图片显示效果是变形的;那它是否也可以像UIImageView一样具有可设置的方法呢,答案是肯定的,我们可以使用如下的代码,将图片自适应显示:

代码语言:javascript
复制
colorView.layer.contentsGravity = kCAGravityResizeAspect;

效果图如下:

测试CALayer寄宿图2.png

另外,类似的对于CALayer的显示设置和UIView具有下面的对应关系(这里仅简单总结概念和用处):

CALayer属性

UIView属性

属性说明

contentsGravity(NSString)

contentMode (枚举)

内容填充方式(填满、自适应等)

contentsScale(CGFloat)

contentScaleFactor(CGFloat)

像素尺寸和视图大小的比例,默认1.0;1.0:以每个点1个像素绘制图片;2.0:以每个点2个像素绘制图片,Retina屏幕;

maskToBounds(BOOL)

clipsToBounds(BOOL)

超出边界的内容或者子视图是否显示

contentsRect (CGRect)

允许在图层边框里显示寄宿图的一个子域

contentCenter (CGRect)

定义一个固定的边框和一个在图层上可拉伸的区域

三、UIView方法绘制自定义寄宿图

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

-drawRect:方法是UIView没有默认实现的方法,因为寄宿图并不是必须的;但如果UIView检测到此方法被实现了,此方法会被自动调用,然后我们就可以在其中使用Core Graphics绘制自己需要的内容了;下面的代码就演示了drawRect自定义绘制寄宿图的具体操作,实现了一个环形的绘制:

代码语言:javascript
复制
@implementation TestLayerVC
- (void)viewDidLoad {
    //测试drawRect自定义绘制寄宿图
    CustomCircleView *customCircleView = [CustomCircleView new];
    customCircleView.frame = CGRectMake((kDeviceWidth - 100)/2, 250, 100 , 100);
    [self.view addSubview:customCircleView];
}
@end

@implementation CustomCircleView
- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        //使用drawRect,默认背景色为黑色;以下两种方式解决:
        // self.opaque = NO;
        self.backgroundColor = [UIColor purpleColor];
    }
    return self;
}

- (void)drawRect:(CGRect)rect{
    //获取画布
    CGContextRef context = UIGraphicsGetCurrentContext();
    //画笔颜色
    CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
    //画笔宽度
    CGFloat lineWidth = 5;
    CGContextSetLineWidth(context, lineWidth);
    //圆点坐标
    CGFloat centerX = CGRectGetWidth(rect)/2.0;
    CGFloat centerY = CGRectGetHeight(rect)/2.0;
    CGFloat cusRadius  = self.frame.size.width/2.0 - lineWidth/2.0;
    double  PI = 3.14159265358979323846;

    //绘制路径:初始角度、结束角度
    CGContextAddArc(context, centerX, centerY, cusRadius, 1.5*PI, 1.5*PI + 2*PI, NO);
    CGContextDrawPath(context, kCGPathStroke);
}

绘制效果如下:

自定义绘制寄宿图1.png

特别注意1:如果没有自定义绘制任务不需要寄宿图,就不要在子类中写一个空的-drawRect:方法,否则会造成CPU资源和内存的浪费; 特别注意2:如果我们将绘制过程的角度参数改为动态,并结合定时器调用-setNeedsDisplay方法,就可以实现环形动画的效果(这里就不做具体演示了);

四、CALyer方法绘制自定义寄宿图

虽然-drawRect:方法是实现了自定义寄宿图绘制,但事实上还是底层的CALayer重绘并保存了因此产生的图片;CALayer有一个可选的delegate属性,实现了CALayerDelegate非正式协议,当CALayer需要一个内容特定信息时,就会从协议中请求;而当需要被绘制时,CALayer会通过如下的方法来请求代理给它提供寄宿图;

代码语言:javascript
复制
//方法1:可以直接设置contents属性;
 - (void)displayLayer:(CALayer *)layer;
 
//方法2:在不实现方法1时,CALayer就会转而尝试调用此的方法;
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;

在调用方法2之前,CALayer会创建了一个合适尺寸的空寄宿图(尺寸由bounds和contentScale决定)和一个Core Graphics的绘制上下文环境,为绘制寄宿图做准备,并将其以ctx参数传入。现在我们以方法2为例,演示CALayer绘制自定义寄宿图的过程,具体代码如下:

代码语言:javascript
复制
@implementation TestLayerVC
- (void)viewDidLoad {
    CALayer *blueLayer = [CALayer layer];
    blueLayer.frame =CGRectMake((kDeviceWidth - 100)/2, 400, 100 , 100);
    blueLayer.backgroundColor = [UIColor purpleColor].CGColor;
    blueLayer.delegate = self;
    
    blueLayer.contentsScale = [UIScreen mainScreen].scale;
    [self.view.layer addSublayer:blueLayer];
    
    [blueLayer display];
}


- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx{
    CGContextSetLineWidth(ctx, 10.f);
    CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor);
    CGContextStrokeEllipseInRect(ctx, layer.bounds);
}

@end

效果图如下:

自定义绘制寄宿图2.png

代码分析: 1. 主动绘制 我们需要显式的调用-display方法;这不同于UIView,当图层显示到屏幕上时,CALayer不会自动重绘它的内容,CALayer把重绘的决定权交给了开发者;

2.绘制特点 尽管没有使用masksToBounds属性,但示例中绘制的视图依然被裁剪了,这是因为通过CALayer绘制寄宿图并没有对超出边界外的内容提供绘制支持;

3.设置代理 CALayerDelegate不能是UIView和UIViewController,如上述代码的演示就会造成崩溃; UIView本身携带的layer的代理就是自己,如果将一个layer的代理设置成它,那它本身的layer就会受到影响,通常表现为野指针崩溃;而UIViewController在经历Push和Pop之后也可能被释放,造成野指针崩溃;所以,对于这个问题的解决方案是:创建继承于NSObject的类,用于实现CALayerDelegate并管理CALayer的绘制逻辑;

使用总结:当我们需要自定义寄宿图时,其实不必实现displayLayer:和-drawLayer: inContext:方法来绘制寄宿图。通常的做法还是实现UIView的-drawRect:方法,这样UIView就会自动帮我们做完剩下的工作,包括需要重绘的时候调用-display方法;

五、Frame与Bounds的区别

我们已经知道UIView的很多布局属性其实都来自于图层;UIView的布局属性包括:frame、bouns、center,分别对应了CALayer中frame、bounds、position。为了能清楚区分,图层用了position,视图用了center,但它们都代表了同样的值,另外CALayer的锚点(anchorPoint)没有在UIView中公开。

UIView属性

CALayer属性

属性说明

frame

frame

表示相对于其父视图的坐标位置

bounds

bounds

表示相对于其自身的坐标位置,{0,0}通常是其左上角

center

position

表示父图层所在锚点AnchorPoint所在的位置

frame&&bounds.png

上图对原有视图做了旋转变换,之后的frame实际上代表了覆盖在图层旋转之后的整个轴对齐的矩形区域,此时frame的宽和高和bounds不再一致了。

其实,对于视图和图层来说,frame是根据bounds、position、和transform计算而来的;所以当其中的任何一个值发生变化时,frame就会发生变化,相反改变frame也同样影响他们当中的值。

六、中心点(position)与锚点(anchorPoint)

1.锚点的概念

position与anchorPoint是两个容易混淆的概念,我们首先从Xcode中找到关于它们的注释说明如下:

代码语言:javascript
复制
/* The position in the superlayer that the anchor point of the layer's
 * bounds rect is aligned to. Defaults to the zero point. Animatable. */
@property CGPoint position;

/* Defines the anchor point of the layer's bounds rect, as a point in
 * normalized layer coordinates - '(0, 0)' is the bottom left corner of
 * the bounds rect, '(1, 1)' is the top right corner. Defaults to
 * '(0.5, 0.5)', i.e. the center of the bounds rect. Animatable. */
@property CGPoint anchorPoint;

我们可以看出,position被用于描述当前layer在superlayer中的位置,而且是通过当前layer的anchorPoint来确定的。换句话来讲就是:position是当前layer的anchorPoint在superLayer中的位置

我们也可以更确切理解为:position是相对于superLayer来讲,而anchorPoint是相对于当前layer来讲;只不过在默认情况下,anchorPoint与position是重合的;锚点是用单位坐标来描述的(即图层的相对坐标),图层的左上角是{0,0},右下角是{1,1},因此图层的默认锚点是{0.5, 0.5},表示图层的中间位置代表了其位置position。

下面的图示是将锚点从{0.5,0.5}改为了{0,0},我们在这里更容易看到position与anchorPoint之间的关系:

anchorPoint.png

如图,修改图层锚点会改变layer的frame,但是其position不会改变,这看起来似乎有点奇怪,但是我们依然可以通过一些计算方式看出端倪:

代码语言:javascript
复制
position.x = frame.origin.x + 0.5 * bounds.size.width;  
position.y = frame.origin.y + 0.5 * bounds.size.height; 

这里的0.5参数,其实就是由于锚点默认值得到的,所以改进公式如下:

代码语言:javascript
复制
position.x = frame.origin.x + anchorPoint.x * bounds.size.width;  
position.y = frame.origin.y + anchorPoint.y * bounds.size.height;

此时,我们如果通过修改anchorPoint的值来进行测试,就会发现改变的只有frame的origin,这就说明修改position与anchorPoint中任何一个属性都不能影响另一个属性,由此我们也可以再次改进公式:

代码语言:javascript
复制
frame.origin.x = position.x - anchorPoint.x * bounds.size.width;  
frame.origin.y = position.y - anchorPoint.y * bounds.size.height;

最后得出结论:frame的origin坐标由position与anchorPoint来共同决定;

2.锚点的作用

锚点就相当于一个支点,可以形象的理解为一颗固定了图层的图钉,尤其是我们在做旋转动画时,可能会需要设置此属性来决定图层是围绕哪一个点旋转的;但这时候我们又不得不考虑一个问题:修改锚点可以让我们的动画围绕非中心点旋转,但是这也改变了原有视图的位置frame,这是我们不想要的结果,该如何解决呢?这里提供一种方法如下:

代码语言:javascript
复制
- (void)setAnchorPoint:(CGPoint)anchorPoint forView:(UIView *)view{
    CGPoint oldOrigin = view.frame.origin;
    view.layer.anchorPoint = anchorPoint;
    CGPoint newOrigin = view.frame.origin;
    
    CGPoint transition;
    transition.x = newOrigin.x - oldOrigin.x;
    transition.y = newOrigin.y - oldOrigin.y;
    
    view.center = CGPointMake (view.center.x - transition.x, view.center.y - transition.y);
}

下面再来具体演示一下修改锚点改变动画状态的用法,我们分别创建橙色视图默认围绕中心旋转,而紫色视图围绕左顶点旋转,关键代码如下:

代码语言:javascript
复制
#import "TestLayerFiveVC.h"
@interface TestLayerFiveVC ()

@property (nonatomic,strong) UIView *viewA;
@property (nonatomic,strong) UIView *viewB;

@end

@implementation TestLayerFiveVC

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self.view addSubview:self.viewA];
    [self.view addSubview:self.viewB];
    
    [self.viewA mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(self.view).offset(100);
        make.centerX.equalTo(self.view);
        make.width.height.mas_equalTo(100);
    }];
    
    [self.view addSubview:self.viewB];
    [self.viewB mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(self.viewA.mas_bottom).offset(200);
        make.centerX.equalTo(self.view);
        make.width.height.mas_equalTo(100);
    }];
    [self.view layoutIfNeeded];
    
    
    //ViewA的旋转动画
    [self addRotationAnimation:self.viewA withDuration:3];

    //修改ViewB的锚点,并恢复其原先的Frame,使其可以绕着左上角顶点旋转
    [self resetAnchorPoint:CGPointMake(0, 0) forView:self.viewB];
    [self addRotationAnimation:self.viewB withDuration:3];
}

- (void)resetAnchorPoint:(CGPoint)anchorPoint forView:(UIView *)view{
    CGPoint oldOrigin = view.frame.origin;
    view.layer.anchorPoint = anchorPoint;
    CGPoint newOrigin = view.frame.origin;
    
    CGPoint transition;
    transition.x = newOrigin.x - oldOrigin.x;
    transition.y = newOrigin.y - oldOrigin.y;
    
    //重新设置原来视图位置
    view.center = CGPointMake (view.center.x - transition.x, view.center.y - transition.y);
    [view mas_remakeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(view.superview).offset(view.center.y - transition.y);
        make.leading.equalTo(view.superview).offset(view.center.x - transition.x);
        make.width.mas_equalTo(view.width);
        make.height.mas_equalTo(view.height);
    }];
}

- (void)addRotationAnimation:(__kindof UIView *)view withDuration:(CFTimeInterval)dutation {
    CABasicAnimation *rotationAnimation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
    rotationAnimation.toValue = [NSNumber numberWithFloat: -M_PI * 2.0];
    rotationAnimation.duration = dutation;
    rotationAnimation.cumulative = YES;
    rotationAnimation.repeatCount = MAXFLOAT;
    rotationAnimation.removedOnCompletion = NO;
    [view.layer addAnimation:rotationAnimation forKey:@"rotationAnimation"];
}

效果图如下:

锚点动画.gif

视图与图层的坐标系

CALayer给不同坐标系之间的图层转换提供了一些工具类方法:

代码语言:javascript
复制
- (CGPoint)convertPoint:(CGPoint)p fromLayer:(nullable CALayer *)l;
- (CGPoint)convertPoint:(CGPoint)p toLayer:(nullable CALayer *)l;
- (CGRect)convertRect:(CGRect)r fromLayer:(nullable CALayer *)l;
- (CGRect)convertRect:(CGRect)r toLayer:(nullable CALayer *)l;

与此对应的UIView也具有相似的方法如下:

代码语言:javascript
复制
- (CGPoint)convertPoint:(CGPoint)point toView:(nullable UIView *)view;
- (CGPoint)convertPoint:(CGPoint)point fromView:(nullable UIView *)view;
- (CGRect)convertRect:(CGRect)rect toView:(nullable UIView *)view;
- (CGRect)convertRect:(CGRect)rect fromView:(nullable UIView *)view;

通过这些方法,我们可以把定义在一个图层(或视图)坐标系下的点或者矩形转换为另一个图层(或视图)坐标系下的点或者矩形;开发过程中我们通常操作的对象都是视图,所以下面以视图为例简单演示其用法:首先创建添加两个宽高都是100*100的橙色、紫色视图在控制器的View上,

坐标系.png

使用下面的代码进行测试,结果如下:

代码语言:javascript
复制
CGPoint targetPoint = CGPointMake(10, 10);

CGPoint point1 = [purpleView convertPoint:targetPoint toView:orangeView]; //代码1
CGPoint point2 = [orangeView convertPoint:targetPoint fromView:purpleView];
NSLog(@"\npoint1:%@\npoint2:%@",NSStringFromCGPoint(point1),NSStringFromCGPoint(point2));

/*测试结果:
 point1:{160, 60}
 point2:{160, 60}
 */

代码分析: 这里分别测试了convertPoint的两种用法(convertRect与其相似),我们可以将代码1理解为:参考organView为坐标系时,purpleView上坐标为target的点的坐标值;

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2019.04.14 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、理解视图与图层
  • 二、CALyer寄宿图与contents属性
  • 三、UIView方法绘制自定义寄宿图
  • 四、CALyer方法绘制自定义寄宿图
  • 五、Frame与Bounds的区别
  • 六、中心点(position)与锚点(anchorPoint)
    • 1.锚点的概念
      • 2.锚点的作用
      • 视图与图层的坐标系
      相关产品与服务
      腾讯云代码分析
      腾讯云代码分析(内部代号CodeDog)是集众多代码分析工具的云原生、分布式、高性能的代码综合分析跟踪管理平台,其主要功能是持续跟踪分析代码,观测项目代码质量,支撑团队传承代码文化。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档