iOS核心动画高级技巧-2 - 图1

3. 图层几何学

图层几何学

不熟悉几何学的人就不要来这里了 —柏拉图学院入口的签名

在第二章里面,我们介绍了图层背后的图片,和一些控制图层坐标和旋转的属性。在这一章中,我们将要看一看图层内部是如何根据父图层和兄弟图层来控制位置和尺寸的。另外我们也会涉及如何管理图层的几何结构,以及它是如何被自动调整和自动布局影响的。

3.1 布局

布局

UIView有三个比较重要的布局属性:frameboundscenterCALayer对应地叫做frameboundsposition。为了能清楚区分,图层用了“position”,视图用了“center”,但是他们都代表同样的值。

frame代表了图层的外部坐标(也就是在父图层上占据的空间),bounds是内部坐标({0, 0}通常是图层的左上角),centerposition都代表了相对于父图层anchorPoint所在的位置。anchorPoint的属性将会在后续介绍到,现在把它想成图层的中心点就好了。图3.1显示了这些属性是如何相互依赖的。

图3.2 旋转一个视图或者图层之后的frame属性

3.2 锚点

锚点

之前提到过,视图的center属性和图层的position属性都指定了anchorPoint相对于父图层的位置。图层的anchorPoint通过position来控制它的frame的位置,你可以认为anchorPoint是用来移动图层的把柄。

默认来说,anchorPoint位于图层的中点,所以图层的将会以这个点为中心放置。anchorPoint属性并没有被UIView接口暴露出来,这也是视图的position属性被叫做“center”的原因。但是图层的anchorPoint可以被移动,比如你可以把它置于图层frame的左上角,于是图层的内容将会向右下角的position方向移动(图3.3),而不是居中了。

图3.4 组成钟面和钟表的四张图片

闹钟的组件通过IB来排列(图3.5),这些图片视图嵌套在一个容器视图之内,并且自动调整和自动布局都被禁用了。这是因为自动调整会影响到视图的frame,而根据图3.2的演示,当视图旋转的时候,frame是会发生改变的,这将会导致一些布局上的失灵。

一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS交流群:1012951431, 分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!希望帮助开发者少走弯路。

我们用NSTimer来更新闹钟,使用视图的transform属性来旋转钟表(如果你对这个属性不太熟悉,不要着急,我们将会在第5章“变换”当中详细说明),具体代码见清单3.1

图3.6 钟面,和不对齐的钟指针

你也许会认为可以在Interface Builder当中调整指针图片的位置来解决,但其实并不能达到目的,因为如果不放在钟面中间的话,同样不能正确的旋转。

也许在图片末尾添加一个透明空间也是个解决方案,但这样会让图片变大,也会消耗更多的内存,这样并不优雅。

更好的方案是使用anchorPoint属性,我们来在-viewDidLoad方法中添加几行代码来给每个钟指针的anchorPoint做一些平移(清单3.2),图3.7显示了正确的结果。

清单3.2

  1. - (void)viewDidLoad
  2. {
  3. [super viewDidLoad];
  4. // adjust anchor points
  5. self.secondHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f);
  6. self.minuteHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f);
  7. self.hourHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f);
  8. // start timer
  9. }

3.3 坐标系

坐标系

和视图一样,图层在图层树当中也是相对于父图层按层级关系放置,一个图层的position依赖于它父图层的bounds,如果父图层发生了移动,它的所有子图层也会跟着移动。

这样对于放置图层会更加方便,因为你可以通过移动根图层来将它的子图层作为一个整体来移动,但是有时候你需要知道一个图层的绝对位置,或者是相对于另一个图层的位置,而不是它当前父图层的位置。

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

  1. - (CGPoint)convertPoint:(CGPoint)point fromLayer:(CALayer *)layer;
  2. - (CGPoint)convertPoint:(CGPoint)point toLayer:(CALayer *)layer;
  3. - (CGRect)convertRect:(CGRect)rect fromLayer:(CALayer *)layer;
  4. - (CGRect)convertRect:(CGRect)rect toLayer:(CALayer *)layer;

