呈現與模型 CALayer的屬性行為其實很不正常,因為改變一個圖層的屬性並沒有立刻生效,而是通過一段時間漸變更新。這是怎麼做到的呢? 當你改變一個圖層的屬性,屬性值的確是立刻更新的(如果你讀取它的數據,你會發現它的值在你設置它的那一刻就已經生效了),但是屏幕上並沒有馬上發生改變。這是因為你設置的屬性 ...
呈現與模型
CALayer
的屬性行為其實很不正常,因為改變一個圖層的屬性並沒有立刻生效,而是通過一段時間漸變更新。這是怎麼做到的呢?
當你改變一個圖層的屬性,屬性值的確是立刻更新的(如果你讀取它的數據,你會發現它的值在你設置它的那一刻就已經生效了),但是屏幕上並沒有馬上發生改變。這是因為你設置的屬性並沒有直接調整圖層的外觀,相反,他只是定義了圖層動畫結束之後將要變化的外觀。
當設置CALayer
的屬性,實際上是在定義當前事務結束之後圖層如何顯示的模型。Core Animation扮演了一個控制器的角色,並且負責根據圖層行為和事務設置去不斷更新視圖的這些屬性在屏幕上的狀態。
我們討論的就是一個典型的微型MVC模式。CALayer
是一個連接用戶界面(就是MVC中的view)虛構的類,但是在界面本身這個場景下,CALayer
的行為更像是存儲了視圖如何顯示和動畫的數據模型。實際上,在蘋果自己的文檔中,圖層樹通常都是值的圖層樹模型。
在iOS中,屏幕每秒鐘重繪60次。如果動畫時長比60分之一秒要長,Core Animation就需要在設置一次新值和新值生效之間,對屏幕上的圖層進行重新組織。這意味著CALayer
除了“真實”值(就是你設置的值)之外,必須要知道當前顯示在屏幕上的屬性值的記錄。
每個圖層屬性的顯示值都被存儲在一個叫做呈現圖層的獨立圖層當中,他可以通過-presentationLayer
方法來訪問。這個呈現圖層實際上是模型圖層的複製,但是它的屬性值代表了在任何指定時刻當前外觀效果。換句話說,你可以通過呈現圖層的值來獲取當前屏幕上真正顯示出來的值(圖7.4)。
我們在第一章中提到除了圖層樹,另外還有呈現樹。呈現樹通過圖層樹中所有圖層的呈現圖層所形成。註意呈現圖層僅僅當圖層首次被提交(就是首次第一次在屏幕上顯示)的時候創建,所以在那之前調用-presentationLayer
將會返回nil
。
你可能註意到有一個叫做–modelLayer
的方法。在呈現圖層上調用–modelLayer
將會返回它正在呈現所依賴的CALayer
。通常在一個圖層上調用-modelLayer
會返回–self
(實際上我們已經創建的原始圖層就是一種數據模型)。
圖7.4 一個移動的圖層是如何通過數據模型呈現的
大多數情況下,你不需要直接訪問呈現圖層,你可以通過和模型圖層的交互,來讓Core Animation更新顯示。兩種情況下呈現圖層會變得很有用,一個是同步動畫,一個是處理用戶交互。
- 如果你在實現一個基於定時器的動畫(見第11章“基於定時器的動畫”),而不僅僅是基於事務的動畫,這個時候準確地知道在某一時刻圖層顯示在什麼位置就會對正確擺放圖層很有用了。
- 如果你想讓你做動畫的圖層響應用戶輸入,你可以使用
-hitTest:
方法(見第三章“圖層幾何學”)來判斷指定圖層是否被觸摸,這時候對呈現圖層而不是模型圖層調用-hitTest:
會顯得更有意義,因為呈現圖層代表了用戶當前看到的圖層位置,而不是當前動畫結束之後的位置。
我們可以用一個簡單的案例來證明後者(見清單7.7)。在這個例子中,點擊屏幕上的任意位置將會讓圖層平移到那裡。點擊圖層本身可以隨機改變它的顏色。我們通過對呈現圖層調用-hitTest:
來判斷是否被點擊。
如果修改代碼讓-hitTest:
直接作用於colorLayer而不是呈現圖層,你會發現當圖層移動的時候它並不能正確顯示。這時候你就需要點擊圖層將要移動到的位置而不是圖層本身來響應點擊(這就是為什麼用呈現圖層來響應交互的原因)。
清單7.7 使用presentationLayer
圖層來判斷當前圖層位置
1 @interface ViewController () 2 3 @property (nonatomic, strong) CALayer *colorLayer; 4 5 @end 6 7 @implementation ViewController 8 9 - (void)viewDidLoad 10 { 11 [super viewDidLoad]; 12 //create a red layer 13 self.colorLayer = [CALayer layer]; 14 self.colorLayer.frame = CGRectMake(0, 0, 100, 100); 15 self.colorLayer.position = CGPointMake(self.view.bounds.size.width / 2, self.view.bounds.size.height / 2); 16 self.colorLayer.backgroundColor = [UIColor redColor].CGColor; 17 [self.view.layer addSublayer:self.colorLayer]; 18 } 19 20 - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event 21 { 22 //get the touch point 23 CGPoint point = [[touches anyObject] locationInView:self.view]; 24 //check if we've tapped the moving layer 25 if ([self.colorLayer.presentationLayer hitTest:point]) { 26 //randomize the layer background color 27 CGFloat red = arc4random() / (CGFloat)INT_MAX; 28 CGFloat green = arc4random() / (CGFloat)INT_MAX; 29 CGFloat blue = arc4random() / (CGFloat)INT_MAX; 30 self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor; 31 } else { 32 //otherwise (slowly) move the layer to new position 33 [CATransaction begin]; 34 [CATransaction setAnimationDuration:4.0]; 35 self.colorLayer.position = point; 36 [CATransaction commit]; 37 } 38 } 39 @end 40 41View Code