图层树和寄宿图 -- iOS Core Animation 系列一

本系列文章算是一系列读书笔记,想了解更多,请看原文html

1.图层树

1.1 视图

一个视图就是在屏幕上显示的一个矩形块(好比图片,文字或者视频),它可以拦截相似于鼠标点击或者触摸手势等用户输入。视图在层级关系中能够互相嵌套,一个视图能够管理它的全部子视图的位置。
在iOS中,全部的视图都是从UIView这个基类派生出来的。UIView能够处理触摸时间,支持Core Graphics绘图,能够仿射变换等等操做。ios

1.2 CALayer

CALayer平时你们也很常见,好比简单的设置个圆角,或者边线等操做都会用到。CALayer类在概念上和UIView相似,也是一些被层级关系树管理的矩形块,也能够包含一些内容,而且管理子视图的位置。git

UIView最大的区别是CALayer不能处理用户的操做交互缓存

CALayer不清楚具体的响应链,可是它提供了一些方法来判断是否某个触点在某个图层范围内。性能

1.3 平行的层级关系

每一个UIView都对应着一个CALayer,视图的职责是建立并管理这个图层,以确保党子视图在层级关系中添加或者被移除的时候,他们对应的图层也一样的在对应的层级关系树中有相同的操做。动画

真正用来在屏幕上显示的是图层(CALayer),UIView是对它的一个封装,提供一些交互触摸功能,和一些Core Animation底层的接口。atom

iO S提供UIViewCALayer两个平行的层级关系,应该也是为了解耦,作职责分离。 以便能适应 iOS 和 Mac OS 的系统。spa

对于简单的需求咱们无需深刻了解CALayer使用UIView就很方便灵活了。可是有时候咱们只使用UIView仍是会有些捉襟见肘的,CALayer暴露了一些UIView没有提供的功能:代理

  • 阴影、圆角、边框
  • 3D变换
  • 非矩形范围
  • 透明遮罩
  • 非线性动画

2.寄宿图

2.1 contents属性

CALayer有一个属性叫作contents,这个属性是id类型的,能够是任何类型的对象。也便是意味着在写代码的时候,能够给contents赋任何值(显示不显示是另外一回事)。只有赋CGImage的时候才能正确显示。指针

contents 这个奇怪的表现是由 Mac OS 的历史缘由形成的,由于在 Mac OS 系统上,这个属性对 CGImageNSImage 类型的值都起做用。可是在 iOS上,若是将 UIImage 的值赋给它,只能获得一个空白的图层。

事实上,真正赋值的类型应该是CGImageRef,这是一个指向CGImage结构的指针。UIImage有一个CGImage属性,它返回一个CGImageRef,可是这个值不能直接赋值给CALayercontents,由于CGImageRef不是一个真正的Cocoa对象,而是Core Foundation类型。

Core FoundationCocoa对象是不兼容的,能够经过bridged转换:

layer.contents = (__bridge id)image.CGImage;
2.1.1 示例

既然CALayercontents能够赋值各类类型,咱们能够尝试一下用CALayer实现UIImageView的效果。代码以下:

@implementation ViewController

- (void)viewDidLoad {
  [super viewDidLoad];
  self.view.backgroundColor = [UIColor lightGrayColor];
  
  UIView *layerView = [[UIView alloc] initWithFrame:CGRectMake(100, 200, 50, 100)];
  layerView.backgroundColor = [UIColor whiteColor];
  [self.view addSubview:layerView];
  
  
  UIImage *image = [UIImage imageNamed:@"test"];
  layerView.layer.contents = (__bridge id)image.CGImage;
}

运行一下,效果以下:

clipboard.png

虽然能够实现相似UIImageView的显示效果,但日常并不推荐使用这种方法。

2.1.2 contentGravity

上面示例的图片有点扁,由于咱们设置的frame是个长方形,而图片自己是一个正方形。因此被挤压了。平时使用UIImageView时遇到相似状况,能够设置contentMode来解决。一样:

layerView.contentMode = UIViewContentModeScaleAspectFill;

这样就能够解决了。

UIView大多数视觉相关的属性好比contentMode,对这些属性的操做实际上是对对应图层的操做。
CALayercontentMode对应的属性叫作contentsGravity,这是一个NSString类型,而UIKit部分是枚举。contentsGravity可选的常量值有以下:

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

