上一篇介紹了動畫相關的 api,本篇涉及的就是如何把圖形畫出來了,之前也做過不少相關的畫圖工作,但都比較簡單少量,也沒有刻意的去比較或者直接就是接入了第三方庫去實現...工作需要實現大量的 K線繪制以及各種狀態變更,因此總結一下方便查閱,提升工作效率.
iOS提供了兩套繪圖的框架,
UIBezierPath
和 Core Graphics
.
-
UIBezierPath
是UIKit中的一個關于圖形繪制的類,其實是對Core Graphics框架關于path(CGPathRef數據類型的封裝)的進一步封裝,語法就是 OC 范. -
Core Graphics
也被稱作QuartZ或QuartZ 2D,更接近底層,功能更強大, 提供的都是C語言的函數接口.
Core Graphics
在繪圖之前,我們先需要搞清楚下面幾個概念:
-
CGContextRef
圖形上下文
,可以理解為畫布/畫板,我們要畫畫首先需要一個載體吧,比如電腦繪圖我們會創建一個空白畫布,生活中畫畫我們會先準備好畫板,否則是無法進行繪制的.
通常我們通過以下2種方法來獲取這個context
:
drawRect:(CGRect)rect:
重寫UIView的drawRect:
方法,調用UIGraphicsGetCurrentContext()
即可獲得圖形上下文,在其他地方調用獲得的圖形上下文總是nil
,因為在drawRect之前,系統會往棧里面壓入一個valid的CGContextRef.
ps(*):drawRect:
方法在view第一次顯示的時候會自動調用(如果不設置 frame, 那么默認的第一次也不會調用),當你手動重畫這個View時,不能手動顯示調用,必須通過調用setNeedsDisplay
或者setNeedsDisplayInRect
,讓系統自動調該方法.
ps:drawLayer:(CALayer*)layer inContext:(CGContextRef)ctx
要注意圖層代理對象的設定,具體可以參考iOS 繪圖教程的第三種繪圖形式.UIGraphicsBeginImageContextWithOptions(<#CGSize size#>, <#BOOL opaque#>, <#CGFloat scale#>)
-- CGSize size:指定將來創建出來的view的大小;
-- BOOL opaque:設置透明YES代表透明,NO代表不透明;
-- CGFloat scale:代表縮放,0代表不縮放.
如果想在drawRect之外獲得context怎么辦?那只能自己創建位圖上下文了.
調用UIGraphicsBeginImageContextWithOptions
函數就可獲得用來處理圖片
的圖形上下文,利用該上下文,你就可以在其上進行繪圖,并生成圖片.調用UIGraphicsGetImageFromCurrentImageContext
函數可從當前上下文中獲取一個UIImage對象.記住在你所有的繪圖操作后別忘了調用UIGraphicsEndImageContext
函數關閉圖形上下文(類似于數據庫的打開與關閉)。UIGraphicsBeginImageContextWithOptions
和UIGraphicsEndImageContext
成對出現,類似的 api 還有很多,也可以嵌套.
-
CGContextSaveGState/CGContextRestoreGState
CGContextSaveGState
用于記錄和CGContextRestoreGState
用于恢復已存儲的繪圖上下文.
獲取圖形上下文之后,這時你開始畫圖的下一步準備工作,比如定畫筆的顏色,文本的顏色,字體的大小/型號,然后開始作畫.當你畫到一半的時候,你需要更改這些配置,也就是用特定的顏色/字體等繪制一個特殊的圖形,完成之后又回到最初的圖形.
是不是有點繞...
@舉個栗子:
我要畫三根線,先畫一根寬度為2的紅線,然后畫一根寬度為5的黃線,最后再畫一根寬度為2的紅線.
- (void)drawRect:(CGRect)rect {
CGContextRef ctx = UIGraphicsGetCurrentContext();
//第一條線
CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor);
CGContextSetLineWidth(ctx, 2.0f);
CGContextMoveToPoint(ctx, 10, 30);
CGContextAddLineToPoint(ctx, 10, 100);
CGContextStrokePath(ctx);
//第二條線
CGContextSaveGState(ctx); // ----- 看這里
CGContextSetStrokeColorWithColor(ctx, [UIColor yellowColor].CGColor);
CGContextSetLineWidth(ctx, 5.0f);
CGContextMoveToPoint(ctx, 50, 30);
CGContextAddLineToPoint(ctx, 50, 100);
CGContextStrokePath(ctx);
CGContextRestoreGState(ctx); // ---- 看這里
//第三條線
CGContextMoveToPoint(ctx, 110, 30);
CGContextAddLineToPoint(ctx, 110, 100);
CGContextStrokePath(ctx);
}
大家可以試試,如果把上面代碼標記了"看這里"的兩句刪掉,會是什么結果?
加上這兩句才是正確的:
可以看到,
CGContextSaveGState
存儲下來了當前紅色和寬度為2的線條狀態,然后切換顏色到黃色和5寬度的狀態畫線(你也可以畫圈/畫矩形, LZ 我偷懶),然后在CGContextRestoreGState
恢復到了紅色和默認的線條狀態進行畫,這個就是存儲當前繪制狀態的意思.總結:所以這2個 api 可以理解為,保存當前的上下文拷貝,變化一個樣子出去玩耍一下,結束之后又通過之前保存的拷貝復位.
3.UIGraphicsPushContext/UIGraphicsPopContext
UIGraphicsPushContext
用于完全更改圖形上下文和UIGraphicsPopContext
恢復之前的圖形上下文.從
UI
開頭的 api 也可以看出,它的使用與 UIKit 繪圖相關聯.
- 假設你正在當前圖形上下文中繪制 A,這時想要在位圖上下文中繪制完全不同的B并且使用UIKit來進行任意繪圖,這時你需要切換到一個全新的繪圖上下文中并且想要保存當前的圖形上下文,包括所有已經繪制的內容,那么就需要調用
UIGraphicsPushContext
來將圖形上下文入棧. - 等繪制完B后,再調用
UIGraphicsPopContext
將之前的圖形上下文出棧.
ps(*):這種情況只會在要使用UIKit在新的位圖上下文中繪圖時才會發生,只要你使用的是Core Graphics
繪制,就不需要去執行上下文入棧和出棧,Core Graphics
函數將上下文視作參數。引用iOS --- CoreGraphics中三種繪圖context切換方式的區別總結的一句話:繪圖context切換的關鍵是:要看切換新的繪圖context后,是要繼續使用CoreGraphics繪制圖形,還是要使用UIKit。
4.常用的一些 API
- (void)test {
CGContextRef ctx = UIGraphicsGetCurrentContext();
// 設置畫線的起點 為 (50,30)
CGContextMoveToPoint(ctx, 50, 30);
// 繪制直線連線, 從起點延伸到 (10,100)
CGContextAddLineToPoint(ctx, 10, 100);
// 繪制矩形 從(50,30)開始, 寬度高度均為50
CGContextAddRect(ctx, CGRectMake(50, 30, 50, 50));
// 繪制/渲染圖形
CGContextStrokePath(ctx); // stroke 是描線 ,而 fill 是填充,單純線條下 fill 不會工作
CGContextFillPath(ctx); // 填充
// 設置線條的寬度
CGContextSetLineWidth(ctx, 5.0f);
// 設置線條顏色 -- 注意與 fill 的區別
CGContextSetStrokeColorWithColor(ctx, [UIColor yellowColor].CGColor);
[[UIColor yellowColor] setStroke];
// 設置填充顏色
CGContextSetFillColorWithColor(ctx, [UIColor yellowColor].CGColor);
[[UIColor yellowColor] setFill];
/** 線條交匯處樣式
kCGLineJoinMiter——尖角
kCGLineJoinBevel——平角
kCGLineJoinRound——圓形
**/
CGContextSetLineJoin(ctx, kCGLineJoinRound);
// 繪制虛線,第二個參數為初始跳過幾個點開始繪制,第三個參數為一個CGFloat數組,指定你繪制的樣式,繪幾個點跳幾個點(下面為繪10個點,跳過5個,最后一個參數是上個參數數組元素的個數。
CGContextSetLineDash(ctx, 0, (CGFloat[]){10, 5}, 2);
// 默認系統會繪制填充這個矩形內部的最大橢圓,若矩形為正方形,則為圓
CGContextAddEllipseInRect(ctx, CGRectMake(40, 180, 240, 120));
// 畫切線弧,是說從 起點(50,30)到(100,80)畫一條線,然后再從(100,80)到(130,150)畫一條線,從這兩條線(無限延伸的)和 半徑 50 可以確定一條弧,
CGContextAddArcToPoint(ctx,100,80,130,150,50);
/** 繪制圓弧,餅狀圖() -- 畫圓的時候可通過 線條寬度 來實現中間空心圓效果
void CGContextAddArc (
CGContextRef c,
CGFloat x, // 圓心點坐標的x和y
CGFloat y,
CGFloat radius, // 半徑
CGFloat startAngle, // 繪制起始點的弧度值,一般在IOS繪圖里都使用弧度這個概念 #define RADIANS(x) ((x)*(M_PI)/180) // 角度轉弧度
CGFloat endAngle, // 繪制終點的弧度值
int clockwise // 1為順時針,0為逆時針。
);
**/
CGContextAddArc(ctx, 100, 100, 10, ((60.0)*(M_PI)/180), ((270.0)*(M_PI)/180), 0);
/* 裁剪當前路徑 -- 參照: http://blog.sina.com.cn/s/blog_b876b8ab0102v6gb.html */
// 使用非零繞數規則。
CGContextClip(ctx);
// 使用奇偶規則。
CGContextEOClip(ctx);
//CGContextClipToRect
//CGContextClipToRects
//CGContextGetClipBoundingBox
//CGContextClipToMask
/* 裁剪當前路徑 */
/* 構造路徑 -- 類似于后面的要講的 UIBezierPath */
// 創建一個 path 對象
CGMutablePathRef path = CGPathCreateMutable();
// 將路徑加入到圖形上下文中
CGContextAddPath(ctx, path);
// 制作具體路線 -- 上面兩步其實可以省略
CGPathMoveToPoint(path, NULL, 10, 10);
CGPathAddLineToPoint(path, NULL, 100, 100);
CGPathMoveToPoint(path, NULL, 20, 20);
CGPathAddLineToPoint(path, NULL, 200, 200);
// 渲染/繪制,并且可以設置繪制的類型
/*CGPathDrawingMode是填充方式,枚舉類型
kCGPathFill:只有填充(非零纏繞數填充),不繪制邊框
kCGPathEOFill:奇偶規則填充(多條路徑交叉時,奇數交叉填充,偶交叉不填充)
kCGPathStroke:只有邊框
kCGPathFillStroke:既有邊框又有填充
kCGPathEOFillStroke:奇偶填充并繪制邊框
*/
CGContextDrawPath(ctx, kCGPathFillStroke); // 等價于 CGContextStrokePath + CGContextFillPath
// 釋放資源 -- ARC 并不能處理這類的資源管理,必須手動釋放
CGPathRelease(path);
/* 構造路徑 -- 類似于后面的要講的 UIBezierPath */
// 封閉路徑,不需要一定設置路徑的終點,可以主動關閉
/*
1.起始與終點重合的直線、弧和曲線并不自動閉合路徑,我們必須調用CGContextClosePath來閉合路徑。
2.Quartz的一些函數將路徑的子路徑看成是閉合的。這些函數顯示地添加一條直線來閉合 子路徑,如同調用了CGContextClosePath函數。
3.在閉合一條子路徑后,如果程序再添加直線、弧或曲線到路徑,Quartz將在閉合的子路徑的起點開始創建一個子路徑。
*/
CGContextClosePath(ctx);
// 明確閉合路徑
CGPathCloseSubpath(path);
// 設置陰影 -- 參數依此是:圖形上下文,偏移量(CGSize),模糊值,陰影顏色
CGContextSetShadowWithColor(ctx, CGSizeMake(10, 10), 20.0f, [[UIColor grayColor] CGColor]);
/* 繪制漸變色效果 -- 亦可以 CAGradientLayer 鏈接:https://zsisme.gitbooks.io/ios-/content/chapter6/cagradientLayer.html*/
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
// 創建一個漸變的色值 1:顏色空間 2:漸變的色數組 3:位置數組,如果為NULL,則為平均漸變,否則顏色和位置一一對應 4:位置的個數
CGGradientRef gradient = CGGradientCreateWithColorComponents(colorSpace, (CGFloat[]){
// 如果想知道一個顏色比如[UIColor purpleColor]具體構成 -> CGColorGetComponents([UIColor purpleColor].CGColor); 返回一個數組,包括R,G,B以及alpha的值
0.3, 0.2, 0.2, 1.0,
0.1, 0.5, 0.2, 1.0,
0.6, 0.2, 0.7, 1.0
}, (CGFloat[]){
0.0, 0.5, 1.0
}, 3);
// 繪制漸變, 顏色的0對應start點,顏色的1對應end點,第四個參數是定義漸變是否超越起始點和終止點
CGContextDrawLinearGradient(ctx, gradient, CGPointMake(100, 300), CGPointMake(220, 480), 0);
/* 輻射漸變 有興趣的可以去玩一哈...
void CGContextDrawRadialGradient(
CGContextRef context,
CGGradientRef gradient, //先創造一個CGGradientRef,顏色是白,黑,location分別是0,1
CGPoint startCenter, // 白色的起點(中心圓點)
CGFloat startRadius, // 起點的半徑,這個值多大,中心就是多大一塊純色的白圈
CGPoint endCenter, // 白色的終點(可以和起點一樣,不一樣的話就像探照燈一樣從起點投影到這個終點,按照你的意圖應該和startCenter一樣
CGFloat endRadius, //終點的半徑, 按照你的意圖應該就是從中心到周邊的長
CGGradientDrawingOptions options //應該是 kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation
);
*/
// 釋放資源
CGGradientRelease(gradient);
CGColorSpaceRelease(colorSpace);
/* 繪制漸變色效果 */
// 繪制二次貝塞爾曲線
CGContextMoveToPoint(ctx, 20, 100);//移動到起始位置
/*
c:圖形上下文
cpx:控制點x坐標
cpy:控制點y坐標
x:結束點x坐標
y:結束點y坐標
*/
CGContextAddQuadCurveToPoint(ctx, 160, 0, 300, 100);
// 繪制三次貝塞爾曲線
CGContextMoveToPoint(ctx, 20, 500);
/*
c:圖形上下文
cp1x:第一個控制點x坐標
cp1y:第一個控制點y坐標
cp2x:第二個控制點x坐標
cp2y:第二個控制點y坐標
x:結束點x坐標
y:結束點y坐標
*/
CGContextAddCurveToPoint(ctx, 80, 300, 240, 500, 300, 300);
// 檢測當前的路徑是否包含指定的點
bool isExit = CGContextPathContainsPoint(ctx, CGPointMake(100, 100), kCGPathFill);
// 題外話..
[@"繪制字符串" drawAtPoint:CGPointMake(0, 0) withAttributes:@{NSFontAttributeName : [UIFont systemFontOfSize:[UIFont systemFontSize]]}];
}
ps(*):輔助點 ---------------------------------------------
- 因為 ARC 并不能管理 CG 這一套 api 的內存釋放,所以使用含有“Create”或“Copy”的函數創建的對象,使用完后必須釋放,否則將導致內存泄露.
-
附一個弧度圖:#define RADIANS(x) ((x)*(M_PI)/180) // 角度轉弧度
-
even-odd
和non-zero
規則參照: http://www.lxweimin.com/p/d4b8b5d931df 和 http://blog.csdn.net/jeffasd/article/details/52062375 - 貝塞爾曲線掃盲:很生動形象的解釋.
-
貝塞爾曲線配置:非常非常的實用,很棒的一個工具!還涉及到了之前動畫的
CAMediaTimingFunction
測試. -
附一個貝塞爾圖:
image.png
UIBezierPath+CAShapeLayer
前面介紹過,UIBezierPath
是對CGPathRef
數據類型的封裝.在 drawRect :
中,無須獲取圖形上下文,直接用UIBezierPath
創建路徑來繪制,比如:
- (void)drawRect:(CGRect)rect{
// 顏色
[[UIColor orangeColor] set];
UIBezierPath* path = [UIBezierPath bezierPath];
path.lineWidth = 5.f;
// 起點
[path moveToPoint:CGPointMake(10, 100)];
// 繪制線條
[path addLineToPoint:CGPointMake(100, 20)];
// 繪制渲染
[path stroke];
}
不過,一般,UIBezierPath
配合CAShapeLayer
一起使用.UIBezierPath給CAShapeLayer提供路徑,CAShapeLayer在提供的路徑中進行渲染,繪制出了Shape.
使用CAShapeLayer
有以下一些優點:
- 渲染快速.CAShapeLayer使用了硬件加速,繪制同一圖形會比用Core Graphics快很多。
- 高效使用內存.一個CAShapeLayer不需要像普通CALayer一樣創建一個寄宿圖形,所以無論有多大,都不會占用太多的內存.
- 不會被圖層邊界剪裁掉.一個CAShapeLayer可以在邊界之外繪制。你的圖層路徑不會像在使用Core Graphics的普通CALayer一樣被剪裁掉.
- 不會出現像素化。當你給CAShapeLayer做3D變換時,它不像一個有寄宿圖的普通圖層一樣變得像素化.
CAShapeLayer
繼承自CALayer
,常用屬性:
path:CGPathRef類型,配合 UIBezierPath 的 path
fillColor:填充path的顏色,或無填充。默認為不透明黑色。動畫的。
strokeColor:繪制的線條的顏色。
fillRule:填充path的規則。非零和偶奇。同CGPathDrawingMode
lineCap:線端點類型,同CGContextSetLineJoin
lineDashPattern:線性模版,這是一個NSNumber的數組,索引從1開始記,奇數位數值表示實線長度,偶數位數值表示空白長度。
lineDashPhase:線型模版的起始位置。
lineJoin:線拐點類型。kCALineJoinMiter-尖的,kCALineJoinRound-圓弧,kCALineJoinBevel-梯形
lineWidth:線寬
miterLimit:最大斜接長度。斜接長度指的是在兩條線交匯處和外交之間的距離。只有lineJoin屬性為kCALineJoinMiter時miterLimit才有效。邊角的角度越小,斜接長度就會越大。為了避免斜接長度過長,我們可以使用miterLimit屬性。如果斜接長度超過miterLimit的值,邊角會以lineJoin的“bevel”即kCALineJoinBevel類型來顯示。
strokeStart和strokeEnd:部分繪線,都是0.0~1.0的取值范圍.經常被用來制作動畫效果。
再來看看UIBezierPath
:是不是跟之前的 CG 很像
// 創建基本路徑
+ (instancetype)bezierPath;
// 創建矩形路徑
+ (instancetype)bezierPathWithRect:(CGRect)rect;
// 創建橢圓路徑
+ (instancetype)bezierPathWithOvalInRect:(CGRect)rect;
// 創建圓角矩形
+ (instancetype)bezierPathWithRoundedRect:(CGRect)rect cornerRadius:(CGFloat)cornerRadius; // rounds all corners with the same horizontal and vertical radius
// 創建指定位置圓角的矩形路徑
+ (instancetype)bezierPathWithRoundedRect:(CGRect)rect byRoundingCorners:(UIRectCorner)corners cornerRadii:(CGSize)cornerRadii;
// 創建弧線路徑
+ (instancetype)bezierPathWithArcCenter:(CGPoint)center radius:(CGFloat)radius startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle clockwise:(BOOL)clockwise;
// 通過CGPath創建
+ (instancetype)bezierPathWithCGPath:(CGPathRef)CGPath;
// 與之對應的CGPath,可賦值給 CAShapeLayer 的 path
@property(nonatomic) CGPathRef CGPath;
// 是否為空
@property(readonly,getter=isEmpty) BOOL empty;
// 整個路徑相對于原點的位置及寬高
@property(nonatomic,readonly) CGRect bounds;
// 當前畫筆位置
@property(nonatomic,readonly) CGPoint currentPoint;
// 線寬
@property(nonatomic) CGFloat lineWidth;
// 終點類型,同 CAShapeLayer
@property(nonatomic) CGLineCap lineCapStyle;
// 線條拐點的類型, 同 CAShapeLayer
@property(nonatomic) CGLineJoin lineJoinStyle;
// 兩條線交匯處內角和外角之間的最大距離, 同 CAShapeLayer
@property(nonatomic) CGFloat miterLimit;
// 繪線的精細程度,默認為0.6,數值越大,需要處理的時間越長
@property(nonatomic) CGFloat flatness;
// 決定使用even-odd(奇偶)或者non-zero(非零環繞)規則
@property(nonatomic) BOOL usesEvenOddFillRule;
// 反方向繪制path
- (UIBezierPath *)bezierPathByReversingPath;
// 設置畫筆起始點
- (void)moveToPoint:(CGPoint)point;
// 從當前點到指定點繪制直線
- (void)addLineToPoint:(CGPoint)point;
// 添加弧線, 同 CG
- (void)addArcWithCenter:(CGPoint)center radius:(CGFloat)radius startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle clockwise:(BOOL)clockwise NS_AVAILABLE_IOS(4_0);
// 添加二次貝塞爾曲線, 同 CG
- (void)addQuadCurveToPoint:(CGPoint)endPoint controlPoint:(CGPoint)controlPoint;
// 添加三次貝塞爾曲線, 同 CG
- (void)addCurveToPoint:(CGPoint)endPoint controlPoint1:(CGPoint)controlPoint1 controlPoint2:(CGPoint)controlPoint2;
// 閉合路徑,得到封閉圖形
- (void)closePath;
// 移除所有的點,刪除所有的subPath
- (void)removeAllPoints;
// 將bezierPath添加到當前path
- (void)appendPath:(UIBezierPath *)bezierPath;
// 填充
- (void)fill;
// 繪制/渲染,描繪線條
- (void)stroke;
// 在這以后的圖形繪制超出當前路徑范圍則不可見
- (void)addClip;
參考鏈接
iOS 繪圖教程
iOS核心動畫教程之CAShapeLayer
CoreGraphics之CGContextSaveGState與UIGraphicsPushContext
iOS --- CoreGraphics中三種繪圖context切換方式的區別
iOS開發系列--打造自己的“美圖秀秀”
關于CAShapeLayer
iOS繪圖 - UIBezierPath水波
動畫黃金搭檔:CADisplayLink & CAShapeLayer