这些方法可以把定义在一个图层坐标系下的点或者矩形转换成另一个图层坐标系下的点或者矩形.

翻转的几何结构

常规说来,在iOS上,一个图层的position位于父图层的左上角,但是在Mac OS上,通常是位于左下角。Core Animation可以通过geometryFlipped属性来适配这两种情况,它决定了一个图层的坐标是否相对于父图层垂直翻转,是一个BOOL类型。在iOS上通过设置它为YES意味着它的子图层将会被垂直翻转,也就是将会沿着底部排版而不是通常的顶部(它的所有子图层也同理,除非把它们的geometryFlipped属性也设为YES)。

Z坐标轴

UIView严格的二维坐标系不同,CALayer存在于一个三维空间当中。除了我们已经讨论过的positionanchorPoint属性之外,CALayer还有另外两个属性,zPositionanchorPointZ,二者都是在Z轴上描述图层位置的浮点类型。

注意这里并没有更深的属性来描述由宽和高做成的bounds了,图层是一个完全扁平的对象,你可以把它们想象成类似于一页二维的坚硬的纸片,用胶水粘成一个空洞,就像三维结构的折纸一样。

zPosition属性在大多数情况下其实并不常用。在第五章,我们将会涉及CATransform3D,你会知道如何在三维空间移动和旋转图层,除了做变换之外,zPosition最实用的功能就是改变图层的显示顺序了。

通常,图层是根据它们子图层的sublayers出现的顺序来类绘制的,这就是所谓的画家的算法—就像一个画家在墙上作画—后被绘制上的图层将会遮盖住之前的图层,但是通过增加图层的zPosition,就可以把图层向相机方向前置,于是它就在所有其他图层的前面了(或者至少是小于它的zPosition值的图层的前面)。

这里所谓的“相机”实际上是相对于用户是视角,这里和iPhone背后的内置相机没任何关系。

图3.8显示了在Interface Builder内的一对视图,正如你所见,首先出现在视图层级绿色的视图被绘制在红色视图的后面。

图3.9 绿色视图被绘制在红色视图的前面

3.4 Hit Testing

Hit Testing

第一章“图层树”证实了最好使用图层相关视图,而不是创建独立的图层关系。其中一个原因就是要处理额外复杂的触摸事件。

CALayer并不关心任何响应链事件,所以不能直接处理触摸事件或者手势。但是它有一系列的方法帮你处理事件:-containsPoint:-hitTest:

-containsPoint:接受一个在本图层坐标系下的CGPoint,如果这个点在图层frame范围内就返回YES。如清单3.4所示第一章的项目的另一个合适的版本,也就是使用-containsPoint:方法来判断到底是白色还是蓝色的图层被触摸了 (图3.10)。这需要把触摸坐标转换成每个图层坐标系下的坐标,结果很不方便。

清单3.4 使用containsPoint判断被点击的图层

  1. @interface ViewController ()
  2. @property (nonatomic, weak) IBOutlet UIView *layerView;
  3. @property (nonatomic, weak) CALayer *blueLayer;
  4. @end
  5. @implementation ViewController
  6. - (void)viewDidLoad
  7. {
  8. [super viewDidLoad];
  9. //create sublayer
  10. self.blueLayer = [CALayer layer];
  11. self.blueLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f);
  12. self.blueLayer.backgroundColor = [UIColor blueColor].CGColor;
  13. //add it to our view
  14. [self.layerView.layer addSublayer:self.blueLayer];
  15. }
  16. - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
  17. {
  18. //get touch position relative to main view
  19. CGPoint point = [[touches anyObject] locationInView:self.view];
  20. //convert point to the white layer's coordinates
  21. point = [self.layerView.layer convertPoint:point fromLayer:self.view.layer];
  22. //get layer using containsPoint:
  23. if ([self.layerView.layer containsPoint:point]) {
  24. //convert point to blueLayer’s coordinates
  25. point = [self.blueLayer convertPoint:point fromLayer:self.layerView.layer];
  26. if ([self.blueLayer containsPoint:point]) {
  27. [[[UIAlertView alloc] initWithTitle:@"Inside Blue Layer"
  28. message:nil
  29. delegate:nil
  30. cancelButtonTitle:@"OK"
  31. otherButtonTitles:nil] show];
  32. } else {
  33. [[[UIAlertView alloc] initWithTitle:@"Inside White Layer"
  34. message:nil
  35. delegate:nil
  36. cancelButtonTitle:@"OK"
  37. otherButtonTitles:nil] show];
  38. }
  39. }
  40. }
  41. @end

