UIView
负责交互事件的处理,CALayer
负责视图的展示,各司其职。
backing image
UIView | CALayer | 作用 |
---|---|---|
contents | 用UIImage.cgImage 进行赋值,可以让UIView 展示图片,称为backing image | |
contentsRect | 展示backing image 的指定区域内容,rect按比例指定 | |
contentsCenter | 当layer大小改变时,切分出backing image 的被拉伸压缩和不改变的区域 | |
cotentMode | contentsGravity | 图片展示的模式,包括拉伸、位置等 |
contentScaleFactor | contentsScale | 1point包含的pixel个数,越小图片展示出来越大,可能会超出视图的范围 |
clipsToBounds | masksToBounds | 决定是否展示超出边界的内容 |
coordinate systems
UIView | CALayer | 作用 |
---|---|---|
frame | frame | 在父视图中的位置和大小 |
bounds | bounds | 定义子视图位置的参考系,定义某个点为(0, 0) 之后子视图的frame 就相对该点进行确定,改变该属性子视图的位置将发生变化 |
center | position | 在父视图坐标系中的中心坐标 |
CALayer
还有一个anchorPoint
属性,默认值为(0.5, 0.5)
,表示该点位于视图和图层的中心,视图的transform
以该属性确定的位置作为支点。改变该属性 position
的坐标并不会发生变化,但是frame
会做出调整 -> 使得anchorPoint
确定的值(百分比)满足在视图中的位置。示例如下:
现在想要改变三张图片的旋转基点,注意参考坐标系x从左往右,y从上到下。
self.secondHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f);
self.minuteHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f);
self.hourHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f);
效果如下图:
确定不同坐标系下点的坐标
CALayer
具有这样的方法:convert(_ p: CGPoint, from l: CALayer?)
、convert(_ p: CGPoint, to l: CALayer?)
,将其它layer
坐标系下的点转化为自己坐标系下的点或者相反。
CALayer
还可以通过contains(_ p: CGPoint)
方法判断点是否在自己的frame
范围之内。但是当父视图有多个子视图时,有更好的方法去判断父视图坐标系下的一个点首先满足哪个子视图:hitTest(_ p: CGPoint) -> CALayer?
,该方法根据子视图的zPosition
大小由上到下依次判断。
自动布局
当父视图的尺寸发生改变时(旋转屏幕)可以通过CALayer
的autoresizingMask
对其子视图进行自动布局约束。比如指定其中一个子图层的该属性为[.flexibleBottomMargin, .flexibleTopMargin]
,表示自动布局时该子图层调整大小使得到父视图上边距和下边距的距离不变。
visual effects
边框
CALayer
可以控制borderWidth
和borderColor
,绘制为内边框且显示在子图层之上。
阴影
CALayer | 作用 |
---|---|
shadowOpacity | 阴影的不透明度,默认为0 |
shadowColor | 阴影的颜色,默认为黑色 |
shadowOffset | 阴影的偏移,为CGSize 类型 ,默认为(0, -3) |
shadowRadius | 阴影的模糊度,默人为0,越大阴影越模糊,图层的深度更加明显 |
与边框不同的是,阴影会根据图层的外形进行渲染,且在边框之外显示。带来的问题就是如果masksToBounds
为true
,阴影将不会显示。正确的做法是再创建一个外图层用来显示阴影,内图层则负责剪裁。
由于阴影继承自图层内容,所以计算阴影的形状非常的消耗资源,所以最好通过shadowPath
指定阴影的形状,需要注意的是使用UIBezierPath
的话里面的坐标都是以layer
所属视图为参考系的,UIKit
渲染的会更加容易。
不透明度
UIView
的alpha
和CALayer
的opacity
属性都是用来确定视图的不透明度,这两个属性都是影响子层级的,如果想让每个子视图拥有不同的透明度
transforms
CGAffineTransform
仿射变换相当于对图层的每个点在平面上进行了坐标变换:
a
和d
代表了拉伸/缩小系数,b
和c
与旋转有关。下面看一个例子:
@objc func doTransform() {
var transform = CGAffineTransform.identity
transform = transform.rotated(by: .pi / 4) //顺时针旋转45度
transform = transform.scaledBy(x: 2, y: 2) //放大2倍
transform = transform.translatedBy(x: 100, y: 100) //向右、向下平移100
tempView.layer.setAffineTransform(transform)
}
CATransform3D
在三维空间进行变换增加了z
坐标轴,旋转需要注意的点比较多,首先看一下rotate
的方法:func CATransform3DRotate(_ t: CATransform3D, _ angle: CGFloat, _ x: CGFloat, _ y: CGFloat, _ z: CGFloat) -> CATransform3D
,x、y、z
指的是旋转的中心轴线,一般想按哪个轴线就赋值1
。
由于是三维视角,如果不通过属性m34 = -1.0 / d
指定透视效果,则达不到想要的效果,d
越小透视效果越明显,一般指定500~1000
。示例如下:
@objc func do3DTransform() {
var transform3D = CATransform3DIdentity
//注意改属性要在变换前指定,否则无效
transform3D.m34 = -1 / 500.0
transform3D = CATransform3DRotate(transform3D, .pi / 4, 0, 1, 0)
tempView.layer.transform = transform3D
}
如果想为子图层统一指定透视效果,可以用sublayerTransform
属性,相当于一个相机,并且该相机可以跟图层一样旋转,从而调整观察的角度。
如果旋转180
度,背面其实默认会绘制镜像图层,如果用不到将会造成GPU
浪费,可以调整doubleSided
的值控制是否绘制背面的图层。
接下来看一个有趣的现象:
//rotate the outer layer 45 degrees
var outer = CATransform3DIdentity
outer.m34 = -1 / 500.0
outer = CATransform3DRotate(outer, .pi / 4, 0, 0, 1)
alphaButton.layer.transform = outer
//rotate the inner layer -45 degrees
var inner = CATransform3DIdentity
inner.m34 = -1 / 500.0
inner = CATransform3DRotate(inner, -.pi / 4, 0, 0, 1)
alphaLabel.layer.transform = inner
首先明确的是当父视图旋转时子视图会跟着旋转,绕z
轴旋转相当于平面变换,子视图旋转-45
度相当于抵消了父视图的旋转,视觉上看上去没有发生什么变换。如果二者绕x
或y
轴进行旋转,则不会低消,取而代之的是每个视图做自己的变换,不受父视图影响。
Dedicated layer
CAGradientLayer
用来做图层颜色的渐变:
extension UIView {
// 注意,颜色要是cgColor,否则不会生效
func gradientColor(colors: [Any], locations: [NSNumber], startPoint: CGPoint, endPoint: CGPoint) {
let gralayer = CAGradientLayer()
//注意当UIView的bounds确定时才会生效,当用Snapkit约束确定大小时要在ViewDidAppear时调用该方法
gralayer.frame = layer.bounds
layer.addSublayer(gralayer)
gralayer.colors = colors
//每个值代表对应颜色的结束点,第一个值最好不要设成0
gralayer.locations = locations
gralayer.startPoint = startPoint
gralayer.endPoint = endPoint
}
}
CAShapeLayer
用来绘制一定形状的图层,给图层增加若干个圆角方法如下:
extension UIView {
//注意UIView的bounds确定才会生效
func round(corners: UIRectCorner, radius: CGFloat) {
let path = UIBezierPath(roundedRect: bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
let mask = CAShapeLayer()
mask.path = path.cgPath
layer.mask = mask
}
}
Animation
Implicit Animation
隐式动画通过CATransaction
实现,隐式的意思是说不需要指定动画的类型,当改变图层的某个属性时会添加默认的动画效果。比如:
@objc func changeColor() {
CATransaction.begin()
CATransaction.setAnimationDuration(2)
CATransaction.setCompletionBlock {
var transform = CGAffineTransform.identity
transform = transform.rotated(by: CGFloat(self.rotateCount) * .pi / 4)
self.rotateCount += 1
self.transactionLayer.setAffineTransform(transform)
}
let red = CGFloat(Float(arc4random()) / Float(UINT32_MAX))
let green = CGFloat(Float(arc4random()) / Float(UINT32_MAX))
let blue = CGFloat(Float(arc4random()) / Float(UINT32_MAX))
transactionLayer.backgroundColor = UIColor(red: red, green: green, blue: blue, alpha: 1).cgColor
CATransaction.commit()
}
注意transactionLayer
并不依赖于UIView
存在,如果要对UIView
的关联图层进行同样的操作,则设置的渐变时间不生效,这是因为其隐式动画默认被禁用了。除此之外,还可以通过过渡动画调整隐式动画的效果,如下:
private lazy var transactionLayer: CALayer = {
let layer = CALayer()
layer.backgroundColor = UIColor.brown.cgColor
//变化时从左侧渐入改变颜色
var transition = CATransition()
transition.type = .push
transition.subtype = .fromLeft
layer.actions = ["backgroundColor": transition]
return layer
}()
Explicit Animation
可以看下面一段示例:
@objc func groupAnimation() {
let bezierPath = UIBezierPath()
bezierPath.move(to: CGPoint(x: 50, y: 150))
bezierPath.addCurve(to: CGPoint(x: 300, y: 150), controlPoint1: CGPoint(x: 150, y: 0), controlPoint2: CGPoint(x: 200, y: 300))
let shapeLayer = CAShapeLayer()
shapeLayer.path = bezierPath.cgPath
shapeLayer.strokeColor = UIColor.red.cgColor
view.layer.addSublayer(shapeLayer)
let colorLayer = CALayer()
colorLayer.frame = CGRect(x: 0, y: 0, width: 64, height: 64)
colorLayer.position = CGPoint(x: 50, y: 150)
colorLayer.backgroundColor = UIColor.green.cgColor
view.layer.addSublayer(colorLayer)
//帧动画
let keyAnimation = CAKeyframeAnimation(keyPath: "position")
keyAnimation.path = bezierPath.cgPath
keyAnimation.rotationMode = .rotateAuto
//属性动画
let basicAnimation = CABasicAnimation(keyPath: "backgroundColor")
basicAnimation.toValue = UIColor.red.cgColor
//组动画
let groupAnimation = CAAnimationGroup()
groupAnimation.animations = [keyAnimation, basicAnimation]
groupAnimation.duration = 4
colorLayer.add(groupAnimation, forKey: nil)
}
显式动画做完之后会恢复视图之前的属性,如果想在动画结束之后保存最终状态,可以通过实现CAAnimationDelegate
的animationDidStop(_ anim: CAAnimation, finished flag: Bool)
方法设置。如果有很多动画都设置该代理,可以通过setValue(_ value: Any?, forKey key: String)
和 value(forKey key: String)
区分。
CAPropertyAnimation
的keyPath
针对transform
推出了虚拟属性,旋转缩放可用transform.rotation/scale
。