由于CoreGraphics
框架有太多的API
雄可,對(duì)于初次接觸或者對(duì)該框架不是十分了解的人凿傅,在繪圖時(shí),對(duì)API
的選擇會(huì)感到有些迷茫滞项,甚至?xí)X得iOS
的圖形繪制有些繁瑣狭归。因此,本文主要介紹一下iOS
的繪圖方法和分析一下CoreGraphics
框架的繪圖原理文判。
一、繪圖系統(tǒng)簡(jiǎn)介
iOS
的繪圖框架有多種室梅,我們平常最常用的就是UIKit
戏仓,其底層是依賴CoreGraphics
實(shí)現(xiàn)的,而且絕大多數(shù)的圖形界面也都是由UIKit
完成亡鼠,并且UIImage
赏殃、NSString
、UIBezierPath
间涵、UIColor
等都知道如何繪制自己仁热,也提供了一些方法來滿足我們常用的繪圖需求。除了UIKit
勾哩,還有CoreGraphics
抗蠢、Core Animation
,Core Image
思劳,OpenGL ES
等多種框架迅矛,來滿足不同的繪圖要求。各個(gè)框架的大概介紹如下:
- UIKit:最常用的視圖框架潜叛,封裝度最高秽褒,都是OC對(duì)象
- CoreGraphics:主要繪圖系統(tǒng),常用于繪制自定義視圖威兜,純C的API销斟,使用Quartz2D做引擎
- CoreAnimation:提供強(qiáng)大的2D和3D動(dòng)畫效果
- CoreImage:給圖片提供各種濾鏡處理,比如高斯模糊椒舵、銳化等
- OpenGL-ES:主要用于游戲繪制蚂踊,但它是一套編程規(guī)范,具體由設(shè)備制造商實(shí)現(xiàn)
繪圖系統(tǒng)
二逮栅、繪圖方式
實(shí)際的繪圖包括兩部分:視圖繪制和視圖布局悴势,它們實(shí)現(xiàn)的功能是不同的窗宇,在理解這兩個(gè)概念之前,需要了解一下什么是繪圖周期特纤,因?yàn)槎际窃诶L圖周期中進(jìn)行繪制的军俊。
繪圖周期:
- iOS在運(yùn)行循環(huán)中會(huì)整合所有的繪圖請(qǐng)求,并一次將它們繪制出來
- 不能在子線程中繪制捧存,也不能進(jìn)行復(fù)雜的操作粪躬,否則會(huì)造成主線程卡頓
1.視圖繪制
調(diào)用UIView
的drawRect:
方法進(jìn)行繪制。如果調(diào)用一個(gè)視圖的setNeedsDisplay
方法昔穴,那么該視圖就被標(biāo)記為重新繪制镰官,并且會(huì)在下一次繪制周期中重新繪制,自動(dòng)調(diào)用drawRect:
方法吗货。
2.視圖布局
調(diào)用UIView
的layoutSubviews
方法泳唠。如果調(diào)用一個(gè)視圖的setNeedsLayout
方法,那么該視圖就被標(biāo)記為需要重新布局宙搬,UIKit
會(huì)自動(dòng)調(diào)用layoutSubviews
方法及其子視圖的layoutSubviews
方法笨腥。
在繪圖時(shí),我們應(yīng)該盡量多使用布局勇垛,少使用繪制脖母,是因?yàn)椴季质褂玫氖?code>GPU,而繪制使用的是CPU
闲孤。GPU
對(duì)于圖形處理有優(yōu)勢(shì)谆级,而CPU
要處理的事情較多,且不擅長(zhǎng)處理圖形讼积,所以盡量使用GPU
來處理圖形肥照。
三、繪圖狀態(tài)切換
iOS
的繪圖有多種對(duì)應(yīng)的狀態(tài)切換币砂,比如:pop/push
建峭、save/restore
、context/imageContext
和CGPathRef/UIBezierPath
等决摧,下面分別進(jìn)行介紹:
1.pop / push
設(shè)置繪圖的上下文環(huán)境(context)
push:UIGraphicsPushContext(context)把context壓入棧中亿蒸,并把context設(shè)置為當(dāng)前繪圖上下文
pop:UIGraphicsPopContext將棧頂?shù)纳舷挛膹棾觯謴?fù)先前的上下文掌桩,但是繪圖狀態(tài)不變
下面繪制的視圖是黑色
- (void)drawRect:(CGRect)rect {
[[UIColor redColor] setFill];
UIGraphicsPushContext(UIGraphicsGetCurrentContext());
[[UIColor blackColor] setFill];
UIGraphicsPopContext();
UIRectFill(CGRectMake(90, 340, 100, 100)); // black color
}
2.save / restore
設(shè)置繪圖的狀態(tài)(state)
save:CGContextSaveGState 壓棧當(dāng)前的繪圖狀態(tài)边锁,僅僅是繪圖狀態(tài),不是繪圖上下文
restore:恢復(fù)剛才保存的繪圖狀態(tài)
下面繪制的視圖是紅色
- (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
的繪圖必須在一個(gè)上下文中繪制波岛,所以在繪圖之前要獲取一個(gè)上下文茅坛。如果是繪制圖片,就需要獲取一個(gè)圖片的上下文;如果是繪制其它視圖贡蓖,就需要一個(gè)非圖片上下文曹鸠。對(duì)于上下文的理解,可以認(rèn)為就是一張畫布斥铺,然后在上面進(jìn)行繪圖操作彻桃。
context:圖形上下文,可以通過UIGraphicsGetCurrentContext:
獲取當(dāng)前視圖的上下文
imageContext:圖片上下文晾蜘,可以通過UIGraphicsBeginImageContextWithOptions:
獲取一個(gè)圖片上下文邻眷,然后繪制完成后,調(diào)用UIGraphicsGetImageFromCurrentImageContext
獲取繪制的圖片剔交,最后要記得關(guān)閉圖片上下文UIGraphicsEndImageContext
肆饶。
4.CGPathRef / UIBezierPath
圖形的繪制需要繪制一個(gè)路徑,然后再把路徑渲染出來岖常,而CGPathRef
就是CoreGraphics
框架中的路徑繪制類驯镊,UIBezierPath
是封裝CGPathRef
的面向OC
的類,使用更加方便竭鞍,但是一些高級(jí)特性還是不及CGPathRef
阿宅。
四、具體繪圖方法
由于iOS
常用的繪圖框架有UIKit
和CoreGraphics
兩個(gè)笼蛛,所以繪圖的方法也有多種,下面介紹一下iOS
的幾種常用的繪圖方法蛉鹿。
1.圖片類型的上下文
圖片上下文的繪制不需要在drawRect:
方法中進(jìn)行滨砍,在一個(gè)普通的OC
方法中就可以繪制
使用UIKit實(shí)現(xiàn)
// 獲取圖片上下文
UIGraphicsBeginImageContextWithOptions(CGSizeMake(100,100), NO, 0);
// 繪圖
UIBezierPath* p = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0,0,100,100)];
[[UIColor blueColor] setFill];
[p fill];
// 從圖片上下文中獲取繪制的圖片
UIImage* im = UIGraphicsGetImageFromCurrentImageContext();
// 關(guān)閉圖片上下文
UIGraphicsEndImageContext();
使用CoreGraphics實(shí)現(xiàn)
// 獲取圖片上下文
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();
// 關(guān)閉圖片上下文
UIGraphicsEndImageContext();
2.drawRect:
在UIView
子類的drawRect:
方法中實(shí)現(xiàn)圖形重新繪制,繪圖步驟如下:
- 獲取上下文
- 繪制圖形
- 渲染圖形
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:
方法中也可以實(shí)現(xiàn)繪圖任務(wù)妖异,它是一個(gè)圖層的代理方法惋戏,而為了能夠調(diào)用該方法,需要給圖層的delegate
設(shè)置代理對(duì)象他膳,其中代理對(duì)象不能是UIView
對(duì)象响逢,因?yàn)?code>UIView對(duì)象已經(jīng)是它內(nèi)部根層(隱式層)的代理對(duì)象,再將它設(shè)置為另一個(gè)層的代理對(duì)象就會(huì)出問題棕孙。
一個(gè)view
被添加到其它view
上時(shí)舔亭,圖層的變化如下:
- 先隱式地把此
view
的layer
的CALayerDelegate
設(shè)置成此view
- 調(diào)用此
view
的self.layer
的drawInContext
方法 - 由于
drawLayer
方法的注釋:If defined, called by the default implementation of -drawInContext:
說明了drawInContext
里if([self.delegate responseToSelector:@selector(drawLayer:inContext:)])
就執(zhí)行drawLayer:inContext:
方法,這里我們因?yàn)閷?shí)現(xiàn)了drawLayer:inContext:
所以會(huì)執(zhí)行 -
[super drawLayer:layer inContext:ctx]
會(huì)讓系統(tǒng)自動(dòng)調(diào)用此view
的drawRect:
方法蟀俊,至此self.layer
畫出來了 - 在
self.layer
上再加一個(gè)子layer
钦铺,當(dāng)調(diào)用[layer setNeedsDisplay];
時(shí)會(huì)自動(dòng)調(diào)用此layer
的drawInContext
方法 - 如果
drawRect
不重寫,就不會(huì)調(diào)用其layer
的drawInContext
方法肢预,也就不會(huì)調(diào)用drawLayer:inContext
方法
調(diào)用內(nèi)部根層的drawLayer:inContext:
//如果drawRect不重寫矛洞,就不會(huì)調(diào)用其layer的drawInContext方法,也就不會(huì)調(diào)用drawLayer:inContext方法
-(void)drawRect:(CGRect)rect{
NSLog(@"2-drawRect:");
NSLog(@"drawRect里的CGContext:%@",UIGraphicsGetCurrentContext());
//得到的當(dāng)前圖形上下文正是drawLayer中傳遞過來的
[super drawRect:rect];
}
#pragma mark - CALayerDelegate
-(void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx{
NSLog(@"1-drawLayer:inContext:");
NSLog(@"drawLayer里的CGContext:%@",ctx);
// 如果去掉此句就不會(huì)執(zhí)行drawRect!!!!!!!!
[super drawLayer:layer inContext:ctx];
}
調(diào)用外部代理對(duì)象的drawLayer:inContext:
由于不能把UIView
對(duì)象設(shè)置為CALayerDelegate
的代理烫映,所以我們需要?jiǎng)?chuàng)建一個(gè)NSObject對(duì)象沼本,然后實(shí)現(xiàn)drawLayer:inContext:
方法噩峦,這樣就可以在代理對(duì)象里繪制所需圖形。另外抽兆,在設(shè)置代理時(shí)识补,不需要遵守CALayerDelegate
的代理協(xié)議,即這個(gè)方法是NSObject
的郊丛,不需要顯式地指定協(xié)議李请。
// 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 {
// 設(shè)置layer的delegate為NSObject子類對(duì)象
_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];
}
詳細(xì)實(shí)現(xiàn)過程
當(dāng)UIView
需要顯示時(shí),它內(nèi)部的層會(huì)準(zhǔn)備好一個(gè)CGContextRef
(圖形上下文)厉熟,然后調(diào)用delegate
(這里就是UIView
)的drawLayer:inContext:
方法导盅,并且傳入已經(jīng)準(zhǔn)備好的CGContextRef
對(duì)象。而UIView
在drawLayer:inContext:
方法中又會(huì)調(diào)用自己的drawRect:
方法揍瑟。平時(shí)在drawRect:
中通過UIGraphicsGetCurrentContext()
獲取的就是由層傳入的CGContextRef
對(duì)象白翻,在drawRect:
中完成的所有繪圖都會(huì)填入層的CGContextRef
中,然后被拷貝至屏幕绢片。
iOS
繪圖框架分析如上滤馍,如有不足之處,歡迎指出底循,共同進(jìn)步巢株。(本文圖片來自互聯(lián)網(wǎng),版權(quán)歸原作者所有)
參考資料
iOS繪圖系統(tǒng)(一) UIKit與CoreGraphics
CoreGraphics之CGContextSaveGState與UIGraphicsPushContext