contentMode同样, contentsGravity目的是决定内容在图层中怎么对齐,将上面设置contentMode的代码能够替换以下:

layerView.layer.contentsGravity = kCAGravityResizeAspectFill;

运行后的效果是一致的。

2.1.3 contentsScale

contentsScale属性定义了寄宿图的像素尺寸和视图大小的比例,默认状况下是一个1.0的浮点数。
contentsScale并非总会对寄宿图的效果有影响,由于contents设置了contentsGravity属性,致使常常设置了contentsScale却没反应。

若是单纯的想放大图层的 contents图片,可使用图层的 transformaffineTransform

contentsScale其实属于支持高分辨率屏幕机制的一部分,是用来判断在绘制图层的时候应该为寄宿图建立的空间大小,和须要显示的图片拉伸度(假设没有设置contentsGravity)。UIView有一个相似可是不多用的contentScaleFactor属性。
若是contentsScale设置为1.0,将会以每一个点1个像素绘制图片,若是2.0,则以每一个点2个像素绘制图片(这就是Retina屏)。
修改contentsScale并不会对咱们使用kCAGravityResizeAspectFill有影响,由于kCAGravityResizeAspectFill就是拉伸图片适应图层而已。可是若是把contentsGravity设置成kCAGravityCenter(这个值不会拉伸图片),变化见下图:

clipboard.png

如图所示,图片会变的有点大,并且有像素的颗粒感。由于CGImageUIImage不同,它没有拉伸的感念。用UIImage读取图片时,读取了高质量的Retina图片。但用CGImage设置的时候,拉伸的概念就被丢失了,不过能够手动设置contentsScale来作到一样效果:

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

如今效果以下:

clipboard.png
为了突出layerView的存在感,我把layerViewframe调整到CGRectMake(100, 200, 100, 150)

2.1.4 maskToBounds

看上面最新的运行图,发现图片超出了视图的边界。由于默认状况下,UIView仍会绘制超过边界的内容,在CALayer也不例外。
UIView有个clipsToBounds属性来决定是否显示超出边界的内容。CALayer对应的属性叫作maskToBounds,把它设置成YES就能够不显示超出部分的图片了。

2.1.5 contentsRect

CALayercontentsRect属性容许咱们在图层边框里显示寄宿图的一个子域。和boundsframe不一样,contentsRect不是按点来计算的。它使用单位坐标。单位坐标指定在0到1以前,是一个相对值(像素和点就是绝对值)。

默认的contentsRect{0, 0, 1, 1},意味着整个寄宿图默认都是课件的。若是指定小一点的矩形,图片就会被裁剪:

clipboard.png

上图设置的 contentsRect{0, 0, 0.5, 0.5}

事实上contentsRect设置一个负数的原点或者大于{1, 1}的尺寸也是能够的。这种状况下,最外面的像素会被拉伸。

contentsRect在 App 中最有趣的地方能够用做 image sprites(图片拼合)。图片拼合后能够打包到一张大图上一次载入,相比屡次载入不一样的图片,这样作的性能更优。

2.1.6 图片拼接代码示例:
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIView *view1;
@property (weak, nonatomic) IBOutlet UIView *view2;
@property (weak, nonatomic) IBOutlet UIView *view3;
@property (weak, nonatomic) IBOutlet UIView *view4;


@end

@implementation ViewController

- (void)addSpriteImage:(UIImage *)image withContentRect:(CGRect)rect toLayer:(CALayer *)layer //set image
{
  layer.contents = (__bridge id)image.CGImage;
  
  //scale contents to fit
  layer.contentsGravity = kCAGravityResizeAspect;
  
  //set contentsRect
  layer.contentsRect = rect;
}

- (void)viewDidLoad {
  [super viewDidLoad];
  
  UIImage *image = [UIImage imageNamed:@"test_1"];
  [self addSpriteImage:image withContentRect:CGRectMake(0, 0, 0.5, 0.5) toLayer:self.view1.layer];

  [self addSpriteImage:image withContentRect:CGRectMake(0.5, 0, 0.5, 0.5) toLayer:self.view2.layer];

  [self addSpriteImage:image withContentRect:CGRectMake(0, 0.5, 0.5, 0.5) toLayer:self.view3.layer];

  [self addSpriteImage:image withContentRect:CGRectMake(0.5, 0.5, 0.5, 0.5) toLayer:self.view4.layer];
}

