UIView的剖析!

   前面說過UIViewController,但是UIView也是在MVC中非常重要的一層 。正是因爲UIView是Iphone下所有界面的基礎,所以官方專門寫了一個文檔“View Programming Guide for iOS”。通過這個可以很好的瞭解UIView的功能。

        先來看看官方API的解釋:The UIView class defines a rectangular area on the screen 

and the interfaces for managing the content in that area.

 At runtime, a view object handles the rendering of any content in its area

 and also handles any interactions with that content.(UIView在屏幕上定義了一個矩形區域和管理區域內容的接口。在運行時,一個視圖對象控制該區域的渲染,同時也控制內容的交互。)。所以說UIView具有三個基本的功能,畫圖和動畫,管理內容的佈局,控制事件。正是因爲UIView具有這些功能,它才能擔當起MVC中視圖層的作用。

         UIView咋看起來很複雜,官方API中各種函數接口,要學過運用庖丁解牛的思想,逐個分析,因爲再複雜的東西都是有簡單的東西構成的。回到剛纔提到的UIView的三個基本功能就可以容易的分離出UIView不同的功能是怎麼組合起來的。首先看視圖最基本的功能顯示和動畫,其實UIView的所有的繪圖和動畫的接口,都是可以用CALayer和CAAnimation實現的,也就是說蘋果公司是不是把CoreAnimation的功能封裝到了UIView中,這個文檔中沒有提到過,也沒法斷言。但是每一個UIView都會包含一個CALayer,並且CALayer裏面可以加入各種動畫。再次我們來看UIView管理佈局的思想其實和CALayer也是非常的接近的。最後控制事件的功能,是因爲UIView繼承了UIResponder。經過上面的分析很容易就可以分解出UIView的本質。UIView就相當於一塊白牆,這塊白牆只是負責把加入到裏面的東西顯示出來而已。

                                                                                              圖1

1.UIView中的CALayer

          UIView的一些幾何特性frame,bounds,center都可以在CALayer中找到替代的屬性,所以如果明白了CALayer的特點,自然UIView的圖層中如何顯示的都會一目瞭然。

          CALayer就是圖層,圖層的功能自然就有渲染圖片, 播放動畫的功能。每當創建一個UIView的時候,系統會自動的創建一個CALayer,但是這個CALayer對象你不能改變,只能修改某些屬性。所以通過修改CALayer,不僅可以修飾UIView的外觀,還可以給UIView添加各種動畫。CALayer屬於CoreAnimation框架中的類,通過Core Animation Programming Guide就可以瞭解很多CALayer中的特點,假如掌握了這些特點,自然也就理解了UIView是如何顯示和渲染的。

          先來看下Core Animation框架中關於layer的解釋:While there are obvious similarities between Core Animation layers and Cocoa views the biggest

 conceptual divergence is that layers do not render directly to the screen.

Where NSView and UIView are clearly view objects in the model-view-controller design pattern,

 Core Animation layers are actually model objects. They encapsulate geometry, timing and visual properties,

 and they provide the content that is displayed,

 but the actual display is not the layer’s responsibility.

Each visible layer tree is backed by two corresponding trees: a presentation tree and a rend tree(非常相似的cocoa視圖和core Animation層最大的區別是core Animation不能直接渲染到屏幕上。UIView和NSView明顯是MVC中的視圖模型,animation layer更像是模型對象。他們封裝了幾何,時間和一些可視的屬性,並且提供了可以顯示的內容,但是實際的顯示並不是layer的職責。每一個層樹的後臺都有兩個響應樹:一個曾現樹和一個渲染樹)。所以很顯然Layer封裝了模型數據,每當更改layer中的某些模型數據中數據的屬性時,曾現樹都會做一個動畫代替,之後由渲染樹負責渲染圖片。

        既然Animation Layer封裝了對象模型中的幾何性質,那麼如何取得這些幾何特性。一個方式是根據Layer中定義的屬性,比如bounds,authorPoint,frame等等這些屬性,其次,Core Animation擴展了鍵值對協議,這樣就允許開發者通過get和set方法,方便的得到layer中的各種幾何屬性。下表是Transform的key paths。例如轉換動畫的各種幾何特性,大都可以通過此方法設定:

[plain] view plaincopy
  1. [myLayer setValue:[NSNumber numberWithInt:0] forKeyPath:@"transform.rotation.x"];  

                   

                                                                                                               圖2                                                           

         雖然CALayer跟UIView十分相似,也可以通過分析CALayer的特點理解UIView的特性,但是畢竟蘋果公司不是用CALayer來代替UIView的,否則蘋果公司也不回設計一個UIView類了。就像官方文檔解釋的一樣,CAlayer層樹是cocoa視圖繼承樹的同等物,它具備UIView的很多共同點,但是Core Animation沒有提供一個 方法展示在窗口。他們必須宿主到UIView中,並且UIView給他們提供響應的方法。所以UIReponder就是UIView的又一個大的特性。

