引言
关于UIView的布局有一个经常被问到的问题,frame和bounds有什么区别,同样CALayer也有frame和bounds这两个属性,还有一个与UIView的center对应的position属性,本篇博客我们就来详细的探讨一下图层中的frame和bounds到底有什么区别?以及为什么在UIView中中心点使用了center而图层中使用了position?
图层几何属性
图层的布局
frame其实代表的是外部坐标系,也就是在父图层上占据的空间。
bounds则是内部坐标系,它的x和y总是0。
position(视图中的center)代表了相对于父图层anchorPoint(锚点)所在位置。
上面图片展示了一个白色的View添加到了黑色View上。
对于视图来说它的frame,bounds,和center分别是
frame = {10,10,50,30}
bounds = {0,0,50,30}
center = {35,25}
对于图层来说也是一样的:
frame = {10,10,50,30}
bounds = {0,0,50,30}
position = {35,25}
到目前为止这些属性看起来都是一样的,事实上视图的frame,bounds,center这三个属性仅仅是存取方法。当我们操作视图的frame时,实际上是在改变位图视图下方CALayer的frame。
frame属性其实并不是一个非常明确的属性,而是一个虚拟属性,是根据bounds,position和transform共同计算而来的,所以当其中任何一个发生改变时,frame都会发生改变。
当图层做旋转或者缩放的变换时,frame实际上是覆盖在图层变换之后的整个轴对齐的矩形区域,所以frame的宽高并不总是等于bounds的宽高,如下图所示:
图层发生了旋转之后,它的bounds和position没有任何变化。
但是frame是绿色边框的矩形区域,不管是x,y还是width,height都发生了变化。
图层的anchorPoint(锚点)
上面提到了anchorPoint属性,图层的anchorPoint通过position来控制它的frame属性,我们可以把anchorPoint当做是移动图层的把柄。
anchorPoint也使用单位坐标,默认来说它位于图层的中点{0,0},所以图层将会以这个点为中心放置,而UIView的anchorPoint属性没有暴漏出来,所以对于视图来说它的锚点总是中心点,因此视图的position也就可以被称之为center。
但是图层的anchorPoint是可以被移动的,比如我们把它的anchorPoint调整到左上角{0,0},如图所示(虚框为调整后的图层显示):
图层的坐标系
图层在图层树当中也是相对父层级按层级关系来放置,这一点和视图是一样的。
当父图层发生移动和变化时,所有的子图层也会跟随移动和变化。
这样确实很方便,但是有的时候我们还是需要知道一个图层的绝对位置,或者是一个图层相对另外一个图层的位置。
和视图一样CALayer也提供了一系列的图层间坐标转换的方法:
open func convert(_ p: CGPoint, from l: CALayer?) -> CGPointopen func convert(_ p: CGPoint, to l: CALayer?) -> CGPointopen func convert(_ r: CGRect, from l: CALayer?) -> CGRectopen func convert(_ r: CGRect, to l: CALayer?) -> CGRect
这些方法,可以把定义在一个图层坐标系下的点或者矩形区域转换成另一个图层坐标系吓得点或者矩形区域。
图层的Z坐标
CALayer实际上存在于一个三维空间当中,所以它还有另外两个特殊的属性,zPosition和anchorPointZ,两个都是在z轴上描述图层位置的浮点类型。
不过zPosition属性其实并不常用,它最直观的功能就是修改图层的显示顺序,通常来讲图层是根据它们子图层的sublayers出现的顺序来绘制的,也就是后绘制的图层会覆盖先绘制的图层。但是我们可以通过修改zPosition,来调整图层的显示顺序,当我们增加这个值的时候它就可以显示到所有小于它的zPosition图层的前面。
举个例子,我们创建两个图层:
let layer = CALayer()layer.backgroundColor = UIColor.white.cgColorlayer.frame = CGRect(x: 0, y: 0, width: 226, height: 137)layer.position = self.view.centerself.view.layer.addSublayer(layer)let redLayer = CALayer()redLayer.backgroundColor = UIColor.red.cgColorredLayer.frame = CGRect(x: 0, y: 0, width: 200, height: 100)redLayer.position = CGPoint(x: self.view.center.x + 100, y: self.view.center.y + 100)self.view.layer.addSublayer(redLayer)
在没有修改zPosition的情况下显示如下:
当我们把白色图层的zPosition设置为1时:
let layer = CALayer()layer.zPosition = 1layer.backgroundColor = UIColor.white.cgColorlayer.frame = CGRect(x: 0, y: 0, width: 226, height: 137)layer.position = self.view.centerself.view.layer.addSublayer(layer)
效果如下:
我们发现通过修改zPosition白色的图层已经显示在了红色图层之上。
但是有个问题需要注意,当我们处理图层的点击事件时,仍然还是按照图层的添加顺序来处理,通过修改zPosition并不能改变图层的响应顺序。不过我们不是说图层不关心响应链事件吗?下面就来说明这件事儿。
图层的点击事件
CALayer确实不关心响应链,但是它有一些列的方法来处理事件
判断触摸点是否在图层内
open func contains(_ p: CGPoint) -> Bool
获取触摸点所在图层
open func hitTest(_ p: CGPoint) -> CALayer?
使用contains方法判断被点击的图层:
var whiteLayer: CALayer!var redLayer: CALayer!override func viewDidLoad() {super.viewDidLoad()self.view.backgroundColor = .blacklet layer = CALayer()layer.backgroundColor = UIColor.white.cgColorlayer.frame = CGRect(x: 0, y: 0, width: 226, height: 137)layer.position = self.view.centerself.view.layer.addSublayer(layer)self.whiteLayer = layerlet redLayer = CALayer()redLayer.backgroundColor = UIColor.red.cgColorredLayer.frame = CGRect(x: 0, y: 0, width: 200, height: 100)redLayer.position = CGPoint(x: self.view.center.x + 100, y: self.view.center.y + 100)self.view.layer.addSublayer(redLayer)self.redLayer = redLayer}override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {let point = touches.first?.location(in: self.view)let layerPoint = self.whiteLayer.convert(point!, from: self.view.layer)if self.whiteLayer.contains(layerPoint) {print("点击了白色图层")}let redLayerPoint = self.redLayer.convert(point!, from: self.view.layer)if self.redLayer.contains(redLayerPoint) {print("点击了红色图层")}}
点击了白色图层
点击了红色图层
可以看见确实可以处理点击事件。
使用hitTest方法处理点击图层:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {let point = touches.first?.location(in: self.view)let layer = self.view.layer.hitTest(point!)if layer == self.whiteLayer {print("点击了白色图层")} else if layer == self.redLayer {print("点击了红色图层")}}
点击了白色图层
点击了红色图层
之前提到的修改zPosition属性可以修改图层的显示顺序,但是不能改变事件的传递顺序也可以在这里进行验证。
总结
本篇博客主要说明了图层的几何结构,包括它的frame,position和bounds以及三者的关系。图层的点击事件,后面的博客会继续介绍一些图层的外表特性。