UIKit框架和Core Graphics
作為初學者,很容易被UIKit和Core Graphics兩個支持繪圖的框架迷惑。
UIKit
像UIImage、NSString(繪制文本)、UIBezierPath(繪制形狀)、UIColor都知道如何繪制自己。這些類提供了功能有限但使用方便的方法來讓我們完成繪圖任務。一般情況下,UIKit就是我們所需要的。
使用UiKit,你只能在當前上下文中繪圖,所以如果你當前處于UIGraphicsBeginImageContextWithOptions函數或drawRect:方法中,你就可以直接使用UIKit提供的方法進行繪圖。如果你持有一個context:參數,那么使用UIKit提供的方法之前,必須將該上下文參數轉化為當前上下文。幸運的是,調用UIGraphicsPushContext 函數可以方便的將context:參數轉化為當前上下文,記住最后別忘了調用UIGraphicsPopContext函數恢復上下文環境。
從第一段話開始就重要了,這給我很大啟示,UIKit框架中都是像UIImage、NSString(繪制文本)、UIBezierPath(繪制形狀)、UIColor,這些類的調用方法,我就想起在上一篇中我們發現CoreGraphics是基于C的一套API.這樣我們就可以大致區分起來.用UI,NS 開頭都這些類.調用的方法就屬于UIKIt框架.
第二段話就可以和我們上一篇最后一個問題對上了,這個Context參數到底怎么用?
調用UIGraphicsPushContext 函數可以方便的將context:參數轉化為當前上下文,記住最后別忘了調用UIGraphicsPopContext函數恢復上下文環境。
代碼具體怎么寫? 我們接著品讀....
Core Graphics
這是一個繪圖專用的API族,它經常被稱為QuartZ或QuartZ 2D。Core Graphics是iOS上所有繪圖功能的基石,包括UIKit。
使用Core Graphics之前需要指定一個用于繪圖的圖形上下文(CGContextRef),這個圖形上下文會在每個繪圖函數中都會被用到。如果你持有一個圖形上下文context:參數,那么你等同于有了一個圖形上下文,這個上下文也許就是你需要用來繪圖的那個。如果你當前處于UIGraphicsBeginImageContextWithOptions函數或drawRect:方法中,并沒有引用一個上下文。為了使用Core Graphics,你可以調用UIGraphicsGetCurrentContext函數獲得當前的圖形上下文。
至此,我們有了兩大繪圖框架的支持以及三種獲得圖形上下文的方法(drawRect:、drawRect: inContext:、UIGraphicsBeginImageContextWithOptions)。那么我們就有6種繪圖的形式。如果你有些困惑了,不用怕,我接下來將說明這6種情況。無需擔心還沒有具體的繪圖命令,你只需關注上下文如何被創建以及我們是在使用UIKit還是Core Graphics。
從這里感覺就沒什么說的,思緒捋通了,跟著教程一步一步來.也有可能有什么遺漏了,由于我目前的水平,無法看出什么門道.
下面繪圖方法的具體的代碼了:
第一種繪圖形式:在UIView的子類方法drawRect:中繪制一個藍色圓,使用UIKit在Cocoa為我們提供的當前上下文中完成繪圖任務。
- (void) drawRect: (CGRect) rect {
UIBezierPath* p = [UIBezierPathbezierPathWithOvalInRect:CGRectMake(0,0,100,100)];
[[UIColor blueColor] setFill];
[p fill];
}
這個代碼要注意:
1.使用的cocoa提供給我們的上下文.
2.這里我們使用的是UIKit框架中兩個類UIBezierPath , UIColor
3.以及這兩個結構 組合的時候,沒有出現上下文的影子.但確實是有上下文存在.在回看本篇開頭對UIKIt框架繪圖APi的介紹.可以直接使用API進行繪制.可以理解這里對上下文進行了封裝.
第二種繪圖形式:使用Core Graphics實現繪制藍色圓。
- (void) drawRect: (CGRect) rect {
//這里我對代碼稍微注釋下:
CGContextRef con = UIGraphicsGetCurrentContext();
CGContextAddEllipseInRect(con, CGRectMake(0,0,100,100));
//設定規則 繪制上下文的形狀
CGContextSetFillColorWithColor(con, [UIColor blueColor].CGColor);
//設定規則 繪制上下文的顏色
CGContextFillPath(con);
//Paints the area within the current path, using the nonzero winding number rule.
//就是說使用前面設定的規則,來繪制當前path內的區域.
//Quartz treats each subpath as if it were closed by calling CGContextClosePath. The nonzero winding number rule is described in Filling a Path in Quartz 2D Programming Guide.
As a side effect when you call this function, Quartz clears the current path.
//一般用的是CGContextClosePath 這個方法,現在這個代碼會有一個副作用,會清除當前的路徑.大家使用的時候要注意.看各種情況下使用吧.
}
第二種和第一種對比著看,就很清晰了.這個使用的是Core Graphics框架繪圖.這時候我們再回頭看本篇中Core Graphics中介紹,
使用Core Graphics之前需要指定一個用于繪圖的圖形上下文(CGContextRef)
所以,我就懂了,繪制的時候第一步要先獲取上下文的應用.
說下,上面兩個方法的效果是一樣的,都是一個藍色的填充的圓.
第三種繪圖形式:我將在UIView子類的drawLayer:inContext:方法中實現繪圖任務。drawLayer:inContext:方法是一個繪制圖層內容的代理方法。為了能夠調用drawLayer:inContext:方法,我們需要設定圖層的代理對象。但要注意,不應該將UIView對象設置為顯示層的委托對象,這是因為UIView對象已經是隱式層的代理對象,再將它設置為另一個層的委托對象就會出問題。輕量級的做法是:編寫負責繪圖形的代理類。在MyView.h文件中聲明如下代碼:
@interface MyLayerDelegate : NSObject
@end
然后MyView.m文件中實現接口代碼:
@implementation MyLayerDelegate
- (void)drawLayer:(CALayer*)layer inContext:(CGContextRef)ctx {
UIGraphicsPushContext(ctx);
UIBezierPath* p = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0,0,100,100)];
[[UIColor blueColor] setFill];
[p fill];
UIGraphicsPopContext();
}
@end
直接將代理類的實現代碼放在MyView.m文件的#import代碼的下面,這樣感覺好像在使用私有類完成繪圖任務(雖然這不是私有類)。需要注意的是,我們所引用的上下文并不是當前上下文,所以為了能夠使用UIKit,我們需要將引用的上下文轉變成當前上下文。
因為圖層的代理是assign內存管理策略,那么這里就不能以局部變量的形式創建MyLayerDelegate實例對象賦值給圖層代理。這里選擇在MyView.m中增加一個實例變量,因為實例變量默認是strong:
@interface MyView () {
MyLayerDelegate* _layerDeleagete;
}
@end
使用該圖層代理:
MyView *myView = [[MyView alloc] initWithFrame: CGRectMake(0, 0, 320, 480)];
CALayer *myLayer = [CALayer layer];
_layerDelegate = [[MyLayerDelegate alloc] init];
myLayer.delegate = _layerDelegate;
[myView.layer addSublayer:myLayer];
[myView setNeedsDisplay]; // 調用此方法,drawLayer: inContext:方法才會被調用。
為了保證教程的完整性,我把三種方法的代碼和說明一起復制過來,來說明.第三種方法看似這么復雜,如果僅說明繪制這部分.
我來說簡單點的步驟:
第一步.在我們自定義的UIVIew----MyDrawView中重寫
-(void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx{
UIGraphicsPushContext(ctx);
UIBezierPath* p = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0,0,100,100)];
[[UIColor blueColor] setFill];
[p fill];
UIGraphicsPopContext();
}
第二步.在ViewController中使用
- (void)viewDidLoad {
[super viewDidLoad];
MyDrawView *mydrayView = [[MyDrawView alloc]initWithFrame:CGRectMake(100, 100, 200, 200)];
[self.view addSubview:mydrayView];
[mydrayView.layer setNeedsDisplay];
// Do any additional setup after loading the view, typically from a nib.
}
然后品讀這段教程中的代碼:
大致是封裝一個MyView的自定義View類.
新建了一個圖層,添加在視圖上,并為這個圖層設置了一個代理.并重寫了繪制圖層的方法.這里重寫的方法的代碼也就回答了,context引用轉換成真正的畫布的context上下文.如何寫代碼的問題了.
這里需要注意的是:
1.要有圖層的概念.在view調用[myView setNeedsDisplay]這個的時候,新加的圖層也會被重繪,并調用我們重寫的代理方法.
2.我們在interface 中增加一個實例變量的默認屬性是Strong.
第四種繪圖形式: 使用Core Graphics在drawLayer:inContext:方法中實現同樣操作,代碼如下:
- (void)drawLayer:(CALayer*)lay inContext:(CGContextRef)con {
CGContextAddEllipseInRect(con, CGRectMake(0,0,100,100));
CGContextSetFillColorWithColor(con, [UIColor blueColor].CGColor);
CGContextFillPath(con);
}
這個我感覺沒什么說的,替換了UIKit繪圖的框架,類比第二種方法.
第五種繪圖形式: 使用UIKit實現:
最后,演示UIGraphicsBeginImageContextWithOptions的用法,并從上下文中生成一個UIImage對象。生成UIImage對象的代碼并不需要等待某些方法被調用后或在UIView的子類中才能去做。
UIGraphicsBeginImageContextWithOptions(CGSizeMake(100,100), NO, 0);
UIBezierPath* p = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0,0,100,100)];
[[UIColor blueColor] setFill];
[p fill];
UIImage* im = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
解釋一下UIGraphicsBeginImageContextWithOptions函數參數的含義:第一個參數表示所要創建的圖片的尺寸;第二個參數用來指定所生成圖片的背景是否為不透明,如上我們使用YES而不是NO,則我們得到的圖片背景將會是黑色,顯然這不是我想要的;第三個參數指定生成圖片的縮放因子,這個縮放因子與UIImage的scale屬性所指的含義是一致的。傳入0則表示讓圖片的縮放因子根據屏幕的分辨率而變化,所以我們得到的圖片不管是在單分辨率還是視網膜屏上看起來都會很好。
這段話,作者自己有解釋.我們要注意的是
UIImage* im = UIGraphicsGetImageFromCurrentImageContext(); 這行代碼是從當前上下文中回去圖片,我們是否可以在其他地方例如cocoa提供的上下文中使用呢?
答案是否定的.看方法注釋
You should call this function only when a bitmap-based graphics context is the current graphics context. If the current context is nil or was not created by a call to UIGraphicsBeginImageContext, this function returns nil.
前一句只有是位圖圖形的上下文才可以.我還不是很懂,但是下面一句,如果這個上下文,不是用UIGraphicsBeginImageContext 創建的,那么這個方法就會返回nil,所以這個方法使用有限定,大家要注意了.
第六種繪圖形式: 使用Core Graphics實現:
UIGraphicsBeginImageContextWithOptions(CGSizeMake(100,100), NO, 0);
CGContextRef con = UIGraphicsGetCurrentContext();
CGContextAddEllipseInRect(con, CGRectMake(0,0,100,100));
CGContextSetFillColorWithColor(con, [UIColor blueColor].CGColor);
CGContextFillPath(con);
UIImage* im = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
UIKit和Core Graphics可以在相同的圖形上下文中混合使用。在iOS 4.0之前,使用UIKit和UIGraphicsGetCurrentContext被認為是線程不安全的。而在iOS4.0以后蘋果讓繪圖操作在第二個線程中執行解決了此問題。
第六種繪圖方法也沒有什么說的,就是下面這種說兩個框架可以混合使用,這種操作一般是沒有什么問題,從我的感覺上來說,要不斷地進行縣城交叉渲染的時候盡量避免雙重框架混合使用.避免不必要的bug.也就是沒事別惹事.有事先看這里.出bug,看看是不是混合使用的問題.
下面的教程就是一些使用的例子.
等下篇,我再一一品讀.