iOS 绘制图形

CGContextRef vs CAShapeLayer

Posted by LOLITA0164 on June 23, 2017

CGContextRef

Graphics Context是图形上下文,可以将其理解为一块画布,我们可以在上面进行绘画操作,绘制完成后,将画布放到我们的View中显示,View可以看作是一个画框,CGContextRef 则看作是 Quart 2D 的绘图环境。

如果想要在 iOS 应用中使用 CGContextRef 绘制图形,需要重写视图方法 drawRect: 方法,该方法在视图在屏幕上可见或者其内容需要更新时被调用。在该方法中,你可以使用 UIGraphicsGetCurrentContext 方法来获取到当前的绘制环境。

路径

路径决定了一个或者多个图形形状,路径可以包含直线、曲线,它可以是开发的或封闭的,形状可以是线、圆、矩形、星形或者复杂的不规则图形,路径既有线条也有填充。下面列举了几一些常见的图形。

Quartz支持基于路径的绘图

可以使用函数 CGContextMoveToPoint 来指定新路径的起始点,CGCurrentPoint 表示当前点,这个是用于构建路径的最后位置,一般也是接下来路径的起点。例如调用 CGContextMoveToPoint 将位置设置为(10,10),然后绘制一条长50个单位的水平线,那么最后的位置为(60,10),即当前点,后面绘制的直线、圆弧和曲线都是从当前点开始。

线

一条线段由起始点决定,假定它的起始点为路径中的当前点,那么,只需指定其端点即可确定一条线并将其附加到路径上。可以使用函数 CGContextAddLineToPoint 方法附加一条线到路径上。也可以使用 CGContextAddLines 将一系列点串联起来。

这里的弧是圆弧段。Quartz 提供了两个创建弧的函数。

  • CGContextAddArc:从圆形创建一个弧段,你只需要指定圆的中心 、半径和角度(弧度)即可。当弧度为 2 pi 时,则绘制出了一个圆。

  • CGContextAddArcToPoint:当想要给矩形某个角设置为圆角时,这个方法异常的适用。Quartz使用两个点和圆半径来创建圆弧,该弧的中心点即为圆的中心点,弧上的每一个点都是该圆的切点。如下图所示,圆的红色部分就是实际上绘制的弧。

使用两个点和半径来确定圆弧 如果当前路径已经存在了路径,那么Quartz会将当前点的直线段作为弧的起始点。如果当前路径为空,那么Quartz会在弧的起始点创建一个新的子路径,并且不会添加到初试直线段。

曲线

Quartz支持两种曲线,二次和三次贝塞尔曲线,这两种曲线都是代数曲线。

  • CGContextAddQuadCurveToPoint:二次贝塞尔曲线通过一个控制点和终点来绘制曲线,如下图所示。由于控制点确定曲线的拱起的方向和幅度,但是如果仅使用一个控制点显然是不能创建出很多有趣的形状,例如,无法使用一个控制点来创建交叉。

二次贝塞尔

  • CGContextAddCurveToPoint:三次贝塞尔曲线,相比二次尔塞尔曲线,三次贝塞尔多出了一个控制点,这两个控制点的放置位置决定了曲线的几何形状。如果控制点都在起始点和终点之上,则曲线向上拱起,反之,则曲线向下拱起。

三次贝塞尔

关闭路径

要关闭当前路径,可以使用 CGContextClosePath ,该函数将路径的起点和当前点链接起来,使得路径成闭合路径。以路径起点结束的直线、圆弧和曲线实际上并没有关闭子路径,此时就必须显示的调用函数来关闭路径。

一些Quartz函数会隐式的封闭路径。

关闭路径后,如果在想路径中添加子路径,那么Quartz将从刚刚关闭的路径的起点开始一个新的子路径。

椭圆

椭圆本质上是一个压扁的圆。

Quartz提供 CGContextAddEllipseInRect 便捷方法来创建一个椭圆(相比于提供两个焦点的方式,使用Rect,用户更容易控制),你只需提供绘图上线文和绘制的矩形范围即可。如果该矩形的宽高相等,即绘制了圆形。

矩形

  • CGContextAddRect:创建一个矩形

  • CGContextAddRects:添加多个矩形

持有路径