2.UIView繼承的UIResponder

       UIResponder是所有事件響應的基石,官方也提供了一個重要的文檔給開發者參考”Event Handling Guide for iOS”。

       事件(UIEvent)是發給應用程序,告知用戶的行動的。在IOS中事件有三種事件:多點觸摸事件,行動事件,遠程控制事件。三種事件定義如下:

[plain] view plaincopy
  1. typedef enum {  
  2.    
  3.     UIEventTypeTouches,  
  4.    
  5.     UIEventTypeMotion,  
  6.    
  7.     UIEventTypeRemoteControl,  
  8.    
  9. } UIEventType;  
  10.    

 

       再來看下UIReponder中的事件傳遞過程,如下圖所示:

 

                                                      圖3

首先是被點擊的該視圖響應時間處理函數,如果沒有響應函數就逐級的向上面傳遞,直到有響應處理函數,或者該消息被拋棄。至於蘋果公司是如何讓事件消息這樣流動的,在下面的分析中,可以瞭解一些,至於深層的原理還的進一步挖掘。

        這裏重點看三個事件 中的多點觸摸事件,也就是UITouch事件,下圖是UIEvent中封裝的UITouch內容

                                                                                          圖4

          關於UIView的觸摸響應事件中,這裏有一個常常容易迷惑的方法hitTest:WithEvent。先來看官方的解釋:This method traverses the view hierarchy by sending the pointInside:withEvent: message 

to each subview to determine which subview should receive a touch event. 

If pointInside:withEvent: returns YES, then the subview’s hierarchy is traversed;
 otherwise, its branch of the view hierarchy is ignored.

 You rarely need to call this method yourself, 

but you might override it to hide touch events from subviews.(通過發送PointInside:withEvent:消息給每一個子視圖,這個方法遍歷視圖層樹,來決定那個視圖應該響應此事件。如果PointInside:withEvent:返回YES,然後子視圖的繼承樹就會被遍歷;否則,視圖的繼承樹就會被忽略。你很少需要調用這個方法,僅僅需要重載這個方法去隱藏子視圖的事件)。從官方的API上的解釋,可以看出 hitTest方法中,要先調用PointInside:withEvent:,看是否要遍歷子視圖。如果我們不想讓某個視圖響應事件,只需要重載PointInside:withEvent:方法,讓此方法返回NO就行了。不過從這裏,還是不能瞭解到hitTest:WithEvent的方法的用途。

         下面再從”Event Handling Guide for iOS”找答案,Your custom responder can use hit-testing to find the subview or sublayer of itself that is "under” a touch, and then handle the event appropriately。從中可以看出hitTest主要用途是用來尋找那個視圖是被觸摸了。看到這裏對hitTest的調用過程還是一知半解。我們可以實際建立一個工程進行調試。建立一個MyView裏面重載hitTest和pointInside方法:

