Core Animation是一个复合引擎,它的职责就是尽可能快地组合屏幕上不同的可视内容,这个内容是被分解成独立的图层,存储在一个叫做图层树的体系之中。
1. UIView与CALayer的关系:
CALayer在概念上和UIView类似,同样也是一些被层级关系树管理的矩形块,同样也可以包含一些内容(像图片,文本或者背景色),管理子图层的位置。并且它们有一些方法和属性用来做动画和变换,但是CALayer不能响应事件。
UIView是基于CALayer的一种封装,每一个UIView都有一个CALayer实例的图层属性,也就是所谓的backing layer,视图的职责就是创建并管理这个图层,以确保当子视图在层级关系中添加或者被移除的时候,他们关联的图层也同样对应在层级关系树当中有相同的操作。UIView的尺寸样式以及内容都由CALayer提供。
大多数情况下我们都使用UIView而不使用CALayer的原因是,首先是CALayer不能响应事件,其次是CALayer不方便自动布局,最重要的是苹果已经基于CALayer封装好了UIView,我们在使用UIView高级接口的同时也能使用CALayer的底层功能。
既然这里提到了层级树,顺便提一下事件响应链:
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
UIResponder * next = [self nextResponder];
NSMutableString * prefix = @"-".mutableCopy;
while (next != nil) {
NSLog(@"%@%@", prefix, [next class]);
[prefix appendString: @"--"];
next = [next nextResponder];
}
}
2. CALayer内容相关的属性
-
contents
可以给CALayer的contents赋值
self.layerView.layer.contents = (__bridge id)image.CGImage;
-
contentGravity
可以类似设置UIView的contentModel一样,设置CALayer的contentGravity
self.layerView.layer.contentsGravity = kCAGravityResizeAspect;
contentGravity可以赋值的常量值如下
CA_EXTERN NSString * const kCAGravityCenter
CA_AVAILABLE_STARTING (10.5, 2.0, 9.0, 2.0);
CA_EXTERN NSString * const kCAGravityTop
CA_AVAILABLE_STARTING (10.5, 2.0, 9.0, 2.0);
CA_EXTERN NSString * const kCAGravityBottom
CA_AVAILABLE_STARTING (10.5, 2.0, 9.0, 2.0);
CA_EXTERN NSString * const kCAGravityLeft
CA_AVAILABLE_STARTING (10.5, 2.0, 9.0, 2.0);
CA_EXTERN NSString * const kCAGravityRight
CA_AVAILABLE_STARTING (10.5, 2.0, 9.0, 2.0);
CA_EXTERN NSString * const kCAGravityTopLeft
CA_AVAILABLE_STARTING (10.5, 2.0, 9.0, 2.0);
CA_EXTERN NSString * const kCAGravityTopRight
CA_AVAILABLE_STARTING (10.5, 2.0, 9.0, 2.0);
CA_EXTERN NSString * const kCAGravityBottomLeft
CA_AVAILABLE_STARTING (10.5, 2.0, 9.0, 2.0);
CA_EXTERN NSString * const kCAGravityBottomRight
CA_AVAILABLE_STARTING (10.5, 2.0, 9.0, 2.0);
CA_EXTERN NSString * const kCAGravityResize
CA_AVAILABLE_STARTING (10.5, 2.0, 9.0, 2.0);
CA_EXTERN NSString * const kCAGravityResizeAspect
CA_AVAILABLE_STARTING (10.5, 2.0, 9.0, 2.0);
CA_EXTERN NSString * const kCAGravityResizeAspectFill
CA_AVAILABLE_STARTING (10.5, 2.0, 9.0, 2.0);
-
contentsScale
CALayer的contentsScale属性可以用以适配retina屏幕,该属性值默认为1.0,将会以每个点1个像素绘制图片,如果设置为2.0,则会以每个点2个像素绘制图片,这就是我们熟知的Retina屏幕。
UIImage *image = [UIImage imageNamed:@"doll"];
UIView *view = [[UIView alloc] initWithFrame:self.view.bounds];
view.layer.contents = (__bridge id _Nullable)(image.CGImage);
view.layer.contentsGravity = kCAGravityCenter;
[self.view addSubview:view];
运行结果:
可以看到显示出来的图片有像素颗粒感,在上面代码的基础上再加上一句
view.layer.contentsScale = [UIScreen mainScreen].scale;
运行结果:
-
maskToBounds
类似UIView的clipsToBounds,CALayer有一个属性叫maskToBounds
self.layerView.layer.masksToBounds = YES;
-
contentsRect
CALayer的contentsRect属性可以用于图片拼合
举个栗子,有如下图片:
我们可以利用contentsRect属性来分别单独显示三个娃娃:
#import "ViewController.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIView *leftTopImage;
@property (weak, nonatomic) IBOutlet UIView *rightTopImage;
@property (weak, nonatomic) IBOutlet UIView *leftBottomImage;
@end
@implementation ViewController
-(void)viewDidLoad {
[super viewDidLoad];
UIImage *image = [UIImage imageNamed:@"doll"];
self.leftTopImage.layer.contents = (__bridge id _Nullable)(image.CGImage);
self.leftTopImage.layer.contentsRect = CGRectMake(0.0f, 0.0f, 0.5f, 0.5f);
self.rightTopImage.layer.contents = (__bridge id _Nullable)(image.CGImage);
self.rightTopImage.layer.contentsRect = CGRectMake(0.5f, 0.0f, 0.5f, 0.5f);
self.leftBottomImage.layer.contents = (__bridge id _Nullable)(image.CGImage);
self.leftBottomImage.layer.contentsRect = CGRectMake(0.0f, 0.5f, 0.5f, 0.5f);
}
@end
运行结果:
拼合不仅给app提供了一个整洁的载入方式,还有效地提高了载入性能(单张大图比多张小图载入地更快),但是如果有手动安排的话,他们还是有一些不方便的,如果你需要在一个已经创建好的品和图上做一些尺寸上的修改或者其他变动,无疑是比较麻烦的。
Mac上有一些商业软件可以为你自动拼合图片,这些工具自动生成一个包含拼合后的坐标的XML或者plist文件,拼合图片的使用大大简化。这个文件可以和图片一同载入,并给每个拼合的图层设置contentsRect,这样开发者就不用手动写代码来摆放位置了。
-
contentsCenter
CALayer的另一个属性contentsCenter,有点类似于UIImage里的-resizableImageWithCapInsets: 方法,楼主浅薄,实在不懂如何描述,这里有篇文章讲得很清楚:iOS: 关于CALayer的contentsCenter属性
3. CALayer位置坐标相关的属性
对应于UIView的frame、bounds、center,CALayer有frame、bounds、position,访问UIView的这三个属性,返回的其实就是CALayer对应的这三个值。但是CALayer另外还有一个比较难于理解的属性anchorPoint。
-
anchorPoint
anchorPoint决定了CALayer的那个点处于position的位置,其作用效果如下图:
-
坐标转换
CALayer也有用于在不同坐标系中转换坐标的函数:
-(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;
-
zPosition
修改zPosition:
#import "ViewController.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIView *blueView;
@property (weak, nonatomic) IBOutlet UIView *orangeView;
@end
@implementation ViewController
-(void)viewDidLoad {
[super viewDidLoad];
self.blueView.layer.zPosition = 1.0f;
}
@end```
运行结果:
![Paste_Image.png](http://upload-images.jianshu.io/upload_images/1396900-5790571a630184af.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
zPosition属性虽然可以改变图层的显示顺序,但是不能改变图层在图层树中的顺序
- #####CALayer事件处理
CALayer并不关心任何响应链事件,所以不能直接处理触摸事件或者手势。但是它有一系列的方法帮你处理事件:-containsPoint:和-hitTest:。
-containsPoint:接受一个在本图层坐标系下的CGPoint,如果这个点在图层frame范围内就返回YES。举个栗子:
import "ViewController.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIView *whiteView;
@property (weak, nonatomic) IBOutlet UIView *orangeView;
@end
@implementation ViewController
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
UITouch *touch = touches.anyObject;
CGPoint originPoint = [touch locationInView:self.view];
CGPoint thePoint = [self.whiteView.layer convertPoint:originPoint fromLayer:self.view.layer];
BOOL containsResult = [self.whiteView.layer containsPoint:thePoint];
if (containsResult) {
thePoint = [self.orangeView.layer convertPoint:thePoint fromLayer:self.whiteView.layer];
containsResult = [self.orangeView.layer containsPoint:thePoint];
UIAlertController *alert;
if (containsResult) {
alert = [UIAlertController alertControllerWithTitle:nil message:@"inside orange layer" preferredStyle:UIAlertControllerStyleAlert];
}else{
alert = [UIAlertController alertControllerWithTitle:nil message:@"inside white layer" preferredStyle:UIAlertControllerStyleAlert];
}
[alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleCancel handler:nil]];
[self presentViewController:alert animated:YES completion:nil];
}
}
@end
运行结果:
![Paste.gif](http://upload-images.jianshu.io/upload_images/1396900-5dbead9b42769221.gif?imageMogr2/auto-orient/strip)
-hitTest:方法同样接受一个CGPoint类型参数,而不是BOOL类型,它返回图层本身,或者包含这个坐标点的叶子节点图层,如果这个点在最外面图层的范围之外,则返回nil。
下面这段代码实现了上图一样的效果:
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
UITouch *touch = touches.anyObject;
CGPoint originPoint = [touch locationInView:self.view];
CALayer *layer = [self.whiteView.layer hitTest:originPoint];
UIAlertController *alert;
if (layer == self.orangeView.layer) {
alert = [UIAlertController alertControllerWithTitle:nil message:@"inside orange layer" preferredStyle:UIAlertControllerStyleAlert];
}else{
alert = [UIAlertController alertControllerWithTitle:nil message:@"inside white layer" preferredStyle:UIAlertControllerStyleAlert];
}
[alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleCancel handler:nil]];
[self presentViewController:alert animated:YES completion:nil];
}
---
#4. CALayer视觉相关的属性
- #####conrnerRadius
CALayer有一个叫做conrnerRadius的属性控制着图层角的曲率。它是一个浮点数,默认为0(为0的时候就是直角),但是你可以把它设置成任意值。默认情况下,这个曲率值只影响背景颜色而不影响背景图片或是子图层。不过,如果把masksToBounds设置成YES的话,图层里面的所有东西都会被截取。
Interface Build布局如下:
![Paste_Image.png](http://upload-images.jianshu.io/upload_images/1396900-70164d489a904bbb.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
添加如下代码:
import "ViewController.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIView *topWhiteView;
@property (weak, nonatomic) IBOutlet UIView *bottomWhiteView;
@end
@implementation ViewController
-(void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
self.topWhiteView.layer.cornerRadius = 20.0f;
self.bottomWhiteView.layer.cornerRadius = 20.0f;
self.bottomWhiteView.layer.masksToBounds = YES;
}
@end
运行效果:
![Paste_Image.png](http://upload-images.jianshu.io/upload_images/1396900-5fe67d997034d76d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
- #####borderWidth和borderColor
CALayer另外两个非常有用属性就是borderWidth和borderColor。二者共同定义了图层边的绘制样式。这条线(也被称作stroke)沿着图层的bounds绘制,同时也包含图层的角。在上面代码的基础上再加上几句:
self.topWhiteView.layer.borderWidth = 5.0f;
self.bottomWhiteView.layer.borderWidth = 5.0f;
self.topWhiteView.layer.borderColor = [UIColor yellowColor].CGColor;
self.bottomWhiteView.layer.borderColor = [UIColor yellowColor].CGColor;
运行效果:
![Paste_Image.png](http://upload-images.jianshu.io/upload_images/1396900-1f0d94448d3d8080.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
- #####阴影效果
CALayer还有一系列属性用来绘制阴影:
/** Shadow properties. */
/ The color of the shadow. Defaults to opaque black. Colors created
- from patterns are currently NOT supported. Animatable. /
@property(nullable) CGColorRef shadowColor;
/ The opacity of the shadow. Defaults to 0. Specifying a value outside the - [0,1] range will give undefined results. Animatable. /
@property float shadowOpacity;
/ The shadow offset. Defaults to (0, -3). Animatable. /
@property CGSize shadowOffset;
/ The blur radius used to create the shadow. Defaults to 3. Animatable. /
@property CGFloat shadowRadius;
/ When non-null this path defines the outline used to construct the - layer's shadow instead of using the layer's composited alpha
- channel. The path is rendered using the non-zero winding rule.
- Specifying the path explicitly using this property will usually
- improve rendering performance, as will sharing the same path
- reference across multiple layers. Upon assignment the path is copied.
- Defaults to null. Animatable. */
@property(nullable) CGPathRef shadowPath;
在前面代码的基础上再加上几句:
self.topWhiteView.layer.shadowColor = [UIColor blackColor].CGColor;
self.bottomWhiteView.layer.shadowColor = [UIColor blackColor].CGColor;
self.topWhiteView.layer.shadowOpacity = 0.5f;
self.bottomWhiteView.layer.shadowOpacity = 0.5f;
运行效果:
![Paste_Image.png](http://upload-images.jianshu.io/upload_images/1396900-01d2b9a2d8292937.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
发现两个问题:
1、topWhiteView有阴影,但是阴影在视图的上方
2、而bottomWhiteView干脆根本没有阴影
第一个问题的原因在于这个属性:
/* The shadow offset. Defaults to (0, -3). Animatable. */
@property CGSize shadowOffset;
CALayer的阴影偏移量默认为(0, -3),所以阴影出现在了视图的上方
第二个问题的原因在于这句代码:
self.bottomWhiteView.layer.masksToBounds = YES;
如果你开启了masksToBounds属性,所有从图层中突出来的内容都会被才剪掉,一个简单的解决办法就是在bottomWhiteView的下面再插入一个view来充当阴影视图。
修改之前的代码,最终代码如下:
import "ViewController.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIView *topWhiteView;
@property (weak, nonatomic) IBOutlet UIView *bottomWhiteView;
@property (weak, nonatomic) IBOutlet UIView *bottomBackView;
@end
@implementation ViewController
-(void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
self.bottomWhiteView.layer.masksToBounds = YES;
[self configLayer:self.topWhiteView.layer];
[self configLayer:self.bottomWhiteView.layer];
[self configLayer:self.bottomBackView.layer];
}
-(void)configLayer:(CALayer *)aLayer{
aLayer.cornerRadius = 20.0f; //圆角
aLayer.borderWidth = 5.0f; //边框线宽
aLayer.borderColor = [UIColor yellowColor].CGColor; //边框颜色
aLayer.shadowColor = [UIColor blackColor].CGColor; //阴影颜色
aLayer.shadowOpacity = 0.5f; //阴影透明度
aLayer.shadowOffset = CGSizeMake(5.0f, 5.0f); //阴影偏移量
}
@end
运行效果:
![Paste_Image.png](http://upload-images.jianshu.io/upload_images/1396900-36c508fe929347cb.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
- #####shadowRadius
shadowRadius属性可以控制CALayer阴影的模糊度,在viewDidLoad方法的最后加上一句:
self.topWhiteView.layer.shadowRadius = 10.0f;
运行结果:
![Paste_Image.png](http://upload-images.jianshu.io/upload_images/1396900-4f17b30a21551e4b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
- #####shadowPath
shadowPath可以为CALayer指定特定的阴影路径。
举个栗子:
import "ViewController.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIView *orangeView;
@property (weak, nonatomic) IBOutlet UIView *cyanView;
@end
@implementation ViewController
-(void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
[self configLayer:self.orangeView.layer];
[self configLayer:self.cyanView.layer];
//create a square shadow
CGMutablePathRef squarePath = CGPathCreateMutable();
CGPathAddRect(squarePath, NULL, self.cyanView.bounds);
self.cyanView.layer.shadowPath = squarePath; CGPathRelease(squarePath);
//create a circular shadow
CGMutablePathRef circlePath = CGPathCreateMutable();
CGPathAddEllipseInRect(circlePath, NULL, self.orangeView.bounds);
self.orangeView.layer.shadowPath = circlePath; CGPathRelease(circlePath);
}
-(void)configLayer:(CALayer *)aLayer{
aLayer.shadowOpacity = 0.5f;
aLayer.cornerRadius = aLayer.bounds.size.width / 2.0f;
aLayer.shadowOffset = CGSizeMake(0.0f, 0.0f);
aLayer.shadowRadius = 5.0f;
}
@end
运行效果:
![Paste_Image.png](http://upload-images.jianshu.io/upload_images/1396900-2f314b7d2e5536aa.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
- #####mask
mask图层定义了父图层的部分可见区域,mask图层的Color属性是无关紧要的,真正重要的是图层的轮廓。mask属性就像是一个饼干切割机,mask图层实心的部分会被保留下来,其他的则会被抛弃。
举个栗子:
Interface Build布局如下:
![Paste_Image.png](http://upload-images.jianshu.io/upload_images/1396900-6e6dfbfbd4c3c5b5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
使用下面这个小雪人来充当图层蒙版:
![3.png](http://upload-images.jianshu.io/upload_images/1396900-abaada4d571f838d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
代码如下:
import "ViewController.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@end
@implementation ViewController
-(void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
//create mask layer
CALayer *maskLayer = [CALayer layer];
maskLayer.frame = self.imageView.bounds;
UIImage *maskImage = [UIImage imageNamed:@"3"];
maskLayer.contents = (__bridge id)maskImage.CGImage;
maskLayer.contentsGravity = kCAGravityCenter;
maskLayer.contentsScale = [UIScreen mainScreen].scale;
//apply mask to image layer
self.imageView.layer.mask = maskLayer;
}
@end
运行效果:
![Paste_Image.png](http://upload-images.jianshu.io/upload_images/1396900-7b49885e8a7f1014.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
一个利用图层蒙版实现的有趣动画:[iOS-maskLayer实现的炫酷动画](http://www.jianshu.com/p/a52ba15edc35)
[Responder Chain](http://blog.csdn.net/chun799/article/details/8223612)、[Core Animation](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CoreAnimation_guide/Introduction/Introduction.html)、[UIView与CALayer的关系](http://www.cocoachina.com/ios/20150828/13244.html)、[[iOS: 关于CALayer的contentsCenter属性](https://www.mgenware.com/blog/?p=489)](https://www.mgenware.com/blog/?p=489)、[iOS-maskLayer实现的炫酷动画](http://www.jianshu.com/p/a52ba15edc35)