由於`CoreGraphics`框架有太多的`API`,對於初次接觸或者對該框架不是十分瞭解的人,在繪圖時,對`API`的選擇會感到有些迷茫,甚至會覺得`iOS`的圖形繪製有些繁瑣。因此,本文主要介紹一下`iOS`的繪圖方法和分析一下`CoreGraphics`框架的繪圖原理。 ...
由於CoreGraphics
框架有太多的API
,對於初次接觸或者對該框架不是十分瞭解的人,在繪圖時,對API
的選擇會感到有些迷茫,甚至會覺得iOS
的圖形繪製有些繁瑣。因此,本文主要介紹一下iOS
的繪圖方法和分析一下CoreGraphics
框架的繪圖原理。
一、繪圖系統簡介
iOS
的繪圖框架有多種,我們平常最常用的就是UIKit
,其底層是依賴CoreGraphics
實現的,而且絕大多數的圖形界面也都是由UIKit
完成,並且UIImage
、NSString
、UIBezierPath
、UIColor
等都知道如何繪製自己,也提供了一些方法來滿足我們常用的繪圖需求。除了UIKit
,還有CoreGraphics
、Core Animation
,Core Image
,OpenGL ES
等多種框架,來滿足不同的繪圖要求。各個框架的大概介紹如下:
- UIKit:最常用的視圖框架,封裝度最高,都是OC對象
- CoreGraphics:主要繪圖系統,常用於繪製自定義視圖,純C的API,使用Quartz2D做引擎
- CoreAnimation:提供強大的2D和3D動畫效果
- CoreImage:給圖片提供各種濾鏡處理,比如高斯模糊、銳化等
- OpenGL-ES:主要用於游戲繪製,但它是一套編程規範,具體由設備製造商實現
繪圖系統
二、繪圖方式
實際的繪圖包括兩部分:視圖繪製和視圖佈局,它們實現的功能是不同的,在理解這兩個概念之前,需要瞭解一下什麼是繪圖周期,因為都是在繪圖周期中進行繪製的。
繪圖周期:
- iOS在運行迴圈中會整合所有的繪圖請求,並一次將它們繪製出來
- 不能在子線程中繪製,也不能進行複雜的操作,否則會造成主線程卡頓
1.視圖繪製
調用UIView
的drawRect:
方法進行繪製。如果調用一個視圖的setNeedsDisplay
方法,那麼該視圖就被標記為重新繪製,並且會在下一次繪製周期中重新繪製,自動調用drawRect:
方法。
2.視圖佈局
調用UIView
的layoutSubviews
方法。如果調用一個視圖的setNeedsLayout
方法,那麼該視圖就被標記為需要重新佈局,UIKit
會自動調用layoutSubviews
方法及其子視圖的layoutSubviews
方法。
在繪圖時,我們應該儘量多使用佈局,少使用繪製,是因為佈局使用的是GPU
,而繪製使用的是CPU
。GPU
對於圖形處理有優勢,而CPU
要處理的事情較多,且不擅長處理圖形,所以儘量使用GPU
來處理圖形。
三、繪圖狀態切換
iOS
的繪圖有多種對應的狀態切換,比如:pop/push
、save/restore
、context/imageContext
和CGPathRef/UIBezierPath
等,下麵分別進行介紹:
1.pop / push
設置繪圖的上下文環境(context)
push:UIGraphicsPushContext(context)把context壓入棧中,並把context設置為當前繪圖上下文
pop:UIGraphicsPopContext將棧頂的上下文彈出,恢復先前的上下文,但是繪圖狀態不變
下麵繪製的視圖是黑色
- (void)drawRect:(CGRect)rect {
[[UIColor redColor] setFill];
UIGraphicsPushContext(UIGraphicsGetCurrentContext());
[[UIColor blackColor] setFill];
UIGraphicsPopContext();
UIRectFill(CGRectMake(90, 340, 100, 100)); // black color
}
2.save / restore
設置繪圖的狀態(state)
save:CGContextSaveGState 壓棧當前的繪圖狀態,僅僅是繪圖狀態,不是繪圖上下文
restore:恢復剛纔保存的繪圖狀態
下麵繪製的視圖是紅色
- (void)drawRect:(CGRect)rect {
[[UIColor redColor] setFill];
CGContextSaveGState(UIGraphicsGetCurrentContext());
[[UIColor blackColor] setFill];
CGContextRestoreGState(UIGraphicsGetCurrentContext());
UIRectFill(CGRectMake(90, 200, 100, 100)); // red color
}
3.context / imageContext
iOS
的繪圖必須在一個上下文中繪製,所以在繪圖之前要獲取一個上下文。如果是繪製圖片,就需要獲取一個圖片的上下文;如果是繪製其它視圖,就需要一個非圖片上下文。對於上下文的理解,可以認為就是一張畫布,然後在上面進行繪圖操作。
context:圖形上下文,可以通過UIGraphicsGetCurrentContext:
獲取當前視圖的上下文
imageContext:圖片上下文,可以通過UIGraphicsBeginImageContextWithOptions:
獲取一個圖片上下文,然後繪製完成後,調用UIGraphicsGetImageFromCurrentImageContext
獲取繪製的圖片,最後要記得關閉圖片上下文UIGraphicsEndImageContext
。
4.CGPathRef / UIBezierPath
圖形的繪製需要繪製一個路徑,然後再把路徑渲染出來,而CGPathRef
就是CoreGraphics
框架中的路徑繪製類,UIBezierPath
是封裝CGPathRef
的面向OC
的類,使用更加方便,但是一些高級特性還是不及CGPathRef
。
四、具體繪圖方法
由於iOS
常用的繪圖框架有UIKit
和CoreGraphics
兩個,所以繪圖的方法也有多種,下麵介紹一下iOS
的幾種常用的繪圖方法。
1.圖片類型的上下文
圖片上下文的繪製不需要在drawRect:
方法中進行,在一個普通的OC
方法中就可以繪製
使用UIKit實現
// 獲取圖片上下文
UIGraphicsBeginImageContextWithOptions(CGSizeMake(100,100), NO, 0);
// 繪圖
UIBezierPath* p = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0,0,100,100)];
[[UIColor blueColor] setFill];
[p fill];
// 從圖片上下文中獲取繪製的圖片
UIImage* im = UIGraphicsGetImageFromCurrentImageContext();
// 關閉圖片上下文
UIGraphicsEndImageContext();
使用CoreGraphics實現
// 獲取圖片上下文
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();
2.drawRect:
在UIView
子類的drawRect:
方法中實現圖形重新繪製,繪圖步驟如下:
- 獲取上下文
- 繪製圖形
- 渲染圖形
UIKit方法
- (void) drawRect: (CGRect) rect {
UIBezierPath* p = [UIBezierPathbezierPathWithOvalInRect:CGRectMake(0,0,100,100)];
[[UIColor blueColor] setFill];
[p fill];
}
CoreGraphics
- (void) drawRect: (CGRect) rect {
CGContextRef con = UIGraphicsGetCurrentContext();
CGContextAddEllipseInRect(con, CGRectMake(0,0,100,100));
CGContextSetFillColorWithColor(con, [UIColor blueColor].CGColor);
CGContextFillPath(con);
}
3.drawLayer:inContext:
在UIView
子類的drawLayer:inContext:
方法中也可以實現繪圖任務,它是一個圖層的代理方法,而為了能夠調用該方法,需要給圖層的delegate
設置代理對象,其中代理對象不能是UIView
對象,因為UIView
對象已經是它內部根層(隱式層)的代理對象,再將它設置為另一個層的代理對象就會出問題。
一個view
被添加到其它view
上時,圖層的變化如下:
- 先隱式地把此
view
的layer
的CALayerDelegate
設置成此view
- 調用此
view
的self.layer
的drawInContext
方法 - 由於
drawLayer
方法的註釋:If defined, called by the default implementation of -drawInContext:
說明瞭drawInContext
里if([self.delegate responseToSelector:@selector(drawLayer:inContext:)])
就執行drawLayer:inContext:
方法,這裡我們因為實現了drawLayer:inContext:
所以會執行 [super drawLayer:layer inContext:ctx]
會讓系統自動調用此view
的drawRect:
方法,至此self.layer
畫出來了- 在
self.layer
上再加一個子layer
,當調用[layer setNeedsDisplay];
時會自動調用此layer
的drawInContext
方法 - 如果
drawRect
不重寫,就不會調用其layer
的drawInContext
方法,也就不會調用drawLayer:inContext
方法
調用內部根層的drawLayer:inContext:
//如果drawRect不重寫,就不會調用其layer的drawInContext方法,也就不會調用drawLayer:inContext方法
-(void)drawRect:(CGRect)rect{
NSLog(@"2-drawRect:");
NSLog(@"drawRect里的CGContext:%@",UIGraphicsGetCurrentContext());
//得到的當前圖形上下文正是drawLayer中傳遞過來的
[super drawRect:rect];
}
#pragma mark - CALayerDelegate
-(void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx{
NSLog(@"1-drawLayer:inContext:");
NSLog(@"drawLayer里的CGContext:%@",ctx);
// 如果去掉此句就不會執行drawRect!!!!!!!!
[super drawLayer:layer inContext:ctx];
}
調用外部代理對象的drawLayer:inContext:
由於不能把UIView
對象設置為CALayerDelegate
的代理,所以我們需要創建一個NSObject對象,然後實現drawLayer:inContext:
方法,這樣就可以在代理對象里繪製所需圖形。另外,在設置代理時,不需要遵守CALayerDelegate
的代理協議,即這個方法是NSObject
的,不需要顯式地指定協議。
// MyLayerDelegate.m
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
{
CGContextAddEllipseInRect(ctx, CGRectMake(100,100,100,100));
CGContextSetFillColorWithColor(ctx, [UIColor blueColor].CGColor);
CGContextFillPath(ctx);
}
// ViewController.m
@interface ViewController () <CALayerDelegate>
@property (nonatomic, strong) id myLayerDelegate;
@end
@implementation ViewController
- (void)viewDidLoad {
// 設置layer的delegate為NSObject子類對象
_myLayerDelegate = [[MyLayerDelegate alloc] init];
MyView *myView = [[MyView alloc] initWithFrame:self.view.bounds];
[self.view addSubview:myView];
CALayer *layer = [CALayer layer];
layer.backgroundColor = [UIColor magentaColor].CGColor;
layer.bounds = CGRectMake(0, 0, 300, 500);
layer.anchorPoint = CGPointZero;
layer.delegate = _myLayerDelegate;
[layer setNeedsDisplay];
[myView.layer addSublayer:layer];
}
詳細實現過程
當UIView
需要顯示時,它內部的層會準備好一個CGContextRef
(圖形上下文),然後調用delegate
(這裡就是UIView
)的drawLayer:inContext:
方法,並且傳入已經準備好的CGContextRef
對象。而UIView
在drawLayer:inContext:
方法中又會調用自己的drawRect:
方法。平時在drawRect:
中通過UIGraphicsGetCurrentContext()
獲取的就是由層傳入的CGContextRef
對象,在drawRect:
中完成的所有繪圖都會填入層的CGContextRef
中,然後被拷貝至屏幕。
iOS
繪圖框架分析如上,如有不足之處,歡迎指出,共同進步。(本文圖片來自互聯網,版權歸原作者所有)
參考資料
CoreGraphics之CGContextSaveGState與UIGraphicsPushContext
Basic Zooming Using the Pinch Gestures