3.5 自动布局

自动布局

你可能用过UIViewAutoresizingMask类型的一些常量,应用于当父视图改变尺寸的时候,相应UIView的frame也跟着更新的场景(通常用于横竖屏切换)。

在iOS6中,苹果介绍了自动排版机制,它和自动调整不同,并且更加复杂。

在Mac OS平台,CALayer有一个叫做layoutManager的属性可以通过CALayoutManager协议和CAConstraintLayoutManager类来实现自动排版的机制。但由于某些原因,这在iOS上并不适用。

当使用视图的时候,可以充分利用UIView类接口暴露出来的UIViewAutoresizingMaskNSLayoutConstraintAPI,但如果想随意控制CALayer的布局,就需要手工操作。最简单的方法就是使用CALayerDelegate如下函数:

  1. - (void)layoutSublayersOfLayer:(CALayer *)layer;

当图层的bounds发生改变,或者图层的-setNeedsLayout方法被调用的时候,这个函数将会被执行。这使得你可以手动地重新摆放或者重新调整子图层的大小,但是不能像UIViewautoresizingMaskconstraints属性做到自适应屏幕旋转。

这也是为什么最好使用视图而不是单独的图层来构建应用程序的另一个重要原因之一。

3.6 总结

总结

本章涉及了CALayer的集合结构,包括它的framepositionbounds,介绍了三维空间内图层的概念,以及如何在独立的图层内响应事件,最后简单说明了在iOS平台,Core Animation对自动调整和自动布局支持的缺乏。

在第四章“视觉效果”当中,我们接着介绍一些图层外表的特性。

4. 视觉效果

视觉效果

嗯,圆和椭圆还不错,但如果是带圆角的矩形呢? 我们现在能做到那样了么? 史蒂芬·乔布斯

我们在第三章『图层几何学』中讨论了图层的frame,第二章『寄宿图』则讨论了图层的寄宿图。但是图层不仅仅可以是图片或是颜色的容器;还有一系列内建的特性使得创造美丽优雅的令人深刻的界面元素成为可能。在这一章,我们将会探索一些能够通过使用CALayer属性实现的视觉效果。

4.1 圆角

圆角

圆角矩形是iOS的一个标志性审美特性。这在iOS的每一个地方都得到了体现,不论是主屏幕图标,还是警告弹框,甚至是文本框。按照这流行程度,你可能会认为一定有不借助Photoshop就能轻易创建圆角举行的方法。恭喜你,猜对了。

CALayer有一个叫做conrnerRadius的属性控制着图层角的曲率。它是一个浮点数,默认为0(为0的时候就是直角),但是你可以把它设置成任意值。默认情况下,这个曲率值只影响背景颜色而不影响背景图片或是子图层。不过,如果把masksToBounds设置成YES的话,图层里面的所有东西都会被截取。

我们可以通过一个简单的项目来演示这个效果。在Interface Builder中,我们放置一些视图,他们有一些子视图。而且这些子视图有一些超出了边界(如图4.1)。你可能无法看到他们超出了边界,因为在编辑界面的时候,超出的部分总是被Interface Builder裁切掉了。不过,你相信我就好了 :)

4.2 图层边框

图层边框

&nbp; CALayer另外两个非常有用属性就是borderWidthborderColor。二者共同定义了图层边的绘制样式。这条线(也被称作stroke)沿着图层的bounds绘制,同时也包含图层的角。

&nbp; borderWidth是以点为单位的定义边框粗细的浮点数,默认为0.borderColor定义了边框的颜色,默认为黑色。