[plain] view plaincopy
  1. - (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event{  
  2.   
  3. [super hitTest:point withEvent:event];  
  4.   
  5. return self;  
  6.   
  7. }  
  8.   
  9. - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{  
  10.   
  11.     NSLog(@"view pointInside");  
  12.   
  13.     return YES;  
  14.   
  15. }  

然後在MyView中增加一個子視圖MySecondView也重載這兩個方法  

[plain] view plaincopy
  1. - (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event{  
  2.   
  3. [super hitTest:point withEvent:event];  
  4.   
  5. return self;  
  6.   
  7. }  
  8.   
  9. - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{  
  10.   
  11.     NSLog(@"second view pointInside");  
  12.   
  13.     return YES;  
  14.   
  15. }  

        這裏注意[super hitTest:point withEvent:event];必須要包括,否則hitTest無法調用父類的方法,這樣就沒法使用PointInside:withEvent:進行判斷,那麼就沒法進行子視圖的遍歷。當去掉這個語句的時候,觸摸事件就不可能進到子視圖中了,除非你在方法中直接返回子視圖的對象。這樣你在調試的過程中就會發現,每次你點擊一個view都會先進入到這個view的父視圖中的hitTest方法,然後調用super的hitTest方法之後就會查找pointInside是否返回YES如果是,則就把消息傳遞個子視圖處理,子視圖用同樣的方法遞歸查找自己的子視圖。所以從這裏調試分析看,hitTest方法這種遞歸調用的方式就一目瞭然了。

        這個只是說了調試中吻合官方文檔中解釋的部分,但是還有一個問題,就是每個view中hitTest總要調用三個,這個查找了API和很多資料都沒有找到解決的方法,然後google了以下在overflowstack中發現了有人這樣解釋:There are indeed 3 calls to hitTest. It is not clear why,

 but we can surmise by the timestamps on the event

 that the first two calls are to do with completing the previous gesture -

 those timestamps are always very close to whenever the previous touch happened, 

and will be some distance from the current time.   (確實有3次調用hitTest,不清楚爲什麼,但是前兩次調用時裏面的UIEvent中的timestamps屬性和上一次已經完成的手勢有關。這些時間timestamps是如此的接近無論先前的觸摸什麼時候發生,並且和系統當前的時間有一定的間隔)。看到這裏我想到了,”Event Handling Guide for iOS”中曾經解釋,如何區分單擊和雙擊的區別,用的方法很簡單,代碼如下:

[plain] view plaincopy
  1. - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {  
  2.    
  3.     UITouch *aTouch = [touches anyObject];  
  4.    
  5.     if (aTouch.tapCount == 2) {  
  6.    
  7.         [NSObject cancelPreviousPerformRequestsWithTarget:self];  
  8.    
  9.     }  
  10.    
  11. }  
  12.    
  13.    
  14.    
  15. - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {  
  16.    
  17. }  
  18.    
  19.    
  20.    
  21. - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {  
  22.    
  23.     UITouch *theTouch = [touches anyObject];  
  24.    
  25.     if (theTouch.tapCount == 1) {  
  26.    
  27.         NSDictionary *touchLoc = [NSDictionary dictionaryWithObject:  
  28.    
  29.             [NSValue valueWithCGPoint:[theTouch locationInView:self]] forKey:@"location"];  
  30.    
  31.         [self performSelector:@selector(handleSingleTap:) withObject:touchLoc afterDelay:0.3];  
  32.    
  33.     } else if (theTouch.tapCount == 2) {  
  34.    
  35.         // Double-tap: increase image size by 10%"  
  36.    
  37.         CGRect myFrame = self.frame;  
  38.    
  39.         myFrame.size.width += self.frame.size.width * 0.1;  
  40.    
  41.         myFrame.size.height += self.frame.size.height * 0.1;  
  42.    
  43.         myFrame.origin.x -= (self.frame.origin.x * 0.1) / 2.0;  
  44.    
  45.         myFrame.origin.y -= (self.frame.origin.y * 0.1) / 2.0;  
  46.    
  47.         [UIView beginAnimations:nil context:NULL];  
  48.    
  49.         [self setFrame:myFrame];  
  50.    
  51.         [UIView commitAnimations];  
  52.    
  53.     }  
  54.    
  55. }  
  56.    
  57.    
  58.    
  59. - (void)handleSingleTap:(NSDictionary *)touches {  
  60.    
  61.     // Single-tap: decrease image size by 10%"  
  62.    
  63.     CGRect myFrame = self.frame;  
  64.    
  65.     myFrame.size.width -= self.frame.size.width * 0.1;  
  66.    
  67.     myFrame.size.height -= self.frame.size.height * 0.1;  
  68.    
  69.     myFrame.origin.x += (self.frame.origin.x * 0.1) / 2.0;  
  70.    
  71.     myFrame.origin.y += (self.frame.origin.y * 0.1) / 2.0;  
  72.    
  73.     [UIView beginAnimations:nil context:NULL];  
  74.    
  75.     [self setFrame:myFrame];  
  76.    
  77.     [UIView commitAnimations];  
  78.    
  79. }  
  80.    

        所以區別這兩個手勢的思想,就是判斷tapcount如果發現touchEnd的時候tapcount是2就取消第一次執行的動作。但是這一點是否想過,蘋果公司是如何判斷tapcount的,比如說我在屏幕上按了下去,過了一分鐘後鬆開,那麼在touchEnd方法中捕捉到的touch事件和我點擊一下屏幕就起來一樣麼?答案是不一樣的,可以寫程序親自試驗以下,按下去一分鐘再鬆開,這裏沒必要一分鐘了,就幾秒也足夠了,你會發現再touchEnd中tapCount爲0,而點擊一下鬆開的tapCount爲1。還有一種情況就是雙擊,如果我雙擊間隔的時間超過大概4,5秒鐘,再次偵測touchEnd中的tapCount就會發現是1,而正常的雙擊tapCount爲2。這裏和hitTest執行三次,並且前兩次記錄的時間是上一次觸摸手勢的時間,後一次纔是本次觸摸手勢的時間,有沒有關係,官方沒有任何解釋,這裏也只能臆測。是不是用來區分上面所說的情況,也就是說根據這個事件timestamp來改變UITouch中tapCount的次數,還希望那位高手給予解釋。所以上面提到的UIEvent,這個事件爲何能向蘋果官方解釋的那樣流動,這裏也就可見一斑了。


原文地址:http://blog.csdn.net/mengtnt/article/details/6716289


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章