iOS 繪圖使用總結 Core Graphics UIBezierPath+CAShapeLayer 參考鏈接

上一篇介紹了動畫相關的 api,本篇涉及的就是如何把圖形畫出來了,之前也做過不少相關的畫圖工作,但都比較簡單少量,也沒有刻意的去比較或者直接就是接入了第三方庫去實現...工作需要實現大量的 K線繪製以及各種狀態變更,因此總結一下方便查閱,提升工作效率.


iOS提供了兩套繪圖的框架,UIBezierPathCore Graphics.

  • UIBezierPath是UIKit中的一個關於圖形繪製的類,其實是對Core Graphics框架關於path(CGPathRef數據類型的封裝)的進一步封裝,語法就是 OC 範.
  • Core Graphics也被稱作QuartZ或QuartZ 2D,更接近底層,功能更強大, 提供的都是C語言的函數接口.

Core Graphics

在繪圖之前,我們先需要搞清楚下面幾個概念:

  1. 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函數關閉圖形上下文(類似於數據庫的打開與關閉)。UIGraphicsBeginImageContextWithOptionsUIGraphicsEndImageContext成對出現,類似的 api 還有很多,也可以嵌套.

  1. 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(*):輔助點 ---------------------------------------------

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章