前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >图层树和寄宿图 -- iOS Core Animation 系列一

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

作者头像
Charlie_W
发布2018-10-19 14:57:39
1.2K0
发布2018-10-19 14:57:39
举报
文章被收录于专栏:Charlie's RoadCharlie's Road

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

1.图层树

1.1 视图

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

1.2 CALayer

CALayer平时大家也很常见,比如简单的设置个圆角,或者边线等操作都会用到。CALayer类在概念上和UIView类似,也是一些被层级关系树管理的矩形块,也可以包含一些内容,并且管理子视图的位置。

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

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

1.3 平行的层级关系

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

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

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

对于简单的需求我们无需深入了解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的效果。代码如下:

代码语言:javascript
复制
@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
clipboard.png

虽然可以实现类似UIImageView的显示效果,但平常并不推荐使用这种方法。

2.1.2 contentGravity

上面示例的图片有点扁,因为我们设置的frame是个长方形,而图片本身是一个正方形。所以被挤压了。平时使用UIImageView时遇到类似情况,可以设置contentMode来解决。同样:

代码语言:javascript
复制
  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的代码可以替换如下:

代码语言:javascript
复制
  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
clipboard.png

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

代码语言:javascript
复制
layerView.layer.contentsScale = [UIScreen mainScreen].scale;

现在效果如下:

clipboard.png
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
clipboard.png

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

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

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

2.1.6 图片拼接代码示例:
代码语言:javascript
复制
@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
clipboard.png

本来原文是用四张不同的图做拼接,我只是展示下这种功能实现,所以偷懒只用了一张图片。如果有不解之处请看原文

2.1.7 contentsCenter

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

clipboard.png
clipboard.png

上图是借用原书的图

这效果看起来和UIImage里的resizableImageWithCapInsets:非常类似,它可以运用到任何寄宿图,包括在Core Graphics运行时绘制的图形。

clipboard.png
clipboard.png

同一图片使用不同的contentsCenter

contentsCenter使用起来也很方便,可以用代码:

代码语言:javascript
复制
layer.contentsCenter = CGRectMake(0.25, 0.25, 0.5, 0.5);

也可以在XIB里面设置:

clipboard.png
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会从下面这个代理方法请求一个寄宿图来展示:

代码语言:javascript
复制
- (void)displayLayer:(CALayer *)layer;

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

代码语言:javascript
复制
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;

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

2.2.1示例:

下面我们使用CALayerDelegate是做个示例。

代码语言:javascript
复制
- (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
clipboard.png

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

除非创建一个单独的图层,我们平时基本不会用到CALayerDelegate。因为UIView在创建时,会自动的吧图层的代理设置为自己,然后提供了一个-displayLayer:方法实现。


- 系列一完 -
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018-09-14,如有侵权请联系 cloudcommunity@tencent.com 删除

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.图层树
    • 1.1 视图
      • 1.2 CALayer
        • 1.3 平行的层级关系
        • 2.寄宿图
          • 2.1 contents属性
            • 2.1.1 示例
            • 2.1.2 contentGravity
            • 2.1.3 contentsScale
          • 2.1.4 maskToBounds
            • 2.1.5 contentsRect
            • 2.1.7 contentsCenter
          • 2.2 Custom Drawing
            • 2.2.1示例:
          • - 系列一完 -
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档