iOS CALayer图层漫谈(二)

上一篇《iOS CALayer图层漫谈(一)》我们聊了CALayer的基本的概念和有关寄宿图的一些属性,这一篇呢我们来聊一下CALayer几何学相关的事情。

CALayer的布局

说到布局我们知道UIView有三个特别重要的布局属性:frameboundscenter,CALayer同样也有这样三个属性,只不过center在CALayer中叫做position(为什么叫法不一样呢?这里是有原因的,后面会给大家解释)。

平时当我们改变UIView的frame的时候,实际上是在改变视图中layer的frame,也就是说我们不能只改变UIView的frame而让layer的frame不变,它们之间是同步的。至于这三个属性的用法这里就不多做解释了,我想聊一下的是这三个属性之间,以及和transform之间的一些关系。

对于视图或者图层来说,frame其实是一个虚拟属性,它是根据bounds、position和transform计算而来的(你可以通过下面的图来理解),所以当其中任何一个值发生变化,frame都会变化。反之,改变frame也会影响到他们当中的值。

通常情况下,frame的宽高和bounds的宽高是相同的,但是当改变了transform的时候,frame的宽高和bounds的宽高可能就不再相同了。通过下图大家可以理解一下这些属性之间的关系:

CALayer的“图钉说” -- 锚点

在聊锚点之前,我们有必要重新认识一下center属性和position属性。我们知道,这两个属性都是设置当前视图(或图层)的中心点在父视图(或父图层)坐标系统中位置,从而确定当前视图(或图层)在父视图(或父图层)位置。作为视图这个点是当前视图的中心点,但是对于图层来说可能就不是中心点了。

在CALayer中有一个属性叫做anchorPoint,也就是我们接下来要说的锚点。但是在UIView中,这个属性并没有被接口暴露出来,这也是为什么UIView的position属性被叫做center的原因,因为center包含了位置+中心两层含义,center其实是由positionanchorPoint两个属性叠加得到的,此时anchorPoint对应的值是默认的中心点位置。

那么positionanchorPoint到底代表什么呢?下面我们通过一个物理模型“图钉说”来帮助大家理解一下(请牢记这个模型):

我们可以把当前图层比作一张纸,父图层比作一面墙。我们通过positionanchorPoint属性将当前图层添加到父图层上,其实就相当于拿一枚图钉将这张纸固定到这面墙上。那么positionanchorPoint分别代表这个物理模型中的什么呢?这里图钉穿过纸张的位置就是anchorPoint锚点的位置,图钉扎在墙上的位置就是position的位置。也就是说anchorPoint锚点的位置是相对于当前图层的,而position是相对于父图层的。

【 这里要说明一点的是:anchorPoint使用的坐标系统跟我们上篇文章提到的contentsRect是一样的,使用的是相对坐标系统,也就是说左上角的坐标是{0,0}右下角的坐标是{1,1}。并且我们也可以通过设置小于0或大于1的值,把锚点放置在图层范围之外。】

看完上面这个物理模型之后,我们再来思考这样一个问题:如果在position不变的情况下改变anchorPoint,此时会发生什么变化?继续利用上面的模型,相当于我们将图钉摘下,从纸的另一个位置穿过,然后重新扎回上次的位置。此时图钉在墙上的位置不变,但是纸张相对墙的位置却发生了变化,其实也就是frame发生了变化。我们可以通过下图进行进一步的理解:

关于anchorPointposition就先聊这么多,如果想找一个应用场景通过代码来深入理解一下anchorPointposition的话,可以去看我的另一篇文章《iOS制作一个模拟时钟》,文中模拟时钟的制作就用到了锚点的知识。这里还要给大家补充另外一个知识点:当一个图层通过transform进行旋转的时候,图层所围绕的中心就是锚点,这跟我们上面提到的物理模型也是相吻合的。

CALayer的“空间”转换

