iOS 重绘之drawRect
1. 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方法进行重绘。苹果官网关于drawRect的介绍
2. drawRect的使用过程
在view的drawRect方法中,利用Quartz 2D 提供的API绘制图形的步骤:
1)新建一个view,继承自UIView,并重写drawRect方法;
2)在drawRect方法中,获取图形上下文;
3)绘图操作;
4)渲染。
3. 何为CGContext
Quartz 2D是CoreGraphics框架的一部分,因此其中的相关类及方法都是以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);
5. 用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)区别CGContextAddArc与CGContextAddArcToPoint;
5)画虚线时,之后所有的线条均变成虚线(除非再手动设置成是实现)
6. CAShapeLayer绘图与drawRect重绘的比较
在网上查了一些CAShapeLayer与drawRect重绘的一些比较,整理如下,有助于我们学习与区分:
(1)两种自定义控件样式的方法各有优缺点,CAShapeLayer配合贝赛尔曲线使用时,绘图形状更灵活,而drawRect只是一个方法而已,在其中更适合绘制大量有规律的通用的图形;
(2)CALayer的属性变化默认会有动画,drawRect绘图没有动画;
(3)CALayer绘制图形是实时的,drawRect多次重绘需要手动调用setNeedsLayout;
(4)性能方面,CAShapeLayer使用了硬件加速,绘制同一图形会比用Core Graphics快很多,CAShapeLayer属于CoreAnimation框架,动画渲染直接提交给手机GPU,不消耗内,而Core Graphics会消耗大量的CPU资源。
另外,源码中还通过重绘实现了两个简单的排序算法,工程源码GitHub地址