运行的效果以下:

clipboard.png

原本原文是用四张不一样的图作拼接,我只是展现下这种功能实现,因此偷懒只用了一张图片。若是有不解之处请看原文

2.1.7 contentsCenter

contentsCenter看名字大部分人会误觉得是和位置有关,其实它是一个CGRect。它定义了一个苦丁的边框和在图层上可拉伸的区域。
默认状况下,contentsCenter{0, 0, 1, 1},意味着若是大小改变(contentsGravity),寄宿图会被均匀的拉伸。
假设咱们增长原点的值,并减少尺寸的值,例如将它变为{0.25, 0.25, 0.5, 0.5}将会在寄宿图周围留出一个边框。以下图:

clipboard.png
上图是借用原书的图

这效果看起来和UIImage里的resizableImageWithCapInsets:很是相似,它能够运用到任何寄宿图,包括在Core Graphics运行时绘制的图形。

clipboard.png
同一图片使用不一样的 contentsCenter

contentsCenter使用起来也很方便,能够用代码:

layer.contentsCenter = CGRectMake(0.25, 0.25, 0.5, 0.5);

也能够在XIB里面设置:

clipboard.png

2.2 Custom Drawing

除了给contents赋值CGImage来设置寄宿图以外,还能够直接用Core Graphics来绘制寄宿图。

-drawRect: 经过继承UIView来实现此方法进行自定义绘制。这个方法默认是没有被实现的。由于对于UIView来讲,寄宿图不是必须的。若是UIView检测到-drawRect:被调用,会自动给视图分配一个寄宿图。这个寄宿图的像素尺寸等于视图大小乘以contentsScale

若是你不须要寄宿图,不要写这个方法,会形成资源浪费,详细部分见 《内存恶鬼drawRect》

视图在屏幕上出现的时候-drawRect:会自动被调用。-drawRect:方法里面的代码利用Core Graphics绘制一个寄宿图,而后被缓存起来直到须要被更显(通常是调用了- setNeedDisplay方法)。

CALayer有一个可选的delegate属性<CALayerDelegate>,当CALayer须要内容的时候,会从这个delegate里面查询。
当须要被重绘时,CALayer会从下面这个代理方法请求一个寄宿图来展现:

- (void)displayLayer:(CALayer *)layer;

若是这个方法没有被实现,CALayer会尝试下面这个:

- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;

drawLayer:被调用以前,CALayer建立了一个合适尺寸的寄宿图(尺寸由boundscontentsScale决定)和一个Core Graphics的绘制上下文环境,并做为ctx传入。

2.2.1示例:

下面咱们使用CALayerDelegate是作个示例。

- (void)viewDidLoad {
  [super viewDidLoad];
  
  UIView *layerView = [[UIView alloc] initWithFrame:CGRectMake(100, 200, 150, 150)];
  layerView.backgroundColor = [UIColor lightGrayColor];
  [self.view addSubview:layerView];
  
  CALayer *blueLayer = [CALayer layer];
  blueLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f);
  blueLayer.backgroundColor = [UIColor blueColor].CGColor;
  
  blueLayer.delegate = self;
  
  blueLayer.contentsScale = [UIScreen mainScreen].scale;
  [layerView.layer addSublayer:blueLayer];
  //
  [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);
}

clipboard.png

  • blueLayer上显式调用了-display。由于当图层显示在屏幕上时,CALayer不会自动重绘,这和UIView不一样。须要手动调用。
  • 咱们没有调用masksToBounds。可是绘制的圆仍然被裁剪了。这是由于咱们在CALayerDelegate方法中,没有对超出边界歪的内容提供绘制支持。

除非建立一个单独的图层,咱们平时基本不会用到CALayerDelegate。由于UIView在建立时,会自动的吧图层的代理设置为本身,而后提供了一个-displayLayer:方法实现。


- 系列一完 -

相关文章
相关标签/搜索