iOS 绘图使用总结
上一篇介绍了动画相关的 api,本篇涉及的就是如何把图形画出来了,之前也做过不少相关的画图工作,但都比较简单少量,也没有刻意的去比较或者直接就是接入了第三方库去实现...工作需要实现大量的 K线绘制以及各种状态变更,因此总结一下方便查阅,提升工作效率.
image.pngiOS提供了两套绘图的框架,
UIBezierPath
和 Core Graphics
.
-
UIBezierPath
是UIKit中的一个关于图形绘制的类,其实是对Core Graphics框架关于path(CGPathRef数据类型的封装)的进一步封装,语法就是 OC 范. -
Core Graphics
也被称作QuartZ或QuartZ 2D,更接近底层,功能更强大, 提供的都是C语言的函数接口.
Core Graphics
在绘图之前,我们先需要搞清楚下面几个概念:
-
CGContextRef
图形上下文
,可以理解为画布/画板,我们要画画首先需要一个载体吧,比如电脑绘图我们会创建一个空白画布,生活中画画我们会先准备好画板,否则是无法进行绘制的.
通常我们通过以下2种方法来获取这个context
:
-
drawRect:(CGRect)rect:
重写UIView的drawRect:
方法,调用UIGraphicsGetCurrentContext()
即可获得图形上下文,在其他地方调用获得的图形上下文总是nil
,因为在drawRect之前,系统会往栈里面压入一个valid的CGContextRef.
ps(*):drawRect:
方法在view第一次显示的时候会自动调用(如果不设置 frame, 那么默认的第一次也不会调用),当你手动重画这个View时,不能手动显示调用,必须通过调用setNeedsDisplay
或者setNeedsDisplayInRect
,让系统自动调该方法.
ps:drawLayer:(CALayer*)layer inContext:(CGContextRef)ctx
要注意图层代理对象的设定,具体可以参考iOS 绘图教程的第三种绘图形式. -
UIGraphicsBeginImageContextWithOptions(<#CGSize size#>, <#BOOL opaque#>, <#CGFloat scale#>)
-- CGSize size:指定将来创建出来的view的大小;
-- BOOL opaque:设置透明YES代表透明,NO代表不透明;
-- CGFloat scale:代表缩放,0代表不缩放.
如果想在drawRect之外获得context怎么办?那只能自己创建位图上下文了.
调用UIGraphicsBeginImageContextWithOptions
函数就可获得用来处理图片
的图形上下文,利用该上下文,你就可以在其上进行绘图,并生成图片.调用UIGraphicsGetImageFromCurrentImageContext
函数可从当前上下文中获取一个UIImage对象.记住在你所有的绘图操作后别忘了调用UIGraphicsEndImageContext
函数关闭图形上下文(类似于数据库的打开与关闭)。UIGraphicsBeginImageContextWithOptions
和UIGraphicsEndImageContext
成对出现,类似的 api 还有很多,也可以嵌套.
-
CGContextSaveGState/CGContextRestoreGState
CGContextSaveGState
用于记录和CGContextRestoreGState
用于恢复已存储的绘图上下文.
获取图形上下文之后,这时你开始画图的下一步准备工作,比如定画笔的颜色,文本的颜色,字体的大小/型号,然后开始作画.当你画到一半的时候,你需要更改这些配置,也就是用特定的颜色/字体等绘制一个特殊的图形,完成之后又回到最初的图形.
是不是有点绕...
@举个栗子:
我要画三根线,先画一根宽度为2的红线,然后画一根宽度为5的黄线,最后再画一根宽度为2的红线.
- (void)drawRect:(CGRect)rect {
CGContextRef ctx = UIGraphicsGetCurrentContext();
//第一条线
CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor);
CGContextSetLineWidth(ctx, 2.0f);
CGContextMoveToPoint(ctx, 10, 30);
CGContextAddLineToPoint(ctx, 10, 100);
CGContextStrokePath(ctx);
//第二条线
CGContextSaveGState(ctx); // ----- 看这里
CGContextSetStrokeColorWithColor(ctx, [UIColor yellowColor].CGColor);
CGContextSetLineWidth(ctx, 5.0f);
CGContextMoveToPoint(ctx, 50, 30);
CGContextAddLineToPoint(ctx, 50, 100);
CGContextStrokePath(ctx);
CGContextRestoreGState(ctx); // ---- 看这里
//第三条线
CGContextMoveToPoint(ctx, 110, 30);
CGContextAddLineToPoint(ctx, 110, 100);
CGContextStrokePath(ctx);
}
大家可以试试,如果把上面代码标记了"看这里"的两句删掉,会是什么结果?
加上这两句才是正确的:
621504601986_.pic.jpg
可以看到,
CGContextSaveGState
存储下来了当前红色和宽度为2的线条状态,然后切换颜色到黄色和5宽度的状态画线(你也可以画圈/画矩形, LZ 我偷懒),然后在CGContextRestoreGState
恢复到了红色和默认的线条状态进行画,这个就是存储当前绘制状态的意思.总结:所以这2个 api 可以理解为,保存当前的上下文拷贝,变化一个样子出去玩耍一下,结束之后又通过之前保存的拷贝复位.
3.UIGraphicsPushContext/UIGraphicsPopContext
UIGraphicsPushContext
用于完全更改图形上下文和UIGraphicsPopContext
恢复之前的图形上下文.从
UI
开头的 api 也可以看出,它的使用与 UIKit 绘图相关联.
- 假设你正在当前图形上下文中绘制 A,这时想要在位图上下文中绘制完全不同的B并且使用UIKit来进行任意绘图,这时你需要切换到一个全新的绘图上下文中并且想要保存当前的图形上下文,包括所有已经绘制的内容,那么就需要调用
UIGraphicsPushContext
来将图形上下文入栈. - 等绘制完B后,再调用
UIGraphicsPopContext
将之前的图形上下文出栈.
ps(*):这种情况只会在要使用UIKit在新的位图上下文中绘图时才会发生,只要你使用的是Core Graphics
绘制,就不需要去执行上下文入栈和出栈,Core Graphics
函数将上下文视作参数。引用iOS --- CoreGraphics中三种绘图context切换方式的区别总结的一句话:绘图context切换的关键是:要看切换新的绘图context后,是要继续使用CoreGraphics绘制图形,还是要使用UIKit。
4.常用的一些 API
- (void)test {
CGContextRef ctx = UIGraphicsGetCurrentContext();
// 设置画线的起点 为 (50,30)
CGContextMoveToPoint(ctx, 50, 30);
// 绘制直线连线, 从起点延伸到 (10,100)
CGContextAddLineToPoint(ctx, 10, 100);
// 绘制矩形 从(50,30)开始, 宽度高度均为50
CGContextAddRect(ctx, CGRectMake(50, 30, 50, 50));
// 绘制/渲染图形
CGContextStrokePath(ctx); // stroke 是描线 ,而 fill 是填充,单纯线条下 fill 不会工作
CGContextFillPath(ctx); // 填充
// 设置线条的宽度
CGContextSetLineWidth(ctx, 5.0f);
// 设置线条颜色 -- 注意与 fill 的区别
CGContextSetStrokeColorWithColor(ctx, [UIColor yellowColor].CGColor);
[[UIColor yellowColor] setStroke];
// 设置填充颜色
CGContextSetFillColorWithColor(ctx, [UIColor yellowColor].CGColor);
[[UIColor yellowColor] setFill];
/** 线条交汇处样式
kCGLineJoinMiter——尖角
kCGLineJoinBevel——平角
kCGLineJoinRound——圆形
**/
CGContextSetLineJoin(ctx, kCGLineJoinRound);
// 绘制虚线,第二个参数为初始跳过几个点开始绘制,第三个参数为一个CGFloat数组,指定你绘制的样式,绘几个点跳几个点(下面为绘10个点,跳过5个,最后一个参数是上个参数数组元素的个数。
CGContextSetLineDash(ctx, 0, (CGFloat[]){10, 5}, 2);
// 默认系统会绘制填充这个矩形内部的最大椭圆,若矩形为正方形,则为圆
CGContextAddEllipseInRect(ctx, CGRectMake(40, 180, 240, 120));
// 画切线弧,是说从 起点(50,30)到(100,80)画一条线,然后再从(100,80)到(130,150)画一条线,从这两条线(无限延伸的)和 半径 50 可以确定一条弧,
CGContextAddArcToPoint(ctx,100,80,130,150,50);
/** 绘制圆弧,饼状图() -- 画圆的时候可通过 线条宽度 来实现中间空心圆效果
void CGContextAddArc (
CGContextRef c,
CGFloat x, // 圆心点坐标的x和y
CGFloat y,
CGFloat radius, // 半径
CGFloat startAngle, // 绘制起始点的弧度值,一般在IOS绘图里都使用弧度这个概念 #define RADIANS(x) ((x)*(M_PI)/180) // 角度转弧度
CGFloat endAngle, // 绘制终点的弧度值
int clockwise // 1为顺时针,0为逆时针。
);
**/
CGContextAddArc(ctx, 100, 100, 10, ((60.0)*(M_PI)/180), ((270.0)*(M_PI)/180), 0);
/* 裁剪当前路径 -- 参照: http://blog.sina.com.cn/s/blog_b876b8ab0102v6gb.html */
// 使用非零绕数规则。
CGContextClip(ctx);
// 使用奇偶规则。
CGContextEOClip(ctx);
//CGContextClipToRect
//CGContextClipToRects
//CGContextGetClipBoundingBox
//CGContextClipToMask
/* 裁剪当前路径 */
/* 构造路径 -- 类似于后面的要讲的 UIBezierPath */
// 创建一个 path 对象
CGMutablePathRef path = CGPathCreateMutable();
// 将路径加入到图形上下文中
CGContextAddPath(ctx, path);
// 制作具体路线 -- 上面两步其实可以省略
CGPathMoveToPoint(path, NULL, 10, 10);
CGPathAddLineToPoint(path, NULL, 100, 100);
CGPathMoveToPoint(path, NULL, 20, 20);
CGPathAddLineToPoint(path, NULL, 200, 200);
// 渲染/绘制,并且可以设置绘制的类型
/*CGPathDrawingMode是填充方式,枚举类型
kCGPathFill:只有填充(非零缠绕数填充),不绘制边框
kCGPathEOFill:奇偶规则填充(多条路径交叉时,奇数交叉填充,偶交叉不填充)
kCGPathStroke:只有边框
kCGPathFillStroke:既有边框又有填充
kCGPathEOFillStroke:奇偶填充并绘制边框
*/
CGContextDrawPath(ctx, kCGPathFillStroke); // 等价于 CGContextStrokePath + CGContextFillPath
// 释放资源 -- ARC 并不能处理这类的资源管理,必须手动释放
CGPathRelease(path);
/* 构造路径 -- 类似于后面的要讲的 UIBezierPath */
// 封闭路径,不需要一定设置路径的终点,可以主动关闭
/*
1.起始与终点重合的直线、弧和曲线并不自动闭合路径,我们必须调用CGContextClosePath来闭合路径。
2.Quartz的一些函数将路径的子路径看成是闭合的。这些函数显示地添加一条直线来闭合 子路径,如同调用了CGContextClosePath函数。
3.在闭合一条子路径后,如果程序再添加直线、弧或曲线到路径,Quartz将在闭合的子路径的起点开始创建一个子路径。
*/
CGContextClosePath(ctx);
// 明确闭合路径
CGPathCloseSubpath(path);
// 设置阴影 -- 参数依此是:图形上下文,偏移量(CGSize),模糊值,阴影颜色
CGContextSetShadowWithColor(ctx, CGSizeMake(10, 10), 20.0f, [[UIColor grayColor] CGColor]);
/* 绘制渐变色效果 -- 亦可以 CAGradientLayer 链接:https://zsisme.gitbooks.io/ios-/content/chapter6/cagradientLayer.html*/
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
// 创建一个渐变的色值 1:颜色空间 2:渐变的色数组 3:位置数组,如果为NULL,则为平均渐变,否则颜色和位置一一对应 4:位置的个数
CGGradientRef gradient = CGGradientCreateWithColorComponents(colorSpace, (CGFloat[]){
// 如果想知道一个颜色比如[UIColor purpleColor]具体构成 -> CGColorGetComponents([UIColor purpleColor].CGColor); 返回一个数组,包括R,G,B以及alpha的值
0.3, 0.2, 0.2, 1.0,
0.1, 0.5, 0.2, 1.0,
0.6, 0.2, 0.7, 1.0
}, (CGFloat[]){
0.0, 0.5, 1.0
}, 3);
// 绘制渐变, 颜色的0对应start点,颜色的1对应end点,第四个参数是定义渐变是否超越起始点和终止点
CGContextDrawLinearGradient(ctx, gradient, CGPointMake(100, 300), CGPointMake(220, 480), 0);
/* 辐射渐变 有兴趣的可以去玩一哈...
void CGContextDrawRadialGradient(
CGContextRef context,
CGGradientRef gradient, //先创造一个CGGradientRef,颜色是白,黑,location分别是0,1
CGPoint startCenter, // 白色的起点(中心圆点)
CGFloat startRadius, // 起点的半径,这个值多大,中心就是多大一块纯色的白圈
CGPoint endCenter, // 白色的终点(可以和起点一样,不一样的话就像探照灯一样从起点投影到这个终点,按照你的意图应该和startCenter一样
CGFloat endRadius, //终点的半径, 按照你的意图应该就是从中心到周边的长
CGGradientDrawingOptions options //应该是 kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation
);
*/
// 释放资源
CGGradientRelease(gradient);
CGColorSpaceRelease(colorSpace);
/* 绘制渐变色效果 */
// 绘制二次贝塞尔曲线
CGContextMoveToPoint(ctx, 20, 100);//移动到起始位置
/*
c:图形上下文
cpx:控制点x坐标
cpy:控制点y坐标
x:结束点x坐标
y:结束点y坐标
*/
CGContextAddQuadCurveToPoint(ctx, 160, 0, 300, 100);
// 绘制三次贝塞尔曲线
CGContextMoveToPoint(ctx, 20, 500);
/*
c:图形上下文
cp1x:第一个控制点x坐标
cp1y:第一个控制点y坐标
cp2x:第二个控制点x坐标
cp2y:第二个控制点y坐标
x:结束点x坐标
y:结束点y坐标
*/
CGContextAddCurveToPoint(ctx, 80, 300, 240, 500, 300, 300);
// 检测当前的路径是否包含指定的点
bool isExit = CGContextPathContainsPoint(ctx, CGPointMake(100, 100), kCGPathFill);
// 题外话..
[@"绘制字符串" drawAtPoint:CGPointMake(0, 0) withAttributes:@{NSFontAttributeName : [UIFont systemFontOfSize:[UIFont systemFontSize]]}];
}
ps(*):辅助点 ---------------------------------------------
- 因为 ARC 并不能管理 CG 这一套 api 的内存释放,所以使用含有“Create”或“Copy”的函数创建的对象,使用完后必须释放,否则将导致内存泄露.
-
附一个弧度图:#define RADIANS(x) ((x)*(M_PI)/180) // 角度转弧度
-
even-odd
和non-zero
规则参照: http://www.jianshu.com/p/d4b8b5d931df 和 http://blog.csdn.net/jeffasd/article/details/52062375 - 贝塞尔曲线扫盲:很生动形象的解释.
-
贝塞尔曲线配置:非常非常的实用,很棒的一个工具!还涉及到了之前动画的
CAMediaTimingFunction
测试. -
附一个贝塞尔图:
image.png
UIBezierPath+CAShapeLayer
前面介绍过,UIBezierPath
是对CGPathRef
数据类型的封装.在 drawRect :
中,无须获取图形上下文,直接用UIBezierPath
创建路径来绘制,比如:
- (void)drawRect:(CGRect)rect{
// 颜色
[[UIColor orangeColor] set];
UIBezierPath* path = [UIBezierPath bezierPath];
path.lineWidth = 5.f;
// 起点
[path moveToPoint:CGPointMake(10, 100)];
// 绘制线条
[path addLineToPoint:CGPointMake(100, 20)];
// 绘制渲染
[path stroke];
}
不过,一般,UIBezierPath
配合CAShapeLayer
一起使用.UIBezierPath给CAShapeLayer提供路径,CAShapeLayer在提供的路径中进行渲染,绘制出了Shape.
使用CAShapeLayer
有以下一些优点:
- 渲染快速.CAShapeLayer使用了硬件加速,绘制同一图形会比用Core Graphics快很多。
- 高效使用内存.一个CAShapeLayer不需要像普通CALayer一样创建一个寄宿图形,所以无论有多大,都不会占用太多的内存.
- 不会被图层边界剪裁掉.一个CAShapeLayer可以在边界之外绘制。你的图层路径不会像在使用Core Graphics的普通CALayer一样被剪裁掉.
- 不会出现像素化。当你给CAShapeLayer做3D变换时,它不像一个有寄宿图的普通图层一样变得像素化.
CAShapeLayer
继承自CALayer
,常用属性:
path:CGPathRef类型,配合 UIBezierPath 的 path
fillColor:填充path的颜色,或无填充。默认为不透明黑色。动画的。
strokeColor:绘制的线条的颜色。
fillRule:填充path的规则。非零和偶奇。同CGPathDrawingMode
lineCap:线端点类型,同CGContextSetLineJoin
lineDashPattern:线性模版,这是一个NSNumber的数组,索引从1开始记,奇数位数值表示实线长度,偶数位数值表示空白长度。
lineDashPhase:线型模版的起始位置。
lineJoin:线拐点类型。kCALineJoinMiter-尖的,kCALineJoinRound-圆弧,kCALineJoinBevel-梯形
lineWidth:线宽
miterLimit:最大斜接长度。斜接长度指的是在两条线交汇处和外交之间的距离。只有lineJoin属性为kCALineJoinMiter时miterLimit才有效。边角的角度越小,斜接长度就会越大。为了避免斜接长度过长,我们可以使用miterLimit属性。如果斜接长度超过miterLimit的值,边角会以lineJoin的“bevel”即kCALineJoinBevel类型来显示。
strokeStart和strokeEnd:部分绘线,都是0.0~1.0的取值范围.经常被用来制作动画效果。
再来看看UIBezierPath
:是不是跟之前的 CG 很像
// 创建基本路径
+ (instancetype)bezierPath;
// 创建矩形路径
+ (instancetype)bezierPathWithRect:(CGRect)rect;
// 创建椭圆路径
+ (instancetype)bezierPathWithOvalInRect:(CGRect)rect;
// 创建圆角矩形
+ (instancetype)bezierPathWithRoundedRect:(CGRect)rect cornerRadius:(CGFloat)cornerRadius; // rounds all corners with the same horizontal and vertical radius
// 创建指定位置圆角的矩形路径
+ (instancetype)bezierPathWithRoundedRect:(CGRect)rect byRoundingCorners:(UIRectCorner)corners cornerRadii:(CGSize)cornerRadii;
// 创建弧线路径
+ (instancetype)bezierPathWithArcCenter:(CGPoint)center radius:(CGFloat)radius startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle clockwise:(BOOL)clockwise;
// 通过CGPath创建
+ (instancetype)bezierPathWithCGPath:(CGPathRef)CGPath;
// 与之对应的CGPath,可赋值给 CAShapeLayer 的 path
@property(nonatomic) CGPathRef CGPath;
// 是否为空
@property(readonly,getter=isEmpty) BOOL empty;
// 整个路径相对于原点的位置及宽高
@property(nonatomic,readonly) CGRect bounds;
// 当前画笔位置
@property(nonatomic,readonly) CGPoint currentPoint;
// 线宽
@property(nonatomic) CGFloat lineWidth;
// 终点类型,同 CAShapeLayer
@property(nonatomic) CGLineCap lineCapStyle;
// 线条拐点的类型, 同 CAShapeLayer
@property(nonatomic) CGLineJoin lineJoinStyle;
// 两条线交汇处内角和外角之间的最大距离, 同 CAShapeLayer
@property(nonatomic) CGFloat miterLimit;
// 绘线的精细程度,默认为0.6,数值越大,需要处理的时间越长
@property(nonatomic) CGFloat flatness;
// 决定使用even-odd(奇偶)或者non-zero(非零环绕)规则
@property(nonatomic) BOOL usesEvenOddFillRule;
// 反方向绘制path
- (UIBezierPath *)bezierPathByReversingPath;
// 设置画笔起始点
- (void)moveToPoint:(CGPoint)point;
// 从当前点到指定点绘制直线
- (void)addLineToPoint:(CGPoint)point;
// 添加弧线, 同 CG
- (void)addArcWithCenter:(CGPoint)center radius:(CGFloat)radius startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle clockwise:(BOOL)clockwise NS_AVAILABLE_IOS(4_0);
// 添加二次贝塞尔曲线, 同 CG
- (void)addQuadCurveToPoint:(CGPoint)endPoint controlPoint:(CGPoint)controlPoint;
// 添加三次贝塞尔曲线, 同 CG
- (void)addCurveToPoint:(CGPoint)endPoint controlPoint1:(CGPoint)controlPoint1 controlPoint2:(CGPoint)controlPoint2;
// 闭合路径,得到封闭图形
- (void)closePath;
// 移除所有的点,删除所有的subPath
- (void)removeAllPoints;
// 将bezierPath添加到当前path
- (void)appendPath:(UIBezierPath *)bezierPath;
// 填充
- (void)fill;
// 绘制/渲染,描绘线条
- (void)stroke;
// 在这以后的图形绘制超出当前路径范围则不可见
- (void)addClip;
参考链接
iOS 绘图教程
iOS核心动画教程之CAShapeLayer
CoreGraphics之CGContextSaveGState与UIGraphicsPushContext
iOS --- CoreGraphics中三种绘图context切换方式的区别
iOS开发系列--打造自己的“美图秀秀”
关于CAShapeLayer
iOS绘图 - UIBezierPath水波
动画黄金搭档:CADisplayLink & CAShapeLayer