iOS的繪圖之drawRect和drawLayer:inContext

作者:Love@YR
鏈接:http://blog.csdn.net/jingqiu880905/article/details/51851534
請尊重原創,謝謝!

demo下載
一個測試手動調用drawRect方法的demo

這篇文章講了用Core Graphics來繪製各種圖形,可以參考:http://blog.csdn.net/rhljiayou/article/details/9919713

簡單的繪圖可以直接使用CocoaTouch 層提供的UIKit Framework裏的方法來完成。
但UI比較複雜、比較個性化,普通的UI控件無法實現的可以利用Core Graphics Framework來繪製。
Core Graphics是一套基於C的API框架,使用了Quartz2D作爲繪圖引擎。在Media Layer層。

如果對層的概念比較模糊,建議看下這篇文章的圖,很直觀。

網上說Quartz2D提供了以下幾種類型的Graphics Context:
Bitmap Graphics Context
PDF Graphics Context
Window Graphics Context
Layer Graphics Context
Printer Graphics Context
不知道哪裏得來的資料,反正我是沒找到那麼多。只看到了CGLayerRef(CGLayerCreateWithContext),CGPDFContextCreate,CGBitmapContextCreate即Layer,PDF,Bitmap這3種context

根據這篇文章
的E第4條,drawRect:方法中UIGraphicsGetCurrentContext()取得的是一個Layer Graphics Context。(這樣就能理解爲啥要畫image的時候必須先創建一個image的context)

看下此方法的描述:
You can get a reference to the graphics context using the UIGraphicsGetCurrentContext function, but do not establish a strong reference to the graphics context because it can change between calls to the drawRect: method.
This method is called when a view is first displayed or when an event occurs that invalidates a visible part of the view. You should never call this method directly yourself. To invalidate part of your view, and thus cause that portion to be redrawn, call the setNeedsDisplay or setNeedsDisplayInRect: method instead.

主要就是說你不能手動調drawRect:方法,而是調用setNeedsDisplay去讓系統自動調用。

經過上述demo的實踐我們看出:
當一個view被addsubview到其他view上時
1.先隱式地把此view的layer的CALayerDelegate設置成此view
2.調用此view的self.layer的drawInContext方法
3.由於drawLayer方法的註釋 If defined, called by the default implementation of -drawInContext:
說明了drawInContext裏if([self.delegate responseToSelector:@selector(drawLayer:inContext:)])時就執行drawLayer:inContext:方法,這裏我們因爲實現了drawLayer:inContext:所以會執行
4.[super drawLayer:layer inContext:ctx]會讓系統自動調用此view的drawRect:方法
至此self.layer畫出來了
5.在self.layer上再加一個子layer來繪圖,當調用[layer setNeedsDisplay];時會自動調用此layer的drawInContext方法。drawInContext方法不能手動調用,只能通過這個方法讓系統自動調用
6.如果drawRect不重寫,就不會調用其layer的drawInContext方法,也就不會調用drawLayer:inContext方法

在UIView的drawRect:rect方法中:
1.直接用UIKit提供的繪圖如UIBezierPath去stroke描邊,fill填充。
2.也可以用CoreGraphics在ctx上繪圖
需要先UIGraphicsGetCurrentContext()得到上下文CGContextDrawPath(ctx,kCGPathFillStroke);//既有填充又有邊框

在CALayer的delegate方法drawLayer: inContext:中。
1.直接用UIKit提供的繪圖
需要UIGraphicsPushContext(ctx);把當ctx轉爲當前上下文,可用UIBezierPath的stroke,fill去畫,畫完圖再UIGraphicsPopContext();
2.也可以用CoreGraphics在ctx上繪圖
直接用傳過來的ctx即可。如:CGContextFillPath(ctx);

在drawLayer: inContext:方法中想要繪製bitmap
需要自己創建bitmap上下文:UIGraphicsBeginImageContextWithOptions
可以用UIImage* img = UIGraphicsGetImageFromCurrentImageContext();來得到image之後再UIGraphicsEndImageContext();

爲什麼有的時候要UIGraphicsGetCurrentContext,有的時候要push,有的時候直接用呢?我的理解是UIKit和CoreGraphics的上下文不同,座標系也相反,前者y軸向下增大,後者y軸向上增大,所以如果你用UIKit提供的方式繪圖和用CoreGraphics提供的方式繪圖都需要把傳來的ctx轉成即將繪圖的ctx。
UIGraphicsPushContext 是在UIKit裏的方法,無返回,是將傳來的上下文CGContextRef轉成當前上下文
CGContextRef ctx = UIGraphicsGetCurrentContext();得到的ctx是CoreGraphics裏的類型,
drawLayer:inContext裏傳來的也是CGContextRef類型。

知道了用CGContextAddEllipseInRect來畫橢圓之前一定要先UIGraphicsGetCurrentContext或者直接傳過來ctx,後來又發現一種畫橢圓的方法:CGPathAddEllipseInRect
如下示例方法:

   CGMutablePathRef path=CGPathCreateMutable();
     //2.2把繪圖信息添加到路徑裏
     CGPathMoveToPoint(path, NULL, 20, 20);
     CGPathAddLineToPoint(path, NULL, 200, 300);
    //2.3把路徑添加到上下文中
    //把繪製直線的繪圖信息保存到圖形上下文中
     CGContextAddPath(ctx, path); //仍需把path加入ctx裏
     //3.渲染
     CGContextStrokePath(ctx);

參考此篇文章:https://www.mgenware.com/blog/?p=493
http://www.cnblogs.com/wendingding/p/3782679.html

看到UIGraphicsPushContext(ctx)和CGContextSaveGState(ctx)可能會有些疑惑,下面的描述摘抄自以下文章:
http://www.cocoachina.com/industry/20140115/7703.html

使用UiKit,你只能在當前上下文中繪圖,所以如果你當前處於UIGraphicsBeginImageContextWithOptions函數或drawRect:方法中,你就可以直接使用UIKit提供的方法進行繪圖。如果你持有一個context:參數,那麼使用UIKit提供的方法之前,必須將該上下文參數轉化爲當前上下文。幸運的是,調用UIGraphicsPushContext 函數可以方便的將context:參數轉化爲當前上下文,記住最後別忘了調用UIGraphicsPopContext函數恢復上下文環境。

因爲圖形上下文在每一時刻都有一個確定的狀態,該狀態概括了圖形上下文所有屬性的設置。爲了便於操作這些狀態,圖形上下文提供了一個用來持有狀態的棧。調用CGContextSaveGState函數,上下文會將完整的當前狀態壓入棧頂;調用CGContextRestoreGState函數,上下文查找處在棧頂的狀態,並設置當前上下文狀態爲棧頂狀態。

因此一般繪圖模式是:在繪圖之前調用CGContextSaveGState函數保存當前狀態,接着根據需要設置某些上下文狀態,然後繪圖,最後調用CGContextRestoreGState函數將當前狀態恢復到繪圖之前的狀態。要注意的是,CGContextSaveGState函數和CGContextRestoreGState函數必須成對出現,否則繪圖很可能出現意想不到的錯誤,這裏有一個簡單的做法避免這種情況。

但你不需要在每次修改上下文狀態之前都這樣做,因爲你對某一上下文屬性的設置並不一定會和之前的屬性設置或其他的屬性設置產生衝突。你完全可以在不調用保存和恢復函數的情況下先設置線條顏色爲紅色,然後再設置爲藍色。但在一定情況下,你希望你對狀態的設置是可撤銷的

舉個例子:

-(void)drawRect:(CGRect)rect{ 
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    [[UIColor greenColor] set];
    CGContextSaveGState(ctx); 
    [[UIColor brownColor] set];
    CGContextFillRect(ctx, CGRectMake(0, 0, self.frame.size.width, self.frame.size.height));
    CGContextRestoreGState(ctx);
    CGContextFillRect(ctx, CGRectMake(self.frame.size.width / 2 - 25, self.frame.size.height / 2 - 25, 50, 50));
}

結果是有個綠色的小方框和棕色的整個view,說明restore會用了save之前的顏色。一般用於撤銷。
還有一篇文章也可以看下:
http://blog.csdn.net/lihangqw/article/details/9969961

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