几个方法和属性贯穿本文
1、isGeometryFlipped
iOS 坐标从左上角开始,如果有过桌面开发经验的话,可以发现桌面端是从左下角开始。设置一个属性:
layer.isGeometryFlipped = true
让坐标系从左下角开始,左下角坐标变为(0,0)。这个属性有什么用呢?假如有一个 macOS 程序,后来想迁移到 iPad 上,其中用到了 drawRect,为了能直接 copy 代码,发现有这个翻转坐标系属性,移植代码简单了很多。
2、convert 系列方法
某个图层坐标系下的点坐标,转换成另一个图层坐标系下的坐标。有四个 convert 方法,看参数名可以知道功能。
例如 layer.convert(CGPoint, to:CALayer) ,第一个参数是调用者坐标系下的某个点,返回该点在第二个参数 layer 坐标系下的坐标。后面 demo 还介绍了一个。
3、contains
判断图层是否包含某一个点 CGPoint,返回 Bool
CALayer::contains(CGPoint) 实际会返回 layer 的 bounds 是否包含该点。所以用这个方法要小心:乍一看某个点(-20,-20)不可能在 layer 上,但是!!!它可能被 layer 的 bounds 包含!!!
4、zPosition
Z坐标轴。和 UIView 严格的二维坐标系不同,CALayer 有一个 Z 轴,除了做3D动画(以后会介绍)外,这个 zPosition 属性唯一的作用是,调整同级图层的渲染顺序。这里的同级怎么理解呢,,,视图树同时对应着一个图层树,假设 view1 和 view2 是同级的兄弟关系,那么它俩的 layer 也是同级的;view1 的 layer 的 subLayers,子图层之间是同级的,但子图层和 view1、view2 的 layer 不是同级的,和根 view 的 layer 更不是同级的。。。zPosition 默认0,值大的,盖在同级图层的最上面。
5、Hit Testing
CALayer 是不处理交互的,在交互事件传递中,用到的是 UIView 的 hitTest,复习响应者链。那为什么 CALayer 还要提供这个方法呢?可能是预留给开发者自定义一套事件处理机制吧。。
一个简单的 demo 是,如图一个 UIView,其 layer(蓝色)有一个subLayer(绿色),我要实现:
触摸蓝色部分,打印 blue ,触摸 绿色部分,打印 green。
第一版代码如下:
class ViewController:UIViewController,CALayerDelegate {
var blueView: UIView!
var greenLayer:CALayer!
override func viewDidLoad() {
super.viewDidLoad()
blueView = UIView(frame:CGRect(x: 50, y: 200, width: 350, height: 300))
blueView.backgroundColor = UIColor.blue
greenLayer = CALayer()
greenLayer.frame = CGRect(x: 50, y:200, width: 200, height:100)
greenLayer.backgroundColor = UIColor.green.cgColor
blueView.layer.addSublayer(greenLayer)
view.addSubview(blueView)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// 取出 touch
let allTouches = touches as NSSet
let touch = allTouches.anyObject() as! UITouch
// 得到 touch 的 point,注意是相对于 self.view
var point = touch.location(in: self.view)
// point 是触摸位置在 self.view 坐标系中的位置
// 调用 convert 转换坐标系,根据函数名确定转换方式
point = blueView.layer.convert(point, from: view.layer)
// point 假设值为(50,50),convert 方法从 from 参数 view 的坐标系出发,找到此坐标系中的 (50,50) 的点,然后方法调用者 blueView.layer 用自己的坐标系衡量刚找到的点,返回。如果返回的点在 blueView.layer 的 bounds 内,即 contains 方法:
if blueView.layer.contains(point) {
// 此时确定了在蓝色范围内,然后要判断,是否是在绿色范围内
point = greenLayer.convert(point, from:blueView.layer)
if greenLayer.contains(point) {
print(" in green")
} else {
print("in blue")
}
}
}
}
上面的处理挺麻烦的,可以用 hitTest 方法代替,方法接受一个 CGPoint 类型参数,它返回一个包含这个坐标点的图层树上最远的图层,或空。这意味着不再需要 contains 那样人工在每个子图层变换坐标。响应者链的文章里详细说了 UIView::hitTest,CALayer 的函数思路是一样的,不再赘述
override func touchesBegan(_ touches:Set<UITouch>, with event:UIEvent?) {
let point = touches.first!.location(in: view)
let layer = view.layer.hitTest(point)
if layer == blueView.layer {
print(1)
} else if layer == greenLayer {
print(2)
}
// 强调:之前提到的 zPosition 属性可以改变图层的渲染顺序,但不能改变事件传递的顺序。
// 因此如果改变图层的 z轴 顺序,会无法检测到看起来的“最前方”的视图点击事件,这是因为在图层树上实际仍被另一个图层遮盖住了!
}
6、CALayer 的自动布局
在 iOS 中,我们平时写自动布局的代码时,都是给 UIView 加约束或者用 UIViewAutoresizingMask。
经常用到一些 CALayer 的子类,比如 CAGradientLayer 作为一个 view 的子图层加个渐变色。
如果你的 app 想适配屏幕大小变化,比如转屏、iPad 分屏,就会发现,UIView 可以自动布局、自适应宽高,但是那个子图层得手动更新 frame,咋办呢?
一般情况下,都是自定义 view 重写 layoutSubviews,然后指定子图层的 frame 为 self.bounds。
其实还有一个函数,做这件事看起来更“专业”:CALayerDelegate 的 layoutSublayers(of: CALayer) 方法:
class AutoLayoutSublayersView: UIView {
override func layoutSublayers(of layer: CALayer) {
super.layoutSublayers(of: layer)
layer.sublayers?.forEach({ subLayer in
subLayer.frame = layer.bounds
})
}
}
UIView 可以自动布局,管理着自己的 layer 的大小,UIView 遵守 CALayerDelegate(view 自己的 layer 的 delegate 只能是该 view),layer 大小变化*时,回调 delegate,调用上面的代码,我们把更新子图层大小的代码写在这里。
*回调时机?我们熟悉 layoutSubviews 的调用时机,实际上,layoutSubviews 是在 layoutSublayers(of: CALayer) 里被调用的!!!