绘制路径后,路径信息将被释放,如果你希望该路径可以反复使用,你可以使用两种数据类型 CGPathRefCGMutablePathRef 。可以使用函数 CGPathCreateMutable 来创建可变的CGPath对象,之后可以在其中添加直线,圆弧,曲线和矩形等等。Quartz提供了一组CGPath函数,这些路径函数在CGPath对象上运行,而不是在图形上下文上运行,因此你操作CGPath是需要调用对应的路径函数:

  • CGPathCreateMutable,取代 CGContextBeginPath
  • CGPathMoveToPoint,取代 CGContextMoveToPoint
  • CGPathAddLineToPoint,取代 CGContextAddLineToPoint
  • CGPathAddCurveToPoint,取代 CGContextAddCurveToPoint
  • CGPathAddEllipseInRect,取代 CGContextAddEllipseInRect
  • CGPathAddArc,取代 CGContextAddArc
  • CGPathAddRect,取代 CGContextAddRect
  • CGPathCloseSubpath,取代 CGContextClosePath

如果要将持有的路径附加到图形上下文,可以调用函数 CGContextAddPath

绘制的一些参数

在完成一条路径的创建后,我们可以通过描边或者填充来绘制当前路径。描边将路径全部绘出,填充则绘制路径中所包含的区域,当然,我们可以两种方式都选择。描边线可以设置其宽度、颜色等等,填充可设置填充颜色,填充模式等等。

参数 参数字段
线宽 CGContextSetLineWidth
线连接 CGContextSetLineJoin
线帽 CGContextSetLineCap
斜接限制 CGContextSetMiterLimit
Line dash pattern CGContextSetLineDash
Stroke color space CGContextSetStrokeColorSpace
描边色 CGContextSetStrokeColorCGContextSetStrokeColorWithColor
Stroke pattern CGContextSetStrokePattern

线段的链接风格

线段的链接风格

线段端点的风格

线帽的风格

描边路径

描边路径

填充路径

填充路径

一些示例

/*****************************矩形*****************************/
//1.获取图形上下文,即画板
CGContextRef ctx = UIGraphicsGetCurrentContext();
//2.绘制图形
CGContextAddRect(ctx, CGRectMake(10, 10, 40, 40));
//3.设置相关属性
[[UIColor redColor] setStroke];    // 颜色 CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor);
CGContextSetLineWidth(ctx, 5);      //线条到宽度
//4.渲染到画板上
CGContextStrokePath(ctx);           //空心的

/*****************************圆弧/圆*****************************/
//1.获取图形上下文,即画板
CGContextRef ctx = UIGraphicsGetCurrentContext();
//2.绘制图形
CGContextAddArc(ctx, 100, 30, 30, 0, M_PI, 0);         //最后一个参数表示方向(顺时针或者逆时针)
//3.设置相关属性
[[UIColor redColor] set];           //填充色fill,线条色stroke都是一样的
CGContextSetLineWidth(ctx, 5);      //线条到宽度
//4.渲染到画板上
CGContextStrokePath(ctx);           //空心的
//CGContextFillPath(ctx);           //填充实心的 半圆
/*****************************椭圆*****************************/
//1.获取图形上下文,即画板
CGContextRef ctx = UIGraphicsGetCurrentContext();
//2.绘制图形
CGContextAddEllipseInRect(ctx, CGRectMake(150, 10, 80, 40));        //在矩形内绘制椭圆,宽度和高度相等则为圆形
//3.设置相关属性
[[UIColor redColor] set];       
//4.渲染到画板上
CGContextStrokePath(ctx);            //空心的
//CGContextFillPath(ctx);            //填充
/*****************************直线*****************************/
//1.获取图形上下文,即画板
CGContextRef ctx = UIGraphicsGetCurrentContext();
//2.绘制图形
CGContextMoveToPoint(ctx, 10, 100);                     //起点
CGContextAddLineToPoint(ctx, kScreenWidth-10, 100);     //其他点
//3.设置相关属性
CGContextSetRGBStrokeColor(ctx, 0, 1, 0, 1);            //RGB类型的颜色
CGContextSetLineCap(ctx, kCGLineCapSquare);             //起点和重点
CGContextSetLineJoin(ctx, kCGLineJoinBevel);            //转角
CGContextSetLineWidth(ctx, 5);                          //线条的宽度
CGContextStrokePath(ctx);    //直线只能绘制空心的,不能调用CGContextFillPath(ctx)
/*****************************不规则图形*****************************/
//1.获取图形上下文,即画板
CGContextRef ctx = UIGraphicsGetCurrentContext();
//2.绘制图形
CGContextMoveToPoint(ctx, 20, 150);             //设置起点                     
CGContextAddLineToPoint(ctx, 100, 180);         //
CGContextClosePath(ctx);                        //合并路径
//3.设置相关属性
CGContextSetLineWidth(ctx, 5);                  //线宽
CGContextSetRGBStrokeColor(ctx, 0, 1, 0, 1);    //RGB类型的颜色
CGContextSetLineCap(ctx, kCGLineCapSquare);     //终点样式
CGContextSetLineJoin(ctx, kCGLineJoinBevel);    //连接点样式
//[[UIColor redColor] setFill];  
CGContextSetFillColorWithColor(ctx, [UIColor redColor].CGColor);  //设置填充色
//[[UIColor redColor] setStroke];  
CGContextSetStrokeColorWithColor(ctx, [UIColor brownColor].CGColor);  //路径颜色
/*
 kCGPathFill,           //填充
 kCGPathEOFill,        
 kCGPathStroke,         //线条
 kCGPathFillStroke,     //线条+填充
 kCGPathEOFillStroke
*/
CGContextDrawPath(ctx, kCGPathFillStroke);      //fill  and  stroke
/*****************************绘制图片*****************************/
 UIImage *img = [UIImage imageNamed:@"QQ"];