我们知道不管是UIView还是CALayer,它们的位置定义都是相对于它们的父视图(或图层)的,我们拿到也是它们相对于父视图(或图层)坐标系统的位置信息。但是有时候我们想知道的并不是它们相对于父视图(或图层)坐标系统的位置信息,而是它们的绝对位置或者相对于其他视图(或图层)的位置怎么办呢?

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;

我们知道在iOS上坐标系的原点通常位于左上角,但是在MacOS上通常是位于左下角,跟我们通常现实中使用的x/y轴坐标系是一样的。在CALayer中,它提供了一个属性叫做geometryFlipped,我们可以通过这个属性对坐标系进行翻转,当设置成YES后,iOS中,坐标原点变为在左下角;MacOS中,坐标原点变为在左上角。

和UIView所在的二维坐标系统不同,CALayer是存在于一个三维空间中的,除了上面说过的positionanchorPoint属性外,CALayer还有另外两个属性zPositionanchorPointZ,这两个浮点型属性是用来描述图层在z轴方向上的位置,而z轴的方向是垂直于屏幕平面指向人眼方向的。当我们设置各图层的这两个属性的时候,由于各图层在z轴方向上分布的位置不同,就会产生前后遮挡的位置关系,从而改变图层树的层级关系。虽然这个两个属性我们平时很少用到,但是在涉及CATransform3D三维空间移动和旋转图层的时候就会经常出现。

CALayer作为事件响应的“辅助英雄”

在上一篇文章开始我们就提到,CALayer并不能独立的完成事件响应,它也不能直接处理触摸事件或者手势。但是作为UIView的“辅助”,它还是要做一些“辅助英雄”该干的事儿的。

CALayer提供了两个方法:-containsPoint:-hitTest:

-containsPoint:方法接收一个在本图层坐标系下的CGPoint对象,如果在图层范围内,就返回YES,这样我们就可以通过遍历视图树(注意这里指的是视图树而不是图层树)的方式找出应该接收触摸事件的图层以及它对应的UIView。但是这样做实在是太麻烦了,我们不但要手动的遍历视图树,还要将触摸点逐个转换成每个图层坐标系下的点。为此CALayer给我们提供了另外一个方法-hitTest:

-hitTest:方法接收的同样是一个CGPoint对象,但是返回值不是BOOL类型了,而是图层本身,或者包含这个触摸坐标点的叶子节点图层。也就是说-hitTest:方法会自动帮我们遍历它跟它的子图层,并返回包含这个坐标点的图层,如果都不包含则返回nil

这里要说一下为什么上文中说遍历的是视图树而不是图层树,因为在之前聊zPosition属性的时候说过,改变zPosition属性值得时候会改变图层树的层级关系,但是此时layer对应的视图树的层级关系是不发生变化的,所以说对于触摸手势的传递顺序还是按照视图树的层次顺序来,所以这样就会导致更改了zPosition值位置靠前的图层视图不响应触摸手势的问题。

CALayer的自适应

在iOS开发中我们经常使用自动布局相关的一些API,但是对于CALayer来说自动布局还是相对较弱的,当然这只是在iOS中是这样,在MacOS中我们可以通过CALayerManager协议和CAConstraintLayoutManager类来实现自动排版的机制,而在iOS中如果想随意控制CALayer的布局,就需要手动操作了。最简便的方法就是使用CALayerDelegate中的代理方法:

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

当图层的bounds发生变化,或者图层的-setNeedsLayout方法被调用,这个代理方法就会被执行,然后我们就可以在代理方法中重新手动的摆放调整子图层的位置大小了,但是不能像UIView的autoresizingMaskconstraints属性做到自适应屏幕旋转。所以在iOS中涉及到自适应布局的时候还是推荐使用UIView暴露出来的UIViewAutoresizingMaskNSLayoutConstraint的API接口。


下一篇《iOS CALayer图层漫谈(三)》我们将聊一聊CALayer视觉效果相关的一些事情。

版权声明:出自MajorLMJ技术博客的原创作品 ,转载时必须注明出处及相应链接!