文檔地址: 《View Programming Guide for iOS》
View and Window Architecture
-
視圖繪制周期
UIView 類使用了請求式繪制模型來展示內容。當一個視圖第一次出現在屏幕上時,系統要求它繪制自己的內容。系統截取視圖內容的一個快照,并且將這個快照用于視圖的可視化呈現。如果視圖內容永遠不改變,那么這個視圖的繪圖代碼可能永遠都不會再次調用。這個快照的圖片在大部分涉及到該視圖的操作中被重復使用。如果改變了視圖內容,則需要通知系統視圖發生了改變。之后視圖會重復繪制過程并且為新的繪制結果截取一個快照。
當視圖內容發生改變時,不需要直接重繪這些改變。相反,通過調用函數 setNeedsDisplay
或者 setNeedsDisplayInRect:
來使當前視圖無效。這些函數會告訴系統視圖的內容發生改變并且需要在下次時機到來時重繪。系統會一直等到當前的 run loop 結束后,才會開始任何繪制操作。這個延遲,給了你一個機會去廢止多個視圖,從當前視圖層級中添加或者刪除視圖,隱藏視圖,重設視圖大小,和重定位視圖。所有的這些改變稍后會再同一時間呈現。
備注:改變視圖的幾何結構并不會讓系統自動重繪視圖內容。視圖的 contentMode
屬性決定了視圖幾何結構的改變該如何解析。大部分的 content modes 只是在視圖的邊界中拉伸或者重定位已經存在的視圖快照而不需要重新創建一個快照。
當繪制視圖內容的時刻到來時,真正的繪制過程會根據視圖和它的配置而有所不同。系統視圖通常是實現自己的私有繪圖函數來重繪內容。這些一樣的系統視圖通常會暴露一些接口,以便能用來配置視圖實際的外觀。對于自定義的 UIView 的子類,典型的應該重寫視圖的 drawRect:
函數,使用它來繪制視圖內容。當然也存在一些其他的方法去提供視圖的內容,比如直接設置內容下的圖層。但是重寫 drawRect:
函數是使用最多的技術。
UIKit 框架的坐標原點位于左上角,x 軸向右延伸,y 軸向下延伸。而 Core Graphics 和 OpenGL ES 的坐標系統原點則在左下角,y 軸向上延伸,x 軸向右延伸。
UIView 屬性中的 frame和 center 是相對于父視圖的坐標系統的。而 bounds屬性相對于自身的坐標系統,故 bounds 默認的 point 位置是(0,0),大小與 frame 相同
改變視圖的 transform 屬性時,所有變形都是相對于視圖中心點也就是 center 屬性的。
-
在視圖的 drawRect: 方法中,可以使用仿射變換來定位和確定需要繪制的元素。相比于在視圖的某個地點固定一個對象的位置,相對于一個固定點(通常是(0,0))來創建每個對象是更為簡單的。在繪制之前使用 transform 就能做到這點。在這種情況下,如果視圖中的對象位置發生改變,只需要修改這個 transform 即可,這比在新的位置重新創建對象要快速并且花銷更小。可以使用 CGContextGetCTM 函數來檢索圖形上下文的仿射變換矩陣,在繪制過程中也可以使用 Core Graphics 的相關函數來設置 CTM。
CTM(current transformation matrix) 是任何時候都被使用的仿射變換,當操作的是整個視圖時,CTM 就是視圖的 transform 屬性。在 drawRect: 方法中, CTM 與當前活動的圖形上下文有關
當一個視圖的 transform 屬性不是 identity transform 時,這個視圖的 frame 屬性就是未定義并且必須被忽視的。此時,你必須使用視圖的 bounds 和 center 屬性來獲得視圖的大小和位置。該視圖的任何子視圖的 frame 矩形依然是有效的,因為它們是基于父視圖的 bounds 屬性的。
一個點并不一定對應著屏幕上的一個像素
對于顯式定義了 drawRect: 方法的視圖來說,UIKit 負責調用這個方法。這個方法中的實現應該盡可能快地重繪視圖的指定區域并且不應該做別的任何事情。不要在這里做額外的布局,也不要改變應用的數據模型。這個方法的唯一目的就是更新視圖的可視內容。
自定義視圖需要重寫的事件處理函數有
touchesBegan:withEvent:
,touchesMoved:withEvent:
,touchesEnded:withEvent:
,touchesCancelled:withEvent:
如果使用了手勢識別來處理事件,則不需要重寫這些函數。如果視圖不包含任何子視圖或者它的尺寸不發生改變,也不需要重寫layoutSubviews
函數。最后,當視圖內容在運行時發生改變,同時使用了 UIKit 或者 Core Graphics 來繪制圖形,則需要重寫drawRect:
函數。
Windows
每個 iOS 應用程序至少包含一個窗口。窗口通常座位一個或者多個視圖的空白容器。同時,應用程序也不通過展示新的窗口來改變內容。如果想要這么做,改變窗口最前面的視圖來完成。
當創建窗口時,應該總是將窗口的大小設置為充滿屏幕的邊界。不應該為了容納狀態欄或者其他元素而減去窗口大小。無論何時,狀態欄總是浮在窗口的上面的。所以應該是放入到窗口中的視圖來縮減大小去適應狀態欄。如果是使用視圖控制器,則視圖控制器應該自動處理視圖大小。
窗口有等級概念,每個
UIWindow
對象都有一個可配置的windowLevel
屬性。通常不需要改變應用程序的窗口等級。新的窗口在創建時,會自動指派為正常窗口等級。高窗口等級是出現在應用程序內容之上的必要信息,比如系統狀態欄和 alert 消息。雖然可以手動將窗口設置為這樣的等級,但當使用到特殊接口時,通常系統會做好這些事情。舉例來說,當顯示隱藏狀態欄,或者顯示一個 alert 視圖時,系統會自動創建必要的窗口去顯示這些內容。當應用程序進入到后臺時,窗口改變通知并不會被傳遞。因為當程序進入后臺時盡管窗口不在屏幕上顯示了,但在應用程序環境中,窗口依然被認為是可見的。
retina 屏的 iOS 設備可以外接顯示設備。
Views
- 使用編程方法來創建視圖時,視圖創建代碼一般放在視圖控制器的
loadView
函數中。無論是使用編程或者 nib 文件來創建視圖,都可以在viewDidLoad
函數中添加視圖的配置代碼。 - 父視圖會自動 retain 子視圖,所以當添加了一個子視圖后,release 子視圖的操作是安全的。事實上,推薦這么做,因為它能防止應用程序保持太多的視圖而導致的內存泄露。記住,如果從父視圖中移除了子視圖后,還想繼續使用子視圖,必須對子視圖做 retain 操作。
removeFromSuperview
函數會在子視圖從父視圖中移除后,自動釋放子視圖。如果沒有在下一個時間循環周期前做 retain 操作,這個視圖將會被釋放。 - UIView的
window
屬性代表當前正在顯示的視圖所在的窗口。對于當前在屏幕上顯示的視圖來說,窗口對象就是它們所在視圖層次的根視圖。 - 如果隱藏的視圖是 first responder,這個視圖不會自動的取消自己 first responder 的狀態。以 first responder 為目標的事件依然會被傳遞到這個隱藏的視圖。為了防止這種情況發生,應該在隱藏視圖時,強制使其取消 first responder 狀態。
- 當包含了旋轉因子的視圖做矩形轉換時,看如下的圖:
如果一個視圖的
transform
屬性不是 identity transform,那么它的 frame 和 autoresizing 行為結果都是未定義的。視圖在初始化過程之前調用 UIView 類方法
layerClass
,并且使用返回的類來創建 layer 對象。此外,視圖總是會指定自己本身作為 layer 對象的代理。在這點上,視圖擁有著圖層,并且視圖和圖層之前的關系必須不能改變。也就是說,不能指定相同的視圖作為另一個圖層對象的代理。改變這種所屬關系或者代理關系,都可能會導致視圖繪制的錯誤,并且應用程序存在潛在的 crash 問題。通過創建 UIView 的子類,重寫
layerClass
類函數可以改變創建圖層時的默認的 CALayer 類。自定義的 layer 不接收事件,也不參與到 responder chain 中,但是它們繪制自身,并且根據 Core Animation 的規則,在他們的父視圖或者父圖層中響應大小變化。
CGRectGetMidX(),CGRectGetMidY() 兩個函數可以分別得到一個 frame 的中心點的 x 坐標和 y 坐標。
CALayer 的屬性
position
就是中心點,和 UIView 的屬性center
效果相同。自定義的視圖類,如果是通過代碼來創建,則需要重寫
initWithFrame:
初始化函數。而若是從 nib 文件中加載,則需要重寫initWithCoder:
函數。注意:nib 加載的視圖并不會調用initWithFrame:
函數。視圖默認的行為是一次只響應一個 touch。如果用戶按下了第二個手指,系統會忽視這個 touch 事件并且不會將它報告給視圖。如果希望在視圖的事件處理函數中跟蹤多手指手勢,需要設置視圖的
multipleTouchEnabled
屬性為 YES 來使多點觸摸事件生效。
轉自:WebFrogs 的博客