iOS中應用程序基本上都是基於MVC模式開發的。UIView就是模型-視圖-控制器中的視圖,在iOS終端上看到的、摸到的都是UIView。
UIView在屏幕上定義了一個矩形區域和管理區域內容的接口。在運行時,一個視圖對象控制該區域的渲染;UIView繼承自UIResponder,UIResponder是用來響應事件的類,UIView也具有響應事件的能力。所以說UIView具有三個基本的功能,繪制內容並管理內容的布局,響應用戶交互,動畫。正是因為UIView具有這些功能,它才能擔當起MVC中視圖層的作用。
在開發中可以使用UIKit框架中已經提供的視圖組件他們大多繼承自UIView,當然也可以通過繼承UIView定義自己的視圖。只有主線程才能更新UI。UIKit框架內的組件包括UIView及其子類的所有操作都必須在主線程中進行,否則會出現不可預知的問題。
本章主要介紹渲染和內容管理。
1、創建
- (id)initWithFrame:(CGRect)frame; 通過frame創建一個view。
2、幾何屬性
視圖對象使用frame, bounds和center屬性來跟蹤它的尺寸和位置:
frame屬性指定了在視圖的尺寸和在父視圖中的位置。
center屬性指定了視圖的中點在父視圖的位置。
bounds屬性指定了在視圖本地坐標系統中視圖的尺寸。默認原點是(0,0)。
可以通過center和bounds計算得到frame,反之同理。frame.origin.x == center.x - bounds.size.width / 2; frame.size == bounds.size;
transform屬性用於視圖的動畫,通過操作transform實現視圖的旋轉、縮放。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
這兩個方法主要用於響應者鏈, 用於確定視圖是否為第一響應者,當用戶碰觸屏幕的時候系統會生成一個碰觸點。為了確認哪個控件是這個碰觸的第一響應者系統會調用window的hitTest方法,hitTest會調用pointInside方法判斷觸碰點是否在當前視圖的區域內。如果在返回YES,則調用該試圖所有子試圖的hitTest方法;如果不在則返回NO,hitTest函數直接返回nil;如果pointInside方法返回YES,且沒有子視圖或者子視圖的hiteTest方法返回都為nil則此視圖為第一響應者,第一響應者是響應者鏈的開端
點轉換方法。比如一個view上有一個button,在button上有一個點p,這個點相對button的坐標知道了,想知道這個點在這個view上的坐標,用這兩個api去轉換。
- (CGPoint)convertPoint:(CGPoint)point toView:(UIView *)view
[button convertPoint:p toView:view] 這樣得到view上的點。
- (CGPoint)convertPoint:(CGPoint)point fromView:(UIView *)view
[view convertPoint:p fromView:button];
同點轉換方法,轉換一個矩形
- (CGRect)convertRect:(CGRect)rect toView:(UIView *)view
- (CGRect)convertRect:(CGRect)rect fromView:(UIView *)view
- (CGSize)sizeThatFits:(CGSize)size
返回最合適的尺寸,size是首選尺寸。只返回尺寸不會改變尺寸,如一個UILabel的text發生改變,需要UILabel的bounds調整,調用這個方法會返回一個最合適的值。
- (void)sizeToFit
按照sizeThatFits的返回值重新設置視圖的bounds。
autoresizingMask 當父視圖的bounds發生改變時通過這個屬性決定如何調整自己的frame。缺省的值為UIViewAutoresizingNone,表示當父視圖bound變化時,自己相對於父視圖的frame不變。可以通過|設置多個規則,如:UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight
UIViewAutoresizingNone
UIViewAutoresizingFlexibleLeftMargin 到屏幕左邊的距離隨着父視圖的寬度按比例改變
UIViewAutoresizingFlexibleWidth 視圖的寬度隨着父視圖的寬度按比例改變
UIViewAutoresizingFlexibleRightMargin 到屏幕右邊的距離隨着父視圖的寬度按比例改變
UIViewAutoresizingFlexibleTopMargin 到屏幕頂部的距離隨着父視圖的高度按比例改變
UIViewAutoresizingFlexibleHeight 視圖的高度隨着父視圖的高度等比例改變
UIViewAutoresizingFlexibleBottomMargin
autoresizesSubviews 默認值YES,如果設置為NO那么該視圖的所有直接子視圖的frame自動調整行為將被忽略,也就是說無論子視圖的autoresizingMask屬性設置成什么都相當於置為UIViewAutoresizingNone。
3、視圖層次
UIView除了提供自己的內容外,還可以作為一個視圖容器。當一個視圖包含其他視圖時,就在兩者之間建立了一個父子關系。在視覺上子視圖隱藏了父視圖的內容,如果一個子視圖是完全不透明的,那么子視圖所在區域就完全遮擋了父視圖的相應區域。如果子視圖是部分透明的那么兩個視圖在顯示上就混合在了一起。父子視圖關系也會影響一些視圖行為,改變父視圖的尺寸也會相應的改變子視圖的尺寸。隱藏父視圖,改變父視圖的alpha值,轉換父視圖都會影響到子視圖。
UIView通過NSArray管理子視圖的。通過屬性subviews可以訪問視圖的所有子視圖。通過NSArray的特點可以得出兩點:
(1)子視圖的引用計數會+1,當父視圖釋放的時候子視圖的引用計數-1。
(2)子視圖是有序的,后加入的子視圖會疊在上一個子視圖之上。
- (void)addSubview:(UIView *)view 方法增加一個子視圖。
操作子視圖的方法:
- (void)insertSubview:(UIView *)view atIndex:(NSInteger)index 在指定的層級插入一個子視圖。最底層是0;最頂層就是subviews count,相當於addSubview
- (void)insertSubview:(UIView *)b belowSubview:(UIView *)c b成為子視圖並且在已有的子視圖c的下面
- (void)insertSubview:(UIView *)b aboveSubview:(UIView *)c b成為子視圖並且在已有的子視圖c的上面
- (void)exchangeSubviewAtIndex:(NSInteger)index1 withSubviewAtIndex:(NSInteger)index2 交換兩個子視圖的層級。
- (void)bringSubviewToFront:(UIView *)view 視圖上升到最頂層。
- (void)sendSubviewToBack:(UIView *)view 視圖下降到最底層
- (void)removeFromSuperview 刪除所有子視圖。
superview 屬性用於獲取當前視圖的父視圖。
- (void)setNeedsLayout
調用此方法通知系統view的內容需要重新繪制,會異步調用layoutSubviews方法,view會在下一個drawing周期繪制。如果只是簡單的改變view的幾何形狀或者當前視圖並沒有在窗口中顯示,系統可能不會調用layoutSubviews方法。
- (void)layoutIfNeeded
調用此方法通知系統view的內容需要重新繪制,調用layoutSubviews方法。與setNeedLayout不同的時不會等到下一個drawing周期,會立即調用layoutSubviews方法。
- (void)layoutSubviews
此方法的缺省實現是空。子類可以去重寫此方法當需要更精確的subviews布局。當subviews的autoresizes行為不能滿足要求時才去重寫此方法。可以在實現中直接設置subviews的frame。此方法不能被直接調用。如果想要在下一個drawing周期去更新view布局,應該調用setNeedsLayout方法;如果想立即更新view的布局,應該調用layoutIfNeeded方法。
layoutSubviews之所以不能被直接調用的原因可能是系統在繪制時需要進行一些操作並判斷需不需要調用layoutSubviews的方法,並且在一次運行循環(run loop)中無論調用多少次setNeedsLayout都只調用一次layoutSubviews。這樣就避免了資源的重復調用。
4、渲染屬性
clipsToBounds 當值為YES時,子視圖如果超過了當前視圖的區域,超出的部分就會被裁掉。默認值是NO,也就是說當子視圖顯示區域大於主視圖的時候還是正常顯示不會裁剪。
backgroundColor 設置背景色
alpha 設置視圖的透明度,當為1時不透明,為0時完全透明,既隱藏。默認值是1。alpha值會影響到子視圖。如果想讓父視圖半透明而子視圖不受影響可以設置父視圖的backgroundColor = [UIColor colorWithWhite:0 alpha:0.8],這樣就ok了。
opaque 給繪圖系統提供一個性能優化開關。如果該值為YES,那么繪圖在繪制該視圖的時候把整個視圖當作不透明對待。這樣,繪圖系統在執行繪圖過程中會優化一些操作並提升系統性能;如果是設置為NO, 繪圖系統將其和其他內容平等對待,不去做優化操作。為了性能方面的考量,默認被置為YES(意味着‘優化’)。
一個不透明視圖需要整個邊界里面的內容都是不透明的。基於這個原因,opaque設置為YES,要求對應的alpha必須為1.0。如果一個UIView實例opaque被設置為YES, 而同時它又沒有完全填充它的邊界(bounds),或者它包含了整個或部分的透明的內容視圖,那么將會導致未知的結果。
clearsContextBeforeDrawing 決定繪制前是否清屏,默認為YES。用於提高描畫性能,特別是在可滾動的視圖中。當這個屬性被設置為YES時,UIKIt會在調用drawRect:方法之前,把即將被該方法更新的區域填充為透明的黑色。將這個屬性設置為NO可以取消相應的填充操作,view中原有內容會保留。
hidden 視圖是否隱藏,默認為NO(顯示),YES為隱藏。
contentMode 視圖內容的填充方式。類型是UIViewContentMode。默認值是UIViewContentModeScaleToFill,填充到整個視圖區域,不等比例拉伸。
typedef NS_ENUM(NSInteger, UIViewContentMode) {
UIViewContentModeScaleToFill, 填充到整個視圖區域,不等比例拉伸
UIViewContentModeScaleAspectFit, 長寬等比填充視圖區域,當某一個邊到達視圖邊界的時候就不再拉伸,保證內容的長寬比是不變的同時盡可能的填充視圖區域。
UIViewContentModeScaleAspectFill, 長寬等比填充視圖區域,當某一個邊到達視圖邊界的時候還繼續拉伸,直到另一個方向達到視圖邊界。內容的長寬比不變的同時填滿整個視圖區域,不顯示超過的部分。
UIViewContentModeRedraw, 重繪視圖邊界
UIViewContentModeCenter, 視圖居中
UIViewContentModeTop, 視圖頂部對齊
UIViewContentModeBottom, 視圖底部對齊
UIViewContentModeLeft, 視圖左側對齊
UIViewContentModeRight, 視圖右側對齊
UIViewContentModeTopLeft, 視圖左上角對齊
UIViewContentModeTopRight, 視圖右上角對齊
UIViewContentModeBottomLeft, 視圖左下角對齊
UIViewContentModeBottomRight, 視圖右下角對齊
};
以上模式中凡是沒有帶scale的,內容bounds超出視圖bounds時只有未超出部分才在視圖bounds中顯示。
下圖很好的解釋了contentMode效果
enum { UIViewAutoresizingNone = 0, UIViewAutoresizingFlexibleLeftMargin = 1 << 0, 到屏幕左邊的距離隨着父視圖的寬度按比例改變 UIViewAutoresizingFlexibleWidth = 1 << 1, 視圖的寬度隨着父視圖的寬度按比例改變 UIViewAutoresizingFlexibleRightMargin = 1 << 2, 到屏幕右邊的距離隨着父視圖的寬度按比例改變 UIViewAutoresizingFlexibleTopMargin = 1 << 3, 到屏幕頂部的距離隨着父視圖的高度按比例改變 UIViewAutoresizingFlexibleHeight = 1 << 4, 視圖的高度隨着父視圖的高度等比例改變 UIViewAutoresizingFlexibleBottomMargin = 1 << 5 }; typedef NSUInteger UIViewAutoresizing;
autoresizesSubviews 默認值YES,如果設置為NO那么該視圖的所有直接子視圖的frame自動調整行為將被忽略,也就是說無論子視圖的autoresizingMask屬性設置成什么都相當於置為UIViewAutoresizingNone。
contentStretch 用於制定哪部分是可拉伸的,取值在 0.0到1.0之間。下面用一個例子解釋contentStretch是如何工作的。
[imageView setContentStretch:CGRectMake(150.0/300.0, 100.0/200.0, 10.0/300.0, 10.0/200.0)];
image.png的大小是 200 x 150 ;
mageView的frame是(0,0,300,200);
150.0/300.0表示x軸上,前150個像素不進行拉伸。
100.0/200.0表示y軸上,前100個像素不進行拉伸。
10.0/300.0表示x軸上150后的10個像素(151-160)進行拉伸,直到image.png鋪滿imageView。
10.0/200.0表示y軸上100后的10個像素(101-110)進行拉伸,直到image.png鋪滿imageView。
- (void)setNeedsDisPlay
調用此方法通知系統view需要重新繪制,會異步自動調用drawRect方法。
- (void)setNeedsDisplayInRect:(CGRect)rect
同樣異步調用drawRect方法。在一次運行循環(run loop)中無論調用setNeedsDisplay或setNeedsDisplayInRect多少次,只調用drawRect一次。也是從減少資源開銷的角度考慮的。
- (void)drawRect:(CGRect)rect
此方法的缺省實現是空。子類使用原生的繪制技術(Core Graphics and UIKit)繪制內容時應該重寫此方法,在方法里面寫出自己的drawing code。如果view設置自己的內容用其他的方法,則不需要去重寫此方法。如果直接從UIView對象繼承,實現此方法不需要call super。然而如果從其他的UIView對象繼承,則應該調用super。當view 第一次顯示或者當view的可視的一部分無效時,此方法會調用。此方法不能直接被調用。調用setNeedsDisplay 或者 setNeedsDisplayInRect: 方法會觸發重新繪制
5、視圖繪制周期
UIView類使用一個點播繪制模型來展示內容,當一個視圖第一次出現在屏幕前,系統會要求它繪制自己的內容,在該流程中系統會創建一個快照,這個快照是出現在屏幕中得視圖內容的可見部分。如果從來沒有改變視圖的內容,則這個視圖繪制的方法drawRect可能永遠都不會再被調用。這個快照圖像在大部分涉及到視圖的操作中被重用。
如果改變了視圖的內容,系統也不會馬上重繪視圖。使用setNeedsDisPlay或setNeedsDisplayInRect方法廢除該視圖同時讓系統在稍后重繪視圖。系統等待當前運行循環(run loop)結束,然后開始重繪。這個延遲可以用來廢除多個視圖、增加或刪除視圖、隱藏、重設大小。所有的改變會在稍后一起生效。
什么是run loop?個人認為可以簡單的理解為一個事件的處理過程。例如:用戶點擊屏幕會產生兩個run loop。當用戶按下的時候會產生一個run loop;當用戶抬起的時候會產生另一個run loop。
如果在一個run loop中調用setNeedsDisplayInRect方法,系統會保證在這個run loop結束前調用一次drawRect方法;無論在當前run loop調用setNeedsDisplayInRect方法多少次都只調用一次drawRect。setNeedsDisplayInRect方法如果在一個run loop中刷新不同的區域,最后drawRect方法會將這些區域組合起來一起刷新,組合原則用最小的矩形圈起來所有區域,並刷新這個區域。如果刷新區域是屏幕左下角和右上角兩個點,有可能刷新整個屏幕。
6、相關控件
UIWindow
UIWindow對象是所有UIView的父視圖,UIWindow類是UIView的子類,可以看作是特殊的UIView。一般應用程序只有一個UIWindow對象,即使有多個UIWindow對象,也只有一個UIWindow可以接受到用戶的觸屏事件。UIWindow初始化在appDelegate里。 [self.window makeKeyAndVisible]方法,使window顯示。
UIScreen
UIScreen用於獲取設備屏幕的尺寸。
[UIScreen mainScreen].bounds 獲取整個屏幕的大小;當應用程序有狀態欄時,返回值:{{0, 0}, {320, 480}}
[UIScreen mainScreen].applicationFrame獲取應用程序窗口的大小;當應用程序有狀態欄時,返回值:{{0, 20}, {320, 460}}
UIViewController
UIViewController是MVC中的C控制器,控制管理UIView。UIViewController同UIView的關系相當於相框和相片的關系,相框可以操作相片、替換相片、決定相片的顯隱藏,反之則不行。UIView工作在第一線,向用戶展示表現的內容,並接受用戶的交互;UIViewController負責兩個方面,數據和行為,通過數據更新view,通過行為處理用戶交互操作,注意這里是“處理”而不是“響應”。
UIViewController同UIView一樣繼承與UIResponder,所以同樣處於響應者鏈上,同樣可以接收觸碰等用戶事件。
可以通過下面的方法獲得UIView所屬的UIViewController
- (UIViewController*)viewController { for (UIView* next = [self superview]; next; next = next.superview) { UIResponder* nextResponder = [next nextResponder]; if ([nextResponder isKindOfClass:[UIViewController class]]) { return (UIViewController*)nextResponder; } } return nil; }