[img drawAtPoint:CGPointMake( 150, 125)];    //绘制到指定的点,图片size
//[img drawInRect:CGRectMake(120, 140, img.size.width, img.size.height)];       //指定位置大小
//[img drawAsPatternInRect:CGRectMake(0, 0, kScreenWidth, kScreenHeight/2.0)];      //多个平铺
/*****************************绘制文字*****************************/
NSString *str = @"少年不识愁滋味,为赋新词强说愁\n而今识得愁滋味,却道天凉好个秋";
//设置文字属性
NSMutableParagraphStyle *style = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
style.alignment = NSTextAlignmentCenter;
NSDictionary *dic = @{NSForegroundColorAttributeName:[UIColor redColor],
                          NSFontAttributeName:[UIFont systemFontOfSize:18],
                          NSParagraphStyleAttributeName:style};
//绘制
CGSize size = [str sizeWithAttributes:dic];
CGRect strRect = CGRectMake((kScreenWidth-size.width)/2.0, 200, size.width, size.height);
[str drawWithRect:strRect options:NSStringDrawingTruncatesLastVisibleLine | NSStringDrawingUsesLineFragmentOrigin attributes:dic context:nil];

效果图

UIBezierPath路径

UIBezierPath 这个类由 UIKit 提供,是 Core Graphics 框架的关于 CGPathRefCGMutablePathRef 的一个 OC 封装,和之前的 CG 打头的路径使用作比较,UIBezierPath 是一个面向对象的类,对 OC 这个面向对象语言的使用者来说,显得更友好。使用 UIBezierPath 可以创建之前所有的图形,例如直线、矩形、圆弧、椭圆、圆或者不规则的多边形。

使用过程

  1. 重写视图的 drawRect: 方法
  2. 创建 UIBezierPath 的对象,根据需求附加相应的路径
  3. 设置 UIBezierPath 对象的一些绘图属性,如线宽、线段端点的风格、颜色等
  4. 调用 stroke(描边)或者 fill(填充)完成在图形上下文的呈现

创建 UIBerzierPath 对象

UIBerzierPath 类提供了非常多的便捷方式来创建路径对象,具体有下面:

  • bezierPath // UIBerzierPath对象
  • bezierPathWithRect: // 矩形路径对象
  • bezierPathWithOvalInRect: // 椭圆形路径对象
  • bezierPathWithRoundedRect:cornerRadius: // 带圆角的矩形路径对象
  • bezierPathWithRoundedRect:byRoundingCorners:cornerRadii: // 指定圆角的矩形路径对象
  • bezierPathWithArcCenter:radius:startAngle:endAngle:clockwise: // 圆弧对象
  • bezierPathWithCGPath: // 通过CGPath创建对象
  • bezierPathByReversingPath // 反转路径,及当前点和起点交换
  • init // 空路径对象

