視圖基礎 視圖層次結構 任何應用有且只有一個 UIWindow 對象。 UIWindow 對象就像是一個容器,負責包含應用中的所有的視圖。應用需要在啟動時創建並設置 UIWindow 對象,然後為其添加其他視圖。 加入視窗的視圖會成為該視窗的子視圖。視窗的子視圖還可以有自己的子視圖,從而構成一個以 ...
- 視圖基礎
- 視圖是 UIView 對象,或者其子對象。
- 視圖知道如何繪製自己。
- 視圖可以處理事件,例如觸摸(touch)。
- 視圖會按照層次結構排列,位於視圖層次結構頂端的是應用視窗。
- 視圖層次結構
任何應用有且只有一個 UIWindow 對象。 UIWindow 對象就像是一個容器,負責包含應用中的所有的視圖。應用需要在啟動時創建並設置 UIWindow 對象,然後為其添加其他視圖。
加入視窗的視圖會成為該視窗的子視圖。視窗的子視圖還可以有自己的子視圖,從而構成一個以 UIWindow 對象為根視圖的,類似於樹形結構的視圖層次結構。
視圖層次結構形成之後,系統會將其繪製到屏幕上,繪製過程可以分為兩步:
1. 層次結構中的每個視圖(包括 UIWindow 對象)分別繪製自己。視圖會將自己繪製到圖層( layer )上,每個 UIView 對象都有一個 layer 屬性,指向一個 CALayer 類的對象
2. 所有視圖的圖層何曾一幅圖像,繪製到屏幕上。
獲取當前應用程式的 UIWindow 方法是 UIWindow * keyWindow = [UIApplication sharedApplication].keyWindow;
- 創建 UIView 子類
首先創建一個 UIView 子類。
視圖及其 frame 屬性
在控制器中創建一個 CGRect 結構,然後使用該結構創建一個視圖對象,並將這個視圖對象加入到控制器視圖子視圖上。
#import "ViewController.h" #import "JXHypnosisView.h" // 為創建的子類 @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // 創建 CGRect 結構 CGRect rect = CGRectMake(100, 100, 100, 200); // 創建視圖 JXHypnosisView * firstView = [[JXHypnosisView alloc] initWithFrame:rect]; firstView.backgroundColor = [UIColor redColor]; // 將視圖添加到控制器View上 [self.view addSubview:firstView]; } @end
顯示結果
CGRect 結構包含該另外兩個結構: origin 和 size 。其中 origin 的類型是 CGPoint 結構,該結構包含兩個 float 類型測成員。 size 的類型是 CGSize 結構,該結構也包含兩個 float 類型的成員: width 和 height 。
所以我們創建的視圖對象,在上圖中可以看出 JXHypnosisView 對象的左上角位於父視圖右側 100點 、下方 200點 的位置。此外,因為這個 frame 結構中的 size 是(100,200),所以我們自定義 JXHypnosisView 對象的寬度是 100點 、高度是 200點 。
我們這裡所說的這些值的單位是 點(points),不是 像素(pixels)。如果是像素,那麼在不同的 Retina 顯示屏上顯示的大小是不同的。在 Retina 顯示屏上,一個點是兩個像素高度。(所以在跟美工溝通的時候最好讓他們根據像素來做圖片,並且圖片的像素大小是點的兩倍,或者三倍)。
每個視圖對象都有一個 superview 屬性。將一個視圖作為子視圖加入另一個視圖時,會自動創建相應的反向關聯。
- 在 drawRect: 方法中自定義繪圖
前面我們編寫了一個簡單的自定義的 JXHypnosisView 對象,並且設置了他的一些基本的屬性,如位置,大小,顏色等。在本節中我們將在 drawRect: 方法中編寫繪圖代碼。
視圖根據 drawRect: 方法將自己繪製到圖層上。 UIView 的子類可以覆蓋 drawRect: 方法完成自定義的繪圖任務。例如, UIButton 的 drawRect: 方法預設會在 frame 表示的矩形區域中心畫出一行淺藍色的文字。
覆蓋 drawRect: 後首先應該獲取視圖從 UIView 繼承而來的 bounds 屬性,該屬性定義了一個矩形範圍,表示視圖的繪製區域。
視圖在繪製自己時,會參考一個坐標系, bounds 表示的矩形位於自己的坐標系,而 frame 表示的矩形位於父視圖的坐標系,但是兩個矩形的大小是相同的。
frame 和 bounds 表示的矩形用法不同。前者用於確定與視圖層次結構中其他視圖的相對位置,從而將自己的圖層與其他視圖的圖層正確組合成屏幕上的圖像。而後者屬性用於確定繪製區域,避免將自己繪製到圖層邊界之外(其視圖是相對於自己而言,設置只有寬高有效)。
- 繪製圓形
接下來在 JXHypnosisView 的 drawRect 方法中添加繪圖代碼,畫出一個儘可能大的圓形,但是不能好過視圖的繪製區域。
首先,需要根據視圖的 bounds 屬性找到繪製預期的中心點:
#import "JXHypnosisView.h" @implementation JXHypnosisView - (void)drawRect:(CGRect)rect { CGRect bounds = self.bounds; // 根據bounds計算中心點 CGPoint center; center.x = bounds.origin.x + bounds.size.width / 2.0; center.y = bounds.origin.y + bounds.size.height / 2.0; } @end
然後再比較視圖的寬和高,將較小的值的一般設置為圓形的半徑:
#import "JXHypnosisView.h" @implementation JXHypnosisView - (void)drawRect:(CGRect)rect { CGRect bounds = self.bounds; // 根據bounds計算中心點 CGPoint center; center.x = bounds.origin.x + bounds.size.width / 2.0; center.y = bounds.origin.y + bounds.size.height / 2.0; // 根據視圖的寬高比較中的較小的值計算圓形的半徑 float radius = (MIN(bounds.size.width, bounds.size.height) / 2.0); } @end
- UIBezierPath
UIBezierPath 是用來繪製直線或者曲線的一個類。
首先要創建一個 UIBezierPath 對象:
#import "JXHypnosisView.h" @implementation JXHypnosisView - (void)drawRect:(CGRect)rect { CGRect bounds = self.bounds; // 根據bounds計算中心點 CGPoint center; center.x = bounds.origin.x + bounds.size.width / 2.0; center.y = bounds.origin.y + bounds.size.height / 2.0; // 根據視圖的寬高比較中的較小的值計算圓形的半徑 float radius = (MIN(bounds.size.width, bounds.size.height) / 2.0); UIBezierPath * path = [[UIBezierPath alloc] init]; } @end
接下來我們定義 UIBezierPath 對象需要繪製的路徑。
#import "JXHypnosisView.h" @implementation JXHypnosisView - (void)drawRect:(CGRect)rect { CGRect bounds = self.bounds; // 根據bounds計算中心點 CGPoint center; center.x = bounds.origin.x + bounds.size.width / 2.0; center.y = bounds.origin.y + bounds.size.height / 2.0; // 根據視圖的寬高比較中的較小的值計算圓形的半徑 float radius = (MIN(bounds.size.width, bounds.size.height) / 2.0); UIBezierPath * path = [[UIBezierPath alloc] init]; // 以中心點為圓心,radius的值為半徑,定義一個 0 到 M_PI * 2.0 弧度的路徑(整圓) [path addArcWithCenter:center radius:radius startAngle:0.0 endAngle:M_PI * 2.0 clockwise:YES]; } @end
路徑已經定義好了,但是之定義路徑不會進行實際的繪製。我們還需要向 UIBezierPath 對象發送消息,繪製之前定製的路徑:
#import "JXHypnosisView.h" @implementation JXHypnosisView - (void)drawRect:(CGRect)rect { CGRect bounds = self.bounds; // 根據bounds計算中心點 CGPoint center; center.x = bounds.origin.x + bounds.size.width / 2.0; center.y = bounds.origin.y + bounds.size.height / 2.0; // 根據視圖的寬高比較中的較小的值計算圓形的半徑 float radius = (MIN(bounds.size.width, bounds.size.height) / 2.0); UIBezierPath * path = [[UIBezierPath alloc] init]; // 以中心點為圓心,radius的值為半徑,定義一個 0 到 M_PI * 2.0 弧度的路徑(整圓) [path addArcWithCenter:center radius:radius startAngle:0.0 endAngle:M_PI * 2.0 clockwise:YES]; // 繪製路徑 [path stroke]; } @end
繪製結果:
現在改變圓形的線條的粗細和顏色。
#import "JXHypnosisView.h" @implementation JXHypnosisView - (void)drawRect:(CGRect)rect { CGRect bounds = self.bounds; // 根據bounds計算中心點 CGPoint center; center.x = bounds.origin.x + bounds.size.width / 2.0; center.y = bounds.origin.y + bounds.size.height / 2.0; // 根據視圖的寬高比較中的較小的值計算圓形的半徑 float radius = (MIN(bounds.size.width, bounds.size.height) / 2.0); UIBezierPath * path = [[UIBezierPath alloc] init]; // 以中心點為圓心,radius的值為半徑,定義一個 0 到 M_PI * 2.0 弧度的路徑(整圓) [path addArcWithCenter:center radius:radius startAngle:0.0 endAngle:M_PI * 2.0 clockwise:YES]; // 設置線條寬度為 10 點 path.lineWidth = 10; // 繪製路徑 [path stroke]; } @end
下麵來改變繪製圖形的軌跡顏色
#import "JXHypnosisView.h" @implementation JXHypnosisView - (void)drawRect:(CGRect)rect { CGRect bounds = self.bounds; // 根據bounds計算中心點 CGPoint center; center.x = bounds.origin.x + bounds.size.width / 2.0; center.y = bounds.origin.y + bounds.size.height / 2.0; // 根據視圖的寬高比較中的較小的值計算圓形的半徑 float radius = (MIN(bounds.size.width, bounds.size.height) / 2.0); UIBezierPath * path = [[UIBezierPath alloc] init]; // 以中心點為圓心,radius的值為半徑,定義一個 0 到 M_PI * 2.0 弧度的路徑(整圓) [path addArcWithCenter:center radius:radius startAngle:0.0 endAngle:M_PI * 2.0 clockwise:YES]; // 設置線條寬度為 10 點 path.lineWidth = 10; // 設置繪製顏色為灰色 [[UIColor lightGrayColor] setStroke]; // 繪製路徑 [path stroke]; } @end
運行結果:
這裡我們可以嘗試視圖的 backgroundColor 屬性不會受到 drawRect 中代碼的影響,通常應該將重寫 drawRect 方法的視圖的背景色設置為透明(對應於 clearColor),這樣可以讓視圖只顯示 drawRect 方法中繪製的內容。
#import "ViewController.h" #import "JXHypnosisView.h" @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // 創建 CGRect 結構 CGRect rect = CGRectMake(100, 200, 200, 300); // 創建視圖 JXHypnosisView * firstView = [[JXHypnosisView alloc] initWithFrame:rect]; firstView.backgroundColor = [UIColor redColor]; NSLog(@"%f",firstView.bounds.origin.x); // 將視圖添加到控制器View上 [self.view addSubview:firstView]; }
#import "JXHypnosisView.h" @implementation JXHypnosisView - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { // 設置 JXHypnosisView 對象的背景顏色為透明 self.backgroundColor = [UIColor clearColor]; } return self; } - (void)drawRect:(CGRect)rect { CGRect bounds = self.bounds; // 根據bounds計算中心點 CGPoint center; center.x = bounds.origin.x + bounds.size.width / 2.0; center.y = bounds.origin.y + bounds.size.height / 2.0; // 根據視圖的寬高比較中的較小的值計算圓形的半徑 float radius = (MIN(bounds.size.width, bounds.size.height) / 2.0); UIBezierPath * path = [[UIBezierPath alloc] init]; // 以中心點為圓心,radius的值為半徑,定義一個 0 到 M_PI * 2.0 弧度的路徑(整圓) [path addArcWithCenter:center radius:radius startAngle:0.0 endAngle:M_PI * 2.0 clockwise:YES]; // 設置線條寬度為 10 點 path.lineWidth = 10; // 設置繪製顏色為灰色 [[UIColor lightGrayColor] setStroke]; // 繪製路徑 [path stroke]; } @end
- 繪製同心圓
在 JXHypnosisView 中繪製多個同心圓有兩個方法,第一個方法是創建多個 UIBezierPath 對象,每個對象代表一個圓形;第二個方法是使用一個 UIBezierPath 對象繪製多個圓形,為每個圓形定義一個繪製路徑。很明顯第二種方法更好。
#import "JXHypnosisView.h" @implementation JXHypnosisView - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { // 設置 JXHypnosisView 對象的背景顏色為透明 self.backgroundColor = [UIColor clearColor]; } return self; } - (void)drawRect:(CGRect)rect { CGRect bounds = self.bounds; // 根據bounds計算中心點 CGPoint center; center.x = bounds.origin.x + bounds.size.width / 2.0; center.y = bounds.origin.y + bounds.size.height / 2.0; // 根據視圖的寬高比較中的較小的值計算圓形的半徑 float radius = (MIN(bounds.size.width, bounds.size.height) / 2.0);
// 是最外層圓形成為視圖的外接圓 float maxRadius = hypotf(bounds.size.width, bounds.size.height) / 2.0; UIBezierPath * path = [[UIBezierPath alloc] init]; // 以中心點為圓心,radius的值為半徑,定義一個 0 到 M_PI * 2.0 弧度的路徑(整圓) [path addArcWithCenter:center radius:radius startAngle:0.0 endAngle:M_PI * 2.0 clockwise:YES];
for (float currentRadius = maxRadius; currentRadius > 0; currentRadius -= 20) { [path addArcWithCenter:center radius:currentRadius startAngle:0.0 endAngle:M_PI * 2.0 clockwise:YES]; } // 設置線條寬度為 10 點 path.lineWidth = 10; // 設置繪製顏色為灰色 [[UIColor lightGrayColor] setStroke]; // 繪製路徑 [path stroke]; } @end
運行結果:可以看到我們已經畫出了一些列的同心圓,但是屏幕右邊多出了一條奇怪的線條
這是因為單個 UIBezierPath 對象將多個路徑(每個路徑可以畫出一個圓形)連接起來,形成了一個完成的路徑。可以將 UIBezierPath 對象想象成一支在紙上畫畫的鉛筆-但是當我們繪製完成一個圓形之後去繪製另外一個圓形時,鉛筆並沒有抬起,所以才會出現一條很奇怪的線條。
#import "JXHypnosisView.h" @implementation JXHypnosisView - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { // 設置 JXHypnosisView 對象的背景顏色為透明 self.backgroundColor = [UIColor clearColor]; } return self; } - (void)drawRect:(CGRect)rect { CGRect bounds = self.bounds; // 根據bounds計算中心點 CGPoint center; center.x = bounds.origin.x + bounds.size.width / 2.0; center.y = bounds.origin.y + bounds.size.height / 2.0; // 是最外層圓形成為視圖的外接圓 float maxRadius = hypotf(bounds.size.width, bounds.size.height) / 2.0; UIBezierPath * path = [[UIBezierPath alloc] init]; for (float currentRadius = maxRadius; currentRadius > 0; currentRadius -= 20) { // 用來設置繪製起始位置 [path moveToPoint:CGPointMake(center.x + currentRadius, center.y)]; [path addArcWithCenter:center radius:currentRadius startAngle:0.0 endAngle:M_PI * 2.0 clockwise:YES]; } // 設置線條寬度為 10 點 path.lineWidth = 10; // 設置繪製顏色為灰色 [[UIColor lightGrayColor] setStroke]; // 繪製路徑 [path stroke]; } @end
運行結果:完美
- 繪製圖像
創建一個 UIImage 對象: UIImage * logoImage = [UIImage imageNamed:@"train"]; ,然後在 drawRect 方法中將圖像會知道視圖上: [logoImage drawInRect:someRect]
#import "JXHypnosisView.h" @implementation JXHypnosisView - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { // 設置 JXHypnosisView 對象的背景顏色為透明 self.backgroundColor = [UIColor clearColor]; } return self; } - (void)drawRect:(CGRect)rect { CGRect bounds = self.bounds; // 根據bounds計算中心點 CGPoint center; center.x = bounds.origin.x + bounds.size.width / 2.0; center.y = bounds.origin.y + bounds.size.height / 2.0; // 是最外層圓形成為視圖的外接圓 float maxRadius = hypotf(bounds.size.width, bounds.size.height) / 2.0; UIBezierPath * path = [[UIBezierPath alloc] init]; for (float currentRadius = maxRadius; currentRadius > 0; currentRadius -= 20) { // 用來設置繪製起始位置 [path moveToPoint:CGPointMake(center.x + currentRadius, center.y)]; [path addArcWithCenter:center radius:currentRadius startAngle:0.0 endAngle:M_PI * 2.0 clockwise:YES]; } // 設置線條寬度為 10 點 path.lineWidth = 10; // 設置繪製顏色為灰色 [[UIColor lightGrayColor] setStroke]; // 繪製路徑 [path stroke]; // 創建UIImage對象 UIImage * logoImage = [UIImage imageNamed:@"train"]; // 繪製圖像 [logoImage drawInRect:bounds]; } @end
- 深入學習: Core Graphics
UIImage、UIBezierPath 和 NSString 都提供了至少一種用於在 drawRect 中繪圖的方法,這些繪圖的方法會在 drawRect 執行時分別將圖像,圖形,和文本繪製到視圖的圖層上。
無論是繪製 JPEG 、PDF 還是視圖的圖層,都是由 Core Graphics 框架完成的。 Core Graphics 是一套提供 2D 繪圖功能的 C語言API,使用 C結構和 C函數模擬了一套面向對象的編程機制,並沒有OC對象和方法。 Core Graphics 中最重要的“對象”是 圖形上下文 ,圖形上下文是 CGContextRef 的“對象”,負責存儲繪畫狀態(例如畫筆顏色和線條粗細)和繪製內容所處的記憶體空間。
視圖的 drawRect 方法在執行之前,系統首先為視圖的圖層創建一個圖形上下文,然後為繪畫狀態設置一些預設參數。 drawRect 方法開始執行時,隨著圖形上下文不斷執行繪圖操作,圖層上的內容也會隨之改變。 drawRect 執行完畢後,系統會將圖層與其他圖層一起組合成完整的圖像並顯示在屏幕上。
參與繪圖操作的類都定義了改變繪畫狀態和執行繪圖操作的方法,這些方法其實調用了對應的 Core Graphics 函數。例如,向 UIColor 對象發送 setCtroke 消息時,會調用 Core Graphics 中的 CGContextSetRGBSrokeColor 函數改變當前上下文中的畫筆顏色。