前言
?自己的语言能力有限,借鉴别人的文章理解并写下这篇文章。
参考链接:iOS CoreAnimation专题——原理篇(三) CALayer的模型层与展示层 - 知乎
使用Swift语言,深入CALayer内部,通过CABasicAnimation动画来探究CALayer的呈现层(presentation)和模型层(model)。
1.首先cmd CALayer点进去查看着两个属性
/* Returns a copy of the layer containing all properties as they were
* at the start of the current transaction, with any active animations
* applied. This gives a close approximation to the version of the layer
* that is currently displayed. Returns nil if the layer has not yet
* been committed.
*
* The effect of attempting to modify the returned layer in any way is
* undefined.
*
* The `sublayers', `mask' and `superlayer' properties of the returned
* layer return the presentation versions of these properties. This
* carries through to read-only layer methods. E.g., calling -hitTest:
* on the result of the -presentationLayer will query the presentation
* values of the layer tree. */
open func presentation() -> Self?
/* When called on the result of the -presentationLayer method, returns
* the underlying layer with the current model values. When called on a
* non-presentation layer, returns the receiver. The result of calling
* this method after the transaction that produced the presentation
* layer has completed is undefined. */
open func model() -> Self
各位看官自行翻译一下吧。需要特别注意的是呈现图层仅仅当图层首次被提交(就是首次第一次在屏幕上显示)的时候创建,所以在那之前调用presentationLayer将会返回nil.
2.下面我们使用CABasicAnimation添加一个改变view位置的动画,代码如下: ????
var redLayer = CALayer.init()
override func viewDidLoad() {
super.viewDidLoad()
//redLayer
redLayer.backgroundColor = UIColor.red.cgColor
redLayer.frame = CGRect(x: 80, y: 80, width: 100, height: 100)
self.view.layer.addSublayer(redLayer)
//动画
let annimation = CABasicAnimation.init(keyPath: "position")
annimation.duration = 2
annimation.fromValue = CGPoint(x: 80, y: 80)
annimation.toValue = CGPoint(x: 200, y: 500)
redLayer.add(annimation, forKey: nil)
}
也就是指定了动画的四要素:动什么(keyPath)、动多久(duration)、从什么样子开始(fromValue)、动到什么样子(toValue)。这样CABasicAnimation就能在duration内对keyPath指定的属性从fromValue到toValue之间进行插值。然后我们将动画施加给layer,这样它就会按 照我们的四要素进行动画了。
注意到fromValue和我们初始化redLayer时的position是不一样,初始值position为(80+50,80+500)
我们运行一下将会有如下效果: ???????
??????????????
我们看到动画结束之后 redLayer的位置又回到了原来的位置,原因是什么呢 ?
3.?瞎子和瘸子
?网上有一堆的瞎子瘸子比如的文章,感觉解释的很好这里就直接摘抄了。
从前有一个瞎子和一个瘸子,瘸子看得见路,但是不能自己走,瞎子能自己走,但是他看不见路。于是他们想到了一个办法:瞎子背着瘸子,这样瘸子就指挥瞎子如何走路,瞎子负责走路,每走一步,瞎子都会停下来听从瘸子的指挥,这样他们就不紧不慢的走向了目的地。
在CALayer内部也有一个瞎子和一个瘸子:presentation(以下简称P)和model(以下简称M)。presentation负责走路(绘制内容),而model负责看路(如何绘制)。
P有这样的特点:
1.我们看到的一切内容都是p的
2.p只有在下次屏幕刷新的时间才会进行绘制
M的特点:
1.对CALayer的各种绘图属性进行赋值和访问实际上都是访问的M的属性,比如bounds,background,position等
2,对这些属性进行赋值,不会影响p,也就不会影响绘制内容。
那么为什么我们对M的属性进行赋值,“与此同时”视图的显示状态就会发生改变呢?
如果我们用t0表示我们对属性进行赋值的时刻,t1表示下次屏幕刷新的时刻,t0到t1之间的间隔相当短(小于1/60秒),我们的人眼根本察觉不到这样的间隔时间存在,所以我们的感觉就是:赋值和界面绘制是同时进行的。然而实际上,赋值和界面绘制之间有一个t1-t0的时间间隔。
所以总结以上信息,我们可以知道P和M是这样进行交互的:当下次屏幕刷新信号到来时(屏幕重绘时),P为了绘制内容到屏幕上,它会去找M要各种它需要的用来绘制的属性,然后用这些属性的信息来绘制。
直到屏幕刷新信号到来才进行绘制,这种方式能够极大提高绘制效率。考虑这样一种情况,如果连续写了以下三行代码:redLayer.frame = …; redLayer.backgounrdColor = …; redLayer.position?= …;,如果每次赋值就会导致屏幕的重绘,这样就会有三次重绘,也就是GPU会执行三次绘制操作。而如果我们先把绘制信息存起来,当需要绘制的时候(屏幕刷新)再用这些信息去重绘,GPU就只会执行一次绘制操作。
上面三行代码,只是对M的属性赋值了而已,没有任何的绘制操作,当下次屏幕刷新信号到来时,P才会去找M要这些属性,进行一次绘制。执行这三次赋值(执行三行代码???????)所需的时间远远小于两次屏幕刷新的间隔,所以这样三行代码是一定能够在下次绘制开始前执行完的。也就是说,当我们写了这三行代码后,视图的显示内容不会改变三次,而只会改变一次。
如果我们继续按照瞎子和瘸子的理论进行下去,P背着M,如果我们在下次屏幕刷新前(P走下一步之前)对M进行了三次操作:把M移动到点1,把M移动到点2,把M移动到点3。那么下次屏幕刷新时,P会直接问M“你在哪啊”,M说我在点3呢,你这样这样走过来,于是P就屁颠屁颠的按照M的指挥跑到点3去了,它是不会先跑到点1再跑到点2再跑到点3的。
4.CAAnimation对presentation的控制
P这个瞎子背着M这个瘸子,他们互帮互助,完成各种显示的逻辑。每当屏幕刷新的时候,如果没有其他的信息告诉P它应该如何绘制,那么P就只能去问M,然后回到M的状态。
这样一个平衡会在一个CAAnimation被加到CALayer上后会被打破。当我们调用了layer的addAnimation方法后,一个CAAnimation(以下简称A)就控制了P,就相当于A一脚把M从P身上踹了下来,然后P开始背着A走。
现在P要如何显示就完全由A来控制了,因为A被指定了在duration中从fromValue变到toValue。所以在动画的持续时间内,M被丢在了原地,P则背着A到处跑(毕竟P是个瞎子,根本不知道他脑袋顶上的家伙是谁),每一步该如何走都是A通过插值计算出来的(通过duration、from、to就能计算任意时刻P的状态了)。
运行下面的代码可以看出P背着A跑时,M原地不动:
CADisplayLink每(1/60)s执行一次selector方法。
var redLayer = CALayer.init()
var displayLink : CADisplayLink!
override func viewDidLoad() {
super.viewDidLoad()
//redLayer
redLayer.backgroundColor = UIColor.red.cgColor
redLayer.frame = CGRect(x: 80, y: 80, width: 100, height: 100)
self.view.layer.addSublayer(redLayer)
//动画
let annimation = CABasicAnimation.init(keyPath: "position")
annimation.duration = 2
annimation.fromValue = CGPoint(x: 80, y: 80)
annimation.toValue = CGPoint(x: 200, y: 500)
redLayer.add(annimation, forKey: nil)
displayLink = CADisplayLink.init(target: self, selector: #selector(getFrame))
displayLink.add(to: RunLoop.main, forMode: .default)
}
@objc func getFrame() {
print("动画过程中的frame改变")
print("modelFrame ---- \(redLayer.model().frame)")
print("presentationFrame ---- \(redLayer.presentation()!.frame)")
print("\n")
}
//结果
动画过程中的frame改变
modelFrame ---- (80.0, 80.0, 100.0, 100.0)
presentationFrame ---- (30.23992655798793, 30.83974295295775, 100.0, 100.0)
动画过程中的frame改变
modelFrame ---- (80.0, 80.0, 100.0, 100.0)
presentationFrame ---- (31.154924668371677, 34.04223633930087, 100.0, 100.0)
动画过程中的frame改变
modelFrame ---- (80.0, 80.0, 100.0, 100.0)
presentationFrame ---- (32.20869496464729, 37.730432376265526, 100.0, 100.0)
动画过程中的frame改变
modelFrame ---- (80.0, 80.0, 100.0, 100.0)
presentationFrame ---- (33.218724578619, 41.26553602516651, 100.0, 100.0)
......
......
......
动画过程中的frame改变
modelFrame ---- (80.0, 80.0, 100.0, 100.0)
presentationFrame ---- (147.16021299362183, 440.0607454776764, 100.0, 100.0)
动画过程中的frame改变
modelFrame ---- (80.0, 80.0, 100.0, 100.0)
presentationFrame ---- (148.15154314041138, 443.5304009914398, 100.0, 100.0)
动画过程中的frame改变
modelFrame ---- (80.0, 80.0, 100.0, 100.0)
presentationFrame ---- (149.15820837020874, 447.0537292957306, 100.0, 100.0)
动画结束后,默认情况A就被移除了,这时候P身上啥也没有了。那么在下一次屏幕刷新的时候,没有其他的信息告诉P它应该如何绘制,所以它就又开口了“M你在哪里啊”,M说“我在这里,快过来”。于是P就回到M那里去了,继续由M指挥,所以动画结束后,你会看到视图又回去了。
如果想要P在动画结束后就停在当前状态而不回到M的状态,我们就需要给A设置两个属性,一个是A.isRemovedOnCompletion = fase;表示动画结束后A依然影响着P,另一个是A.fillMode = .forwards,这两行代码将会让A控制住P在动画结束后保持不变(不会回到M的状态),代码是这个样子:
override func viewDidLoad() {
super.viewDidLoad()
//redLayer
redLayer.backgroundColor = UIColor.red.cgColor
redLayer.frame = CGRect(x: 80, y: 80, width: 100, height: 100)
self.view.layer.addSublayer(redLayer)
//动画
let annimation = CABasicAnimation.init(keyPath: "position")
annimation.duration = 2
annimation.fromValue = CGPoint(x: 80, y: 80)
annimation.toValue = CGPoint(x: 200, y: 500)
annimation.isRemovedOnCompletion = false
annimation.fillMode = .forwards
redLayer.add(annimation, forKey: nil)
}
运行一下,我们的动画终于停在了终点处,就像这样:
??????????????
5.模型与显示的同步
我们可以通过设置isRemovedOnCompletion = false以及fillMode来让动画结束后保持状态,但是此时我们的P和M不同步,我们看到的P是toValue的状态,而M则还是自己原来的状态。举个栗子,我们初始化一个layer,它的状态为1,我们给它加个动画,from是0,to是2,设置fillMode为.forewards,则动画结束后P的状态是2,M的状态是1,这可能会导致一些问题出现。比如你点P所在的位置点不动,因为响应点击的是M。所以我们应该让P和M同步。
在CABasicAnimation的文档中写了这样一句话:如果不设置toValue,则CABasicAnimation会从fromValue到M的值之间进行插值。也就是说,如果不设置toValue,则CABasicAnimation会把M的值作为toValue,所以我们就可以在加动画的时候只设置fromValue,再手动修改M的值到你想要动画停止的那个状态就保持同步了。
我们可以扩展到任意的CAAnimation对象,比如CAKeyFrameAnimation,都可以通过设置M的值到动画结束的状态来保持P和M的同步。
所以我们的代码可能就会写成这样:
override func viewDidLoad() {
super.viewDidLoad()
//redLayer
redLayer.backgroundColor = UIColor.red.cgColor
redLayer.frame = CGRect(x: 80, y: 80, width: 100, height: 100)
redLayer.position = CGPoint(x: 200, y: 500)
self.view.layer.addSublayer(redLayer)
//动画
let annimation = CABasicAnimation.init(keyPath: "position")
annimation.duration = 2
annimation.fromValue = CGPoint(x: 80, y: 80)
// annimation.toValue = CGPoint(x: 200, y: 500)
// annimation.isRemovedOnCompletion = false
// annimation.fillMode = .forwards
redLayer.add(annimation, forKey: nil)
}
当我们写redLayer.position = CGPoint(x: 200, y: 500)?的时候,只是对M赋值,再次强调,它不会影响P的显示,而当P想要显示的时候,它已经被A控制了,所以P会从一开始就在position为(80,80)那里然后动画地移动到(200,500),不会出现在(200,500)那里闪一下的情况。 运行一下,效果就是这样:
?
6.总结
在CALayer内部,它控制着两个属性:presentation(以下称为P)和model(以下称为M)。P只负责显示,M只负责数据的存储和获取。我们对layer的各种属性赋值比如frame,实际上是直接对M的属性赋值,而P将在每一次屏幕刷新的时候回到M的状态。比如此时M的状态是1,P的状态也是1,然后我们把M的状态改为2,那么此时P还没有过去,也就是我们看到的状态P还是1,在下一次屏幕刷新的时候P才变为2。而我们几乎感知不到两次屏幕刷新之间的间隙,所以感觉就是我们一对M赋值,P就过去了。P就像是瞎子,M就像是瘸子,瞎子背着瘸子,瞎子每走一步(也就是每次屏幕刷新的时候)都要去问瘸子应该怎样走(这里的走路就是绘制内容到屏幕上),瘸子没法走,只能指挥瞎子背着自己走。可以简单的理解为:一般情况下,任意时刻P都会回到M的状态。而当一个CAAnimation(以下称为A)加到了layer上面后,A就把M从P身上挤下去了。现在P背着的是A,P同样在每次屏幕刷新的时候去问他背着的那个家伙,A就指挥它从fromValue到toValue来改变值。而动画结束后,A会自动被移除,这时P没有了指挥,就只能大喊“M你在哪”,M说我还在原地没动呢,于是P就顺声回到M的位置了。这就是为什么动画结束后我们看到这个视图又回到了原来的位置,是因为我们看到在移动的是P,而指挥它移动的是A,M永远停在原来的位置没有动,动画结束后A被移除,P就回到了M的怀里。
动画结束后,P会回到M的状态(当然这是有前提的,因为动画已经被移除了,我们可以设置fillMode来继续影响P),但是这通常都不是我们动画想要的效果。我们通常想要的是,动画结束后,视图就停在结束的地方,并且此时我去访问该视图的属性(也就是M的属性),也应该就是当前看到的那个样子。按照官方文档的描述,我们的CAAnimation动画都可以通过设置model到动画结束的状态来实现P和M的同步。
|