构建路径

  • moveToPoint: // 当前点移动到某个位置
  • addLineToPoint: // 附加一条直线
  • addArcWithCenter:radius:startAngle:endAngle:clockwise: // 附加圆弧
  • addCurveToPoint:controlPoint1:controlPoint2:
  • addQuadCurveToPoint:controlPoint: // 附加二次贝塞尔曲线
  • closePath // 关闭路径
  • removeAllPoints // 移除所有点,这会移除所有点子路径
  • appendPath: // 追加一条路径
  • CGPath // 转换为 CGPath 对象
  • currentPoint // 当前点

路径的一些属性设置

  • lineWidth // 线宽
  • lineCapStyle // 末端的风格。可参考上节CG部分
  • lineJoinStyle // 线和线之间连接处风格。可参考上节CG部分
  • miterLimit // 这个值有助避免线与线之间峰尖的出现
  • flatness

绘制路径

  • fill // 填充
  • fillWithBlendMode:alpha: // 使用指定的混合模式和透明度值填充
  • stroke // 描边
  • strokeWithBlendMode:alpha: // 使用指定的混合模式和透明度值描边

其他

  • containsPoint: // 判断点是否包含在路径中
  • empty // 判断当前路径是否是空的
  • bounds // 当前路径最大横跨的范围
  • applyTransform: // 通过Transforms对象将所有的点转换

示例

绘制路径

自定义弹窗样式

画板

示例传送门

CAShapeLayer

CAShapeLayer 是 CALayer的子类,可以使用 CALayer的所有属性,但是 CAShapeLayer 需要一个 path 才能发挥真正的作用,结合之前介绍的 UIBerzierPath ,我们可以将两者结合,完成非重写 drawRect: 方法即可完成推行的绘制。

一些属性

  • path // 必赋的属性,该路径决定了绘制图形的形状
  • fillColor // 填充色
  • fillRule // 填充规则
  • lineCap // 端点样式,参考上一小节
  • lineJoin // 链接处的样式,参考上一小节
  • lineWidth // 线宽度
  • strokeColor // 描边色
  • strokeStart // 决定路径中开始的位置
  • strokeEnd // 决定路径中结束的位置

示例

绘制路径

UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(20, 300)];
[path addLineToPoint:CGPointMake(100, 300)];
[path addArcWithCenter:CGPointMake(150, 300) radius:50 startAngle:M_PI endAngle:M_PI*3 clockwise:YES];
[path moveToPoint:CGPointMake(200, 300)];
[path addLineToPoint:CGPointMake(280, 300)];

创建 CAShapeLayer 对象

CAShapeLayer *layer = [CAShapeLayer layer];
layer.lineWidth = 3;        // 线条宽度
layer.strokeColor = [UIColor redColor].CGColor;     // 线条颜色
layer.strokeStart = 0.0f;         // 开始
layer.strokeEnd = 1.0f;           // 结束
layer.lineJoin = kCALineJoinRound;    // 连接处样式
layer.lineCap = kCALineCapRound;      // 起点终点样式
layer.fillColor = [UIColor clearColor].CGColor;    // 填充色,默认为黑色
[self.view.layer addSublayer:layer];     // 加到view的layer上
layer.path = path.CGPath;      // 贝塞尔路径赋值

在此基础上添加动画效果

CABasicAnimation *ani = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
ani.duration = 3;
ani.repeatCount = HUGE_VALF;
ani.fromValue = [NSNumber numberWithFloat:0.0];
ani.toValue = [NSNumber numberWithFloat:1.0];
[layer addAnimation:ani forKey:@""];

效果图

CA 和 CG 的比较

CGContentRef 属于 Core Graphic 框架,调用函数的形式完成图形的绘制,并且需要重写视图的 drawRect: 方法,另外,这种方式使用用 CPU ,消耗性能大,很可能阻塞主线程。

CAShapeLayer 属于 Core Animation 框架,通过 GPU 来渲染图形,节省性能,由于图形、动画直接交给了手机 GPU,因此不会阻塞主线程。由于采用 GPU 来渲染,虽然不会阻塞主线程,但是当 GPU 的渲染跟不上用户的操作时,会发生图形虚影的情况。


2018-10-30 新编