&nbp; borderColor是CGColorRef类型,而不是UIColor,所以它不是Cocoa的内置对象。不过呢,你肯定也清楚图层引用了borderColor,虽然属性声明并不能证明这一点。CGColorRef在引用/释放时候的行为表现得与NSObject极其相似。但是Objective-C语法并不支持这一做法,所以CGColorRef属性即便是强引用也只能通过assign关键字来声明。

&nbp; 边框是绘制在图层边界里面的,而且在所有子内容之前,也在子图层之前。如果我们在之前的示例中(清单4.2)加入图层的边框,你就能看到到底是怎么一回事了(如图4.3).

清单4.2 加上边框

  1. @implementation ViewController
  2. - (void)viewDidLoad
  3. {
  4. [super viewDidLoad];
  5. //set the corner radius on our layers
  6. self.layerView1.layer.cornerRadius = 20.0f;
  7. self.layerView2.layer.cornerRadius = 20.0f;
  8. //add a border to our layers
  9. self.layerView1.layer.borderWidth = 5.0f;
  10. self.layerView2.layer.borderWidth = 5.0f;
  11. //enable clipping on the second layer
  12. self.layerView2.layer.masksToBounds = YES;
  13. }
  14. @end

[图片上传失败…(image-f6b464-1574399886581)]
图4.4 边框是跟随图层的边界变化的,而不是图层里面的内容

4.3 阴影

阴影

iOS的另一个常见特性呢,就是阴影。阴影往往可以达到图层深度暗示的效果。也能够用来强调正在显示的图层和优先级(比如说一个在其他视图之前的弹出框),不过有时候他们只是单纯的装饰目的。

shadowOpacity属性一个大于默认值(也就是0)的值,阴影就可以显示在任意图层之下。shadowOpacity是一个必须在0.0(不可见)和1.0(完全不透明)之间的浮点数。如果设置为1.0,将会显示一个有轻微模糊的黑色阴影稍微在图层之上。若要改动阴影的表现,你可以使用CALayer的另外三个属性:shadowColorshadowOffsetshadowRadius

显而易见,shadowColor属性控制着阴影的颜色,和borderColorbackgroundColor一样,它的类型也是CGColorRef。阴影默认是黑色,大多数时候你需要的阴影也是黑色的(其他颜色的阴影看起来是不是有一点点奇怪。。)。

shadowOffset属性控制着阴影的方向和距离。它是一个CGSize的值,宽度控制这阴影横向的位移,高度控制着纵向的位移。shadowOffset的默认值是 {0, -3},意即阴影相对于Y轴有3个点的向上位移。

为什么要默认向上的阴影呢?尽管Core Animation是从图层套装演变而来(可以认为是为iOS创建的私有动画框架),但是呢,它却是在Mac OS上面世的,前面有提到,二者的Y轴是颠倒的。这就导致了默认的3个点位移的阴影是向上的。在Mac上,shadowOffset的默认值是阴影向下的,这样你就能理解为什么iOS上的阴影方向是向上的了(如图4.5).

4.4 图层蒙板

图层蒙板

通过masksToBounds属性,我们可以沿边界裁剪图形;通过cornerRadius属性,我们还可以设定一个圆角。但是有时候你希望展现的内容不是在一个矩形或圆角矩形。比如,你想展示一个有星形框架的图片,又或者想让一些古卷文字慢慢渐变成背景色,而不是一个突兀的边界。

使用一个32位有alpha通道的png图片通常是创建一个无矩形视图最方便的方法,你可以给它指定一个透明蒙板来实现。但是这个方法不能让你以编码的方式动态地生成蒙板,也不能让子图层或子视图裁剪成同样的形状。

CALayer有一个属性叫做mask可以解决这个问题。这个属性本身就是个CALayer类型,有和其他图层一样的绘制和布局属性。它类似于一个子图层,相对于父图层(即拥有该属性的图层)布局,但是它却不是一个普通的子图层。不同于那些绘制在父图层中的子图层,mask图层定义了父图层的部分可见区域。

mask图层的Color属性是无关紧要的,真正重要的是图层的轮廓。mask属性就像是一个饼干切割机,mask图层实心的部分会被保留下来,其他的则会被抛弃。(如图4.12)

如果mask图层比父图层要小,只有在mask图层里面的内容才是它关心的,除此以外的一切都会被隐藏起来。

4.5 拉伸过滤

拉伸过滤

最后我们再来谈谈minificationFiltermagnificationFilter属性。总得来讲当我们视图显示一个图片的时候都应该正确地显示这个图片意即以正确的比例和正确的11像素显示在屏幕上。原因如下

  • 能够显示最好的画质像素既没有被压缩也没有被拉伸。
  • 能更好的使用内存因为这就是所有你要存储的东西。
  • 最好的性能表现CPU不需要为此额外的计算。

不过有时候显示一个非真实大小的图片确实是我们需要的效果。比如说一个头像或是图片的缩略图再比如说一个可以被拖拽和伸缩的大图。这些情况下为同一图片的不同大小存储不同的图片显得又不切实际。

当图片需要显示不同的大小的时候有一种叫做拉伸过滤的算法就起到作用了。它作用于原图的像素上并根据需要生成新的像素显示在屏幕上。

事实上重绘图片大小也没有一个统一的通用算法。这取决于需要拉伸的内容放大或是缩小的需求等这些因素。CALayer为此提供了三种拉伸过滤方法他们是

  • kCAFilterLinear
  • kCAFilterNearest
  • kCAFilterTrilinear

minification缩小图片和magnification放大图片默认的过滤器都是kCAFilterLinear这个过滤器采用双线性滤波算法它在大多数情况下都表现良好。双线性滤波算法通过对多个像素取样最终生成新的值得到一个平滑的表现不错的拉伸。但是当放大倍数比较大的时候图片就模糊不清了。

kCAFilterTrilinearkCAFilterLinear非常相似大部分情况下二者都看不出来有什么差别。但是较双线性滤波算法而言三线性滤波算法存储了多个大小情况下的图片也叫多重贴图并三维取样同时结合大图和小图的存储进而得到最后的结果。

这个方法的好处在于算法能够从一系列已经接近于最终大小的图片中得到想要的结果也就是说不要对很多像素同步取样。这不仅提高了性能也避免了小概率因舍入错误引起的取样失灵的问题

[图片上传失败…(image-79df8a-1574399886581)]

图4.15 对于没有斜线的小图来说最近过滤算法要好很多

总的来说对于比较小的图或者是差异特别明显极少斜线的大图最近过滤算法会保留这种差异明显的特质以呈现更好的结果。但是对于大多数的图尤其是有很多斜线或是曲线轮廓的图片来说最近过滤算法会导致更差的结果。换句话说线性过滤保留了形状最近过滤则保留了像素的差异。

让我们来实验一下。我们对第三章的时钟项目改动一下用LCD风格的数字方式显示。我们用简单的像素字体一种用像素构成字符的字体而非矢量图形创造数字显示方式用图片存储起来而且用第二章介绍过的拼合技术来显示如图4.16。

4.6 组透明

组透明

UIView有一个叫做alpha的属性来确定视图的透明度。CALayer有一个等同的属性叫做opacity,这两个属性都是影响子层级的。也就是说,如果你给一个图层设置了opacity属性,那它的子图层都会受此影响。

iOS常见的做法是把一个控件的alpha值设置为0.5(50%)以使其看上去呈现为不可用状态。对于独立的视图来说还不错,但是当一个控件有子视图的时候就有点奇怪了,图4.20展示了一个内嵌了UILabel的自定义UIButton;左边是一个不透明的按钮,右边是50%透明度的相同按钮。我们可以注意到,里面的标签的轮廓跟按钮的背景很不搭调。

4.7 总结

总结

这一章介绍了一些可以通过代码应用到图层上的视觉效果,比如圆角,阴影和蒙板。我们也了解了拉伸过滤器和组透明。

另外,如果你想一起进阶,不妨添加一下交流群1012951431,选择加入一起交流,一起学习。期待你的加入!

在第五章,『变换』中,我们将会研究图层变化和3D转换