[译] Core Animation 编程指南 - 构建图层层级结构

720 阅读12分钟

大多数时间里,使用图层的最好方式就是和视图对象一起使用。然而,有时你可能需要通过向视图中添加额外的图层对象来增强视图层次结构。当图层能够提供更好的性能或者仅用视图很难实现的特性时,你可能会使用图层。在这些情况下,你需要知道如何管理你创建的图层的层级结构。

重要:在 OS X v10.8 及以后的版本,推荐尽可能少的使用图层层级结构,尽量使用图层支持视图。该版本引进的图层重绘协议允许你自定义图层支持视图的行为,并且仍然能获得之前单独使用图层时的性能。

将图层分配到图层层级结构中

图层层级结构在很多方面都和视图层级结构相似。你可以将一个图层嵌入到另一个图层中,给两个图层创建父-子关系。嵌入的图层称为子图层(sublayer),被嵌入的图层称为父图层(superlayer)。这种父子关系影响子图层的多个方面。例如,它的内容在父图层的内容上面,它的位置与它父图层的坐标系相关,应用在父图层上的任何形变也会影响子图层。

添加、插入、移除子图层

每个图层对象都有添加、插入、移除子图层的对象方法。表 4-1 概述了这些方法和它们的行为。

表 4-1 修改图层层级结构的方法

行为 方法 描述
添加图层 addSublayer: 在当前图层上添加一个新的子图层对象。子图层被添加到当前图层的子图层列表的尾部。这将导致子图层出现在 zPosition 属性中具有相同值的任何同级图层的顶部。
插入图层 insertSublayer:above: insertSublayer:atIndex: insertSublayer:below: 将子图层添加到具体索引的位置或者与另一个子图层相关的位置。当在另一个子图层上面或者下面插入时,你只是指定了子图层在子图层数组中的位置。图层实际的视觉效果主要由它们的 zPosition 属性的值来决定,其次才由它们在子图层数组的位置决定。
移除图层 removeFromSuperlayer 在父图层上移除子图层。
替换图层 replaceSublayer:with: 将子图层替换为另一个。如果你插入的子视图已经添加在别的图层层级结构上,它会先从那个层级结构移除。

你可以在自己创建图层对象时使用上述方法。你不应该使用上述方法来分配属于图层支持视图的图层。但,图层支持视图可以当做你自己创建的单独图层的父图层。

子图层位置和尺寸的调整

当添加和插入子图层时,在图层显示在屏幕上之前,你必须设置图层的位置和尺寸。在将图层添加到图层层级结构之后,你也可以修改子图层的位置和尺寸。但你应该养成在创建图层时就指定图层的位置和尺寸的值得习惯。

你可以使用 bounds 属性来设置子图层的尺寸,使用 position 属性来设置它在父图层中的位置。矩形边界的起点永远是(0, 0),尺寸则是你给图层以点为单位的任何尺寸。position 属性的值是相对于图层的锚点来定义的,它默认是图层的中心点。如果你不给这些属性赋值,Core Animation 会将图层的宽高初始值设为0,位置的初始值设为(0, 0)。

myLayer.bounds = CGRectMake(0, 0, 100, 100);
myLayer.position = CGPointMake(200, 200);

重要:始终使用整数表示图层的宽度和高度。

图层层级结构如何影响动画

一些父图层的属性可以影响应用在子图层上的任何动画的行为。其中一个属性就是 speed,它是动画速度的乘数。该属性的值默认为1.0,但将它改为2.0会导致动画按起始速度的两倍速来运行,结果就是会提前一半的时间结束。该属性不仅影响修改的这一个图层,它也会影响它的子图层。子图层的速度也会加倍。如果父图层和子图层的速度都为2.0,那子图层上的动画会以原始速度的4倍速度来执行。

大多数其他图层更改会以可预测的方式影响任何包含的子图层。例如,对图层应用一个旋转形变会导致该图层的所有子图层都会旋转。类似的,改变图层的透明度也会影响子图层的透明度。图层大小的更改遵循的规则,请参见调整图层层级结构的布局

调整图层层级结构的布局

Core Animation 支持多个选项,用来调整子图层的位置和尺寸以响应父图层的改变。在 iOS,图层支持视图的普遍使用使得图层层次结构的创建变得不那么重要;仅支持手动布局更新。在 OS X,其他的可用选项会使图层层级管理变得更加容易。

图层级布局仅与你使用你创建的单独图层对象来构建图层层级结构时相关。如果你的 app 图层都是和视图联合的,使用基于视图的布局支持去更新视图的尺寸和位置来响应改变。

在 OS X 上使用约束去管理你的图层层级结构

约束是你使用图层和父图层或同级图层的一系列详细的关系来制定图层的尺寸和位置。定义约束需遵循一下步骤:

  1. 创建一个或多个 CAConstraint 对象。使用这些对象去定义约束参数。
  2. 将约束对象添加到属性修改的图层上。
  3. 获取共享的 CAConstraintLayoutManager 对象,将它赋值给最近的父图层。

图4-1 展示了你可以使用去定义约束的属性,和它影响了图层的方面。你可以使用约束,根据中点边缘相对于另一图层的位置来更改图层的位置。你也可以使用它们改变图层的尺寸。你所做的改变可以与父图层成比例,或者和同级图层成比例。你甚至可以将缩放因子或常量添加到结果变化中。这种额外的灵活性使得使用一组简单的规则,即可非常精确地控制层的大小和位置。

图 4-1 约束布局管理属性

每个约束对象包含两个图层在同一轴线上的几何关系。每个轴最多可以分配两个约束对象,正是这两个约束决定了哪个属性是可变的。例如,如果为图层的左边缘和右边缘指定约束,图层的大小会发生变化。如果为图层的左边缘和宽度指定约束,图层右边缘的位置会发生变化。如果为图层的一条边指定单个约束,Core Animation 会创建一个隐式约束,使图层的大小保持固定在给定的尺寸中。

当创建约束时,你必须始终指明这三方面的信息:

  • 要约束的图层的哪个方面
  • 用哪个图层当作参考图层
  • 在比较中使用的参考图层的哪个方面

例4-1 展示了一个约束的例子,该约束表示图层的垂直中心点与父图层的垂直中心点相等。当用父视图做参考图层时,使用 superlayer 字符串。使用它不需要有指向该图层的指针或知道该图层的名称。它也允许你改变父图层,如果你改变父图层,约束会自动应用到新的父图层上。(当创建于同级图层相关联的约束时,你必须使用它的 name 属性来识别同级图层。)

例 4-1 定义简单的约束

[myLayer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMidY
                                                 relativeTo:@"superlayer"
                                                  attribute:kCAConstraintMidY]];

在运行时应用约束,你必须将共享的 CAConstraintLayoutManager 对象附加给最近的父图层。每个图层有责任去管理子图层的布局。将布局管理器分配给父级会告诉 Core Animation 应用其子级定义的约束。布局管理对象自动应用约束。将它赋值给父图层之后,你就不必告诉它去更新布局。

看图 4-2,去了解约束是如何工作的。在该例中,设计要求 layerA 的宽高保持不变,并且 layerA 要位于图图层的中心点。另外, layerB 的宽度必须和 layerA 的宽度相等,layerB 的上边界要距离 layerA 的下边界 10 个点,layerB 的下边界要比父图层的下边界高 10 个点。例 4-2 展示了该例中的代码是如何实现的。

图 4-2 基于布局的约束示例

例 4-2 设置图层的约束

// 创建设置父图层的布局管理
theLayer.layoutManager = [CAConstraintLayoutManager layoutManager];
 
// 创建第一个子图层
CALayer *layerA = [CALayer layer];
layerA.name = @"layerA";
layerA.bounds = CGRectMake(0.0,0.0,100.0,25.0);
layerA.borderWidth = 2.0;
 
// 保持 layerA 的中点与父图层的中点相同
[layerA addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMidY
                                                 relativeTo:@"superlayer"
                                                  attribute:kCAConstraintMidY]];
[layerA addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMidX
                                                 relativeTo:@"superlayer"
                                                  attribute:kCAConstraintMidX]];
[theLayer addSublayer:layerA];
 
// 创建第二个子图层
CALayer *layerB = [CALayer layer];
layerB.name = @"layerB";
layerB.borderWidth = 2.0;
 
// 使 layerB 的宽度与 layerA 的相同
[layerB addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintWidth
                                                 relativeTo:@"layerA"
                                                  attribute:kCAConstraintWidth]];
 
//  layerB 和 layerA 水平居中
[layerB addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMidX
                                                 relativeTo:@"layerA"
                                                  attribute:kCAConstraintMidX]];
 
// layerB 的上边界距 layerA 的下边界 10 个点
[layerB addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMaxY
                                                 relativeTo:@"layerA"
                                                  attribute:kCAConstraintMinY
                                                     offset:-10.0]];
 
// `layerB` 的下边界比父图层的下边界高 10 个点
[layerB addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMinY
                                                 relativeTo:@"superlayer"
                                                  attribute:kCAConstraintMinY
                                                     offset:+10.0]];
 
[theLayer addSublayer:layerB];

上述代码中有一个有意思的事,它没有显式的设置 layerB 的尺寸。由于定义了约束,每次布局更新,layerB 的宽高都会自动设置。因此,再设置它的宽高是没必要的。

警告:当创建约束时,不要在约束中创建循环引用。循环约束使得计算所需的布局信息变得不可能。当遇到这种循环引用时,布局行为是未定义的。

OS X 图层层级结构设置自动调整尺寸规则

在 OS X 中,自动调整尺寸规则是另一种调整图层尺寸和位置的方法。使用自动调整尺寸规则,你可以指定图层的边缘是应该与父图层的相应边缘保持固定距离还是可变距离。类似的,你也可以指定图层的宽高是固定的还是可变的。指定的关系始终是关于图层和其父图层的。你不能将自动调整尺寸规则应用于同级图层。

设置图层的自动调整尺寸规则,你必须给图层的 autoresizingMask 属性赋值合适的常量。图层默认是有固定宽高的。在布局期间,图层精确的尺寸和位置是由 Core Animation 自动为你计算的,这是包含基于很多因素的一系列复杂计算。Core Animation 在你的代理执行任何手动布局更新之前应用自动调整尺寸行为,所以,你可以在需要的时候使用代理去微调自动调整尺寸布局的结果。

手动布局你的图层层次结构

在 iOS 和 OS X 上,你可以通过实现父图层的代理对象的 layoutSublayersOfLayer: 方法来手动处理布局。你可以使用该方法调整图层中任意子图层的尺寸和位置。当进行手动布局更新时,由你来执行必要的计算来定位每个子层

如果你实现一个自定义的图层子类,你的子类可以重写 layoutSublayers 方法,使用该方法(不是代理)去处理任何布局任务。你应该仅在你想完整的控制你自定义图层类的子图层的位置时,才重写此方法。替换默认的实现会阻止 Core Animation 在 OS X 上应用约束或自动调整尺寸规则。

子图层和剪切

不想视图,父图层不会自动剪切子图层的内容,这会导致子图层内容会延伸到父图层外面。换言之,父图层默认允许子图层完整的展示它的内容。然而,你可以通过将图层的 masksToBounds 属性设置为 YES 来将剪切功能生效,

图层的剪切蒙版的形状包含图层的圆角,如果指定圆角的话。图 4-3 展示了图层 masksToBounds 属性如何影响图层圆角。当此属性设置为 NO 时,子类会完整的显示其内容,即使它们内容超出了父图层的边界。将此属性的值改为 YES 会导致它们的内容被剪切。

图 4-3 剪切超出父图层边界的子图层的内容

转换图层之间的坐标值

有时,你可能需要将一个图层的坐标值转换到同屏幕上不同图层上的坐标值。CALayer 类提供一系列简单的转换方法来达到你的目的:

  • convertPoint:fromLayer:
  • convertPoint:toLayer:
  • convertRect:fromLayer:
  • convertRect:toLayer:

除了转换点和矩形值之外,你也可以通过 convertTime:fromLayer:convertTime:toLayer: 方法来转换两个图层之间的时间值。每个图层都定义自己的时空,使用自己的时空去同步动画的开始和结束时间和系统的其余部分。时空默认是同步的;但是,如果你改变一系列图层的动画速度,这些图层的时空也会相应改变。你可以使用时间转换方法来考虑任何此类因素,来确保两层的时间同步。

最后