iOS精品文章-转场&动画iOS 进阶iOS在路上

iOS 绘图使用总结

2017-09-06  本文已影响207人  TommyYaphetS

上一篇介绍了动画相关的 api,本篇涉及的就是如何把图形画出来了,之前也做过不少相关的画图工作,但都比较简单少量,也没有刻意的去比较或者直接就是接入了第三方库去实现...工作需要实现大量的 K线绘制以及各种状态变更,因此总结一下方便查阅,提升工作效率.

image.png
iOS提供了两套绘图的框架,UIBezierPathCore Graphics.

Core Graphics

在绘图之前,我们先需要搞清楚下面几个概念:

  1. CGContextRef
    图形上下文,可以理解为画布/画板,我们要画画首先需要一个载体吧,比如电脑绘图我们会创建一个空白画布,生活中画画我们会先准备好画板,否则是无法进行绘制的.
    通常我们通过以下2种方法来获取这个context:
  1. 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);
}

大家可以试试,如果把上面代码标记了"看这里"的两句删掉,会是什么结果?

631504601986_.pic.jpg
加上这两句才是正确的:
621504601986_.pic.jpg
可以看到,CGContextSaveGState存储下来了当前红色和宽度为2的线条状态,然后切换颜色到黄色和5宽度的状态画线(你也可以画圈/画矩形, LZ 我偷懒),然后在CGContextRestoreGState恢复到了红色和默认的线条状态进行画,这个就是存储当前绘制状态的意思.
总结:所以这2个 api 可以理解为,保存当前的上下文拷贝,变化一个样子出去玩耍一下,结束之后又通过之前保存的拷贝复位.


3.UIGraphicsPushContext/UIGraphicsPopContext
UIGraphicsPushContext用于完全更改图形上下文和UIGraphicsPopContext恢复之前的图形上下文.
UI开头的 api 也可以看出,它的使用与 UIKit 绘图相关联.

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(*):辅助点 ---------------------------------------------

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继承自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

上一篇下一篇

猜你喜欢

热点阅读