iOS 重繪之drawRect

QiShare發表於2019-03-04

級別: ★★☆☆☆
標籤:「iOS」「drawRect」「繪製」「重繪」
作者: dac_1033
審校: QiShare團隊

一、drawRect介紹

drawRect是UIView類的一個方法,在drawRect中所呼叫的重繪功能是基於Quartz 2D實現的,Quartz 2D是一個二維圖形繪製引擎,支援iOS環境和Mac OS X環境。利用UIKit框架提供的控制元件,我們能實現一些簡單的UI介面,但是,有些UI介面比較複雜,用普通的UI控制元件無法實現,或者實現效果不佳,這時可以利用Quartz 2D技術將控制元件內部的結構畫出來,自定義所需控制元件,這也是Quartz 2D框架在iOS開發中一個很重要的價值。

iOS的繪圖操作是在UIView類的drawRect方法中進行的,我們可以重寫一個view的drawRect方法,在其中進行繪圖操作,在首次顯示該view時程式會自動呼叫此方法進行繪圖。 在多次手動重複繪製的情況下,需要呼叫UIView中的setNeedsDisplay方法,則程式會自動呼叫drawRect方法進行重繪。PS:蘋果官閘道器於drawRect的介紹

二、drawRect的使用過程

在view的drawRect方法中,利用Quartz 2D提供的API繪製圖形的步驟:
1)新建一個view,繼承自UIView,並重寫drawRect方法;
2)在drawRect方法中,獲取圖形上下文;
3)繪圖操作;
4)渲染。

三、何為CGContext?

Quartz 2DCoreGraphics框架的一部分,因此其中的相關類及方法都是以CG為字首。在drawRect重繪過程中最常用的就是CGContext類。CGContext又叫圖形上下文,相當於一塊畫板,以堆疊形式存放,只有在當前context上繪圖才有效。iOS又分多種圖形上下文,其中UIView自帶提供的在drawRect方法中通過 UIGraphicsGetCurrentContext獲取,還有專門為圖片處理的context,還有pdf的context等等均有特定的獲取方法,本文只對第一種做相關介紹。

CGContext 類中的常用方法:

// 獲取當前上下文
CGContextRef context = UIGraphicsGetCurrentContext(); 

// 移動畫筆
CGContextMoveToPoint 
// 在畫筆位置與point之間新增將要繪製線段 (在draw時才是真正繪製出來)
CGContextAddLineToPoint 
// 繪製橢圓
CGContextAddEllipseInRect 
CGContextFillEllipseInRect
// 設定線條末端形狀
CGContextSetLineCap 
// 畫虛線
CGContextSetLineDash 
// 畫矩形
CGContextAddRect 
CGContextStrokeRect 
CGContextStrokeRectWithWidth 
// 畫一些線段
CGContextStrokeLineSegments 

// 畫弧: 以(x1, y1)為圓心radius半徑,startAngle和endAngle為弧度
CGContextAddArc(context, x1, y1, radius, startAngle, endAngle, clockwise);
// 先畫兩條線從point 到 (x1, y1) , 從(x1, y1) 到(x2, y2) 的線  切裡面的圓
CGContextAddArcToPoint(context, x1, y1,  x2,  y2, radius);

// 設定陰影
CGContextSetShadowWithColor 
// 設定填充顏色
CGContextSetRGBFillColor 
// 設定畫筆顏色
CGContextSetRGBStrokeColor 
// 設定填充顏色空間
CGContextSetFillColorSpace 
// 設定畫筆顏色空間
CGConextSetStrokeColorSpace 
// 以當前顏色填充rect
CGContextFillRect 
// 設定透明度
CGContextSetAlaha 

// 設定線的寬度
CGContextSetLineWidth 
// 畫多個矩形
CGContextAddRects 
// 畫曲線
CGContextAddQuadCurveToPoint 
// 開始繪製圖片
CGContextStrokePath
// 設定繪製模式 
CGContextDrawPath 
// 封閉當前線路
CGContextClosePath 
// 反轉畫布
CGContextTranslateCTM(context, 0, rect.size.height);   CGContextScaleCTM(context, 1.0, -1.0);
// 從原圖片中取小圖
CGImageCreateWithImageInRect 

// 畫圖片
CGImageRef image=CGImageRetain(img.CGImage);
CGContextDrawImage(context, CGRectMake(10.0, height - 100.0, 90.0, 90.0), image);

// 實現漸變顏色填充
CGContextDrawLinearGradient(context, gradient, CGPointMake(0.0, 0.0) ,CGPointMake(0.0, self.frame.size.height), kCGGradientDrawsBeforeStartLocation);

複製程式碼

四、用drawRect方法重繪的例項

我們在drawRect方法中繪製一些圖形,如圖:

drawRect重繪

程式碼實現如下:

- (void)drawRect:(CGRect)rect {
    
    //1. 注:如果沒有獲取context時,是什麼都不做的(背景無變化)
    [super drawRect:rect];
    
    // 獲取上下文
    CGContextRef context =UIGraphicsGetCurrentContext();
    CGSize size = rect.size;
    CGFloat offset = 20;
    
    // 畫腦袋
    CGContextSetRGBStrokeColor(context,1,1,1,1.0);
    CGContextSetLineWidth(context, 1.0);
    CGContextAddArc(context, size.width / 2, offset + 30, 30, 0, 2*M_PI, 0);
    CGContextDrawPath(context, kCGPathStroke);
    
    // 畫眼睛和嘴巴
    CGContextMoveToPoint(context, size.width / 2 - 23, 40);
    CGContextAddArcToPoint(context, size.width / 2 - 15, 26, size.width / 2 - 7, 40, 10);
    CGContextStrokePath(context);
    
    CGContextMoveToPoint(context, size.width / 2 + 7, 40);
    CGContextAddArcToPoint(context, size.width / 2 + 15, 26, size.width / 2 + 23, 40, 10);
    CGContextStrokePath(context);//繪畫路徑
    
    CGContextMoveToPoint(context, size.width / 2 - 8, 65);
    CGContextAddArcToPoint(context, size.width / 2, 80, size.width / 2 + 8, 65, 10);
    CGContextStrokePath(context);//繪畫路徑
    
    // 畫鼻子
    CGPoint nosePoints[3];
    nosePoints[0] = CGPointMake(size.width / 2, 48);
    nosePoints[1] = CGPointMake(size.width / 2 - 3, 58);
    nosePoints[2] = CGPointMake(size.width / 2 + 3, 58);
    CGContextAddLines(context, nosePoints, 3);
    CGContextClosePath(context);
    CGContextDrawPath(context, kCGPathFillStroke);
    
    // 畫脖子
    CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor);
    CGContextStrokeRect(context, CGRectMake(size.width / 2 - 5, 80, 10, 10));
    CGContextFillRect(context,CGRectMake(size.width / 2 - 5, 80, 10, 10));
    
//    // 畫衣裳
//    CGPoint clothesPoints[4];
//    clothesPoints[0] = CGPointMake(size.width / 2 - 30, 90);
//    clothesPoints[1] = CGPointMake(size.width / 2 + 30, 90);
//    clothesPoints[2] = CGPointMake(size.width / 2 + 100, 200);
//    clothesPoints[3] = CGPointMake(size.width / 2 - 100, 200);
//    CGContextAddLines(context, clothesPoints, 4);
//    CGContextClosePath(context);
//    CGContextDrawPath(context, kCGPathFillStroke);
    
    // 衣裳顏色漸變
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathMoveToPoint(path, NULL, size.width / 2 - 30, 90);
    CGPathAddLineToPoint(path, NULL, size.width / 2 + 30, 90);
    CGPathAddLineToPoint(path, NULL, size.width / 2 + 100, 200);
    CGPathAddLineToPoint(path, NULL, size.width / 2 - 100, 200);
    CGPathCloseSubpath(path);
    [self drawLinearGradient:context path:path startColor:[UIColor cyanColor].CGColor endColor:[UIColor yellowColor].CGColor];
    CGPathRelease(path);
    
    // 畫胳膊
    CGContextSetFillColorWithColor(context, [UIColor colorWithRed:0 green:1 blue:1 alpha:1].CGColor);
    CGContextMoveToPoint(context, size.width / 2 - 28, 90);
    CGContextAddArc(context, size.width / 2 - 28, 90, 80,  - M_PI, -1.05 * M_PI, 1);
    CGContextClosePath(context);
    CGContextDrawPath(context, kCGPathFill);
    CGContextMoveToPoint(context, size.width / 2 + 28, 90);
    CGContextAddArc(context, size.width / 2 + 28, 90, 80,  0, 0.05 * M_PI, 0);
    CGContextClosePath(context);
    CGContextDrawPath(context, kCGPathFill);
    
    // 畫左手
    CGPoint aPoints[2];
    aPoints[0] =CGPointMake(size.width / 2 - 30 - 81, 90);
    aPoints[1] =CGPointMake(size.width / 2 - 30 - 86, 90);
    CGContextAddLines(context, aPoints, 2);
    aPoints[0] =CGPointMake(size.width / 2 - 30 - 80, 93);
    aPoints[1] =CGPointMake(size.width / 2 - 30 - 85, 93);
    CGContextAddLines(context, aPoints, 2);
    CGContextDrawPath(context, kCGPathStroke);
    // 畫右手
    aPoints[0] =CGPointMake(size.width / 2 + 30 + 81, 90);
    aPoints[1] =CGPointMake(size.width / 2 + 30 + 86, 90);
    CGContextAddLines(context, aPoints, 2);
    aPoints[0] =CGPointMake(size.width / 2 + 30 + 80, 93);
    aPoints[1] =CGPointMake(size.width / 2 + 30 + 85, 93);
    CGContextAddLines(context, aPoints, 2);
    CGContextDrawPath(context, kCGPathStroke);
    
//    // 畫虛線
//    aPoints[0] =CGPointMake(size.width / 2 + 30 + 81, 90);
//    aPoints[1] =CGPointMake(size.width / 2 + 30 + 86, 90);
//    CGContextAddLines(context, aPoints, 2);
//    aPoints[0] =CGPointMake(size.width / 2 + 30 + 80, 93);
//    aPoints[1] =CGPointMake(size.width / 2 + 30 + 85, 93);
//    CGContextAddLines(context, aPoints, 2);
//    CGFloat arr[] = {1, 1};
//    CGContextSetLineDash(context, 0, arr, 2);
//    CGContextDrawPath(context, kCGPathStroke);
    
    // 畫雙腳
    CGContextSetFillColorWithColor(context, [UIColor blueColor].CGColor);
    CGContextAddEllipseInRect(context, CGRectMake(size.width / 2 - 30, 210, 20, 15));
    CGContextDrawPath(context, kCGPathFillStroke);
    CGContextSetFillColorWithColor(context, [UIColor yellowColor].CGColor);
    CGContextAddEllipseInRect(context, CGRectMake(size.width / 2 + 10, 210, 20, 15));
    CGContextDrawPath(context, kCGPathFillStroke);
    
    // 繪製圖片
    UIImage *image = [UIImage imageNamed:@"img_watch"];
    [image drawInRect:CGRectMake(60, 270, 100, 120)];
    //[image drawAtPoint:CGPointMake(100, 340)];
    //CGContextDrawImage(context, CGRectMake(100, 340, 20, 20), image.CGImage);
    
    // 繪製文字
    UIFont *font = [UIFont boldSystemFontOfSize:20.0];
    NSDictionary *attriDict = @{NSFontAttributeName:font, NSForegroundColorAttributeName:[UIColor redColor]};
    [@"繪製文字" drawInRect:CGRectMake(180, 270, 150, 30) withAttributes:attriDict];
}

- (void)drawLinearGradient:(CGContextRef)context
                      path:(CGPathRef)path
                startColor:(CGColorRef)startColor
                  endColor:(CGColorRef)endColor {
    
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGFloat locations[] = { 0.0, 1.0 };
    NSArray *colors = @[(__bridge id) startColor, (__bridge id) endColor];
    CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (__bridge CFArrayRef) colors, locations);
    CGRect pathRect = CGPathGetBoundingBox(path);
    //具體方向可根據需求修改
    CGPoint startPoint = CGPointMake(CGRectGetMidX(pathRect), CGRectGetMinY(pathRect));
    CGPoint endPoint = CGPointMake(CGRectGetMidX(pathRect), CGRectGetMaxY(pathRect));
    CGContextSaveGState(context);
    CGContextAddPath(context, path);
    CGContextClip(context);
    CGContextDrawLinearGradient(context, gradient, startPoint, endPoint, 0);
    CGContextRestoreGState(context);
    CGGradientRelease(gradient);
    CGColorSpaceRelease(colorSpace);
}
複製程式碼

注:
1)當view未設定背景顏色時,重繪區域的背景顏色預設為‘黑’;
2)設定畫筆顏色的方法CGContextSetRGBStrokeColor,設定填充顏色的方法CGContextSetFillColorWithColor
3)每次繪製獨立的圖形結束時,都要實時呼叫CGContextDrawPath方法來將這個獨立的圖形繪製出來,否則多次CGContextMoveToPoint會使繪製的圖形亂掉;
4)區別CGContextAddArcCGContextAddArcToPoint
5)畫虛線時,之後所有的線條均變成虛線(除非再手動設定成是實現)

五、CAShapeLayer繪圖與drawRect重繪的比較

在網上查了一些CAShapeLayerdrawRect重繪的一些比較,整理如下,有助於我們學習與區分:
(1)兩種自定義控制元件樣式的方法各有優缺點,CAShapeLayer配合貝賽爾曲線使用時,繪圖形狀更靈活,而drawRect只是一個方法而已,在其中更適合繪製大量有規律的通用的圖形;
(2)CALayer的屬性變化預設會有動畫,drawRect繪圖沒有動畫;
(3)CALayer繪製圖形是實時的,drawRect多次重繪需要手動呼叫setNeedsLayout
(4)效能方面,CAShapeLayer使用了硬體加速,繪製同一圖形會比用Core Graphics快很多,CAShapeLayer屬於CoreAnimation框架,動畫渲染直接提交給手機GPU,不消耗內,而Core Graphics會消耗大量的CPU資源。

另外,原始碼中還通過重繪實現了兩個簡單的排序演算法,工程原始碼GitHub地址


關注我們的途徑有:
QiShare(簡書)
QiShare(掘金)
QiShare(知乎)
QiShare(GitHub)
QiShare(CocoaChina)
QiShare(StackOverflow)
QiShare(微信公眾號)

推薦文章:
iOS 編寫高質量Objective-C程式碼(八)
iOS KVC與KVO簡介
iOS 本地化(IB篇)
iOS 本地化(非IB篇)
奇舞週刊

相關文章