iOS 重绘之drawRect

avatar
奇舞团移动端团队 @奇舞团

级别: ★★☆☆☆
标签:「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篇)
奇舞周刊