一:Quartz 2D 节本介绍

Quartz 2D可以绘制的内容:

  • 绘制几何图形:线条、虚线、三角形、矩形、圆角矩形、圆、椭圆、弧、扇形等
  • 绘制文字
  • 绘制图片

在实际开发中常用于:

  • 自定义UI控件(常用)
  • 截图、裁剪图片
  • 报表:折线图、饼状图、柱状图、股票图

常用的图形上下文CGContextRef (画板)

  • Layer Graphics Contenxt (图层上下文:在UIView中drawRect方法中绘制)
  • Bitmap Graphics Contenxt(图片上下文)
  • PDF Graphics Contenxt (PDF上下文)

特别注意:绘图不是讲内容绘制到UIView上,而是绘制到UIView的根图层上CALayer


二:基本知识介绍

在图层中绘图的基本步骤

  1. 获取图层上下文 UIGraphicsGetCurrentContext()
  2. 绘制路径 UIBezierPath贝塞尔路径
  3. 添加路径到上下文中 addPath
  4. 渲染路径 stroke、fill(将上下文中内容显示到View上,即渲染到view的根图层layer上)

注意:图层上下文必须在drawRect方法中绘制,因为只有该方法才能获取UIView关联的图层上下文而且每次获取的上下文都是空的上下文

-(void)drawRect:(CGRect)rect 方法

UIView中drawRect:方法是在viewWillAppear:视图即将显示viewWillAppear之后调用,rect参数就是该view的bounds属性

  • viewDidLoad
  • viewWillAppear
  • drawRect
  • viewDidAppear

三:基本代码

绘图的标准方式4步曲:

- (void)drawRect:(CGRect)rect {
    // 1.获取上下文(画板)
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetLineWidth(context, 5);

    // 2.绘制路径
    UIBezierPath *bezierPath = [UIBezierPath bezierPath];
    [bezierPath moveToPoint:CGPointMake(50, 280)];
    [bezierPath addLineToPoint:CGPointMake(250, 50)];
    [[UIColor redColor] set];  // 设置颜色

    // 3.添加路径
    CGContextAddPath(context, bezierPath.CGPath);

    // 4.渲染路径
    CGContextStrokePath(context);
}

绘图的简写方式:渲染方法stroke/fill中会自动获取上下文并将路径添加到上线文中,然后渲染; 上面四步可以缩减成2步

  1. 创建路径
  2. 渲染路径
- (void)drawRect:(CGRect)rect {
    // 创建路径
    UIBezierPath *bezierPath = [UIBezierPath bezierPath];
    [bezierPath moveToPoint:CGPointMake(50, 280)];
    [bezierPath addLineToPoint:CGPointMake(250, 50)];

    // 渲染路径
    [bezierPath stroke];
}

fill方法会自动关闭路径的closePath,如果想在其他方法中调用drawRect方法可以调用setNeedsDisplay重绘方法,让系统来调用drawRect方法,不能直接调用drawRect方法


画文本:直接使用NSString中对应的方法,drawAtPoint:withAttributes:或者drawAtRect:withAttributes:方法进行绘制


画图片:直接使用UIImage中的方法:

  • drawAtPoint : 绘制原始图片大小
  • drawInRect:绘制的图片填充整个rect区域
  • drawAsPatternInRect:平铺图片

上下文状态栈:
上下文中有三个重要的属性:

  • 路径数组(用于保存上下文中添加的路径,每次使用CGContextAddPath方法时就将该路径添加到该数组中)
  • 上下文绘图状态GState(上下文的一些属性如线宽、笔触颜色等)
  • 上下文状态栈(使用栈的方式管理GState)

上下文状态栈通过CGContextSaveGState(context)方法来将上下文中的绘图状态GState(是复制上下文绘图状态,上下文中的状态保持不变) 入栈到 上下文状态栈中, 通过 CGContextRestoreGState(context) 方法 将栈顶的GState取出,并赋值给上下文的状态, 注意:上下文中有一份GState,而上下文栈中可能有多个GState,当保存时是不会影响上下文中的GState的,当出栈时会将出栈的GState直接赋值给上下文中的GState,也就是说上下文中的GState将会被覆盖掉,将路径渲染到UIView上是使用上下文中的绘图状态,通过保存状态和回复状态可以完成后面的路径使用前面的绘图状态来绘图(达到后面的路径不用再重新设置复用的状态)

Quartz 2D绘图_绘图

示例代码:

#import "DrawView.h"

@implementation DrawView
- (void)drawRect:(CGRect)rect {
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetLineWidth(context, 10);
    [[UIColor redColor] set];

    // 入栈
    CGContextSaveGState(context);
    UIBezierPath *bezierPath = [UIBezierPath bezierPath];
    [bezierPath moveToPoint:CGPointMake(250, 150)];
    [bezierPath addLineToPoint:CGPointMake(100, 150)];
    CGContextAddPath(context, bezierPath.CGPath);
    CGContextStrokePath(context);


    CGContextSetLineWidth(context, 5);
    [[UIColor blackColor] set];
    UIBezierPath *bezierPath2 = [UIBezierPath bezierPath];
    [bezierPath2 moveToPoint:CGPointMake(200, 100)];
    [bezierPath2 addLineToPoint:CGPointMake(200, 250)];
    CGContextAddPath(context, bezierPath2.CGPath);
    CGContextStrokePath(context);

    // 出栈并覆盖上下文中的状态 第三个路径没有设置GState,但是是使用第一个GState
    CGContextRestoreGState(context);
    UIBezierPath *bezierPath3 = [UIBezierPath bezierPath];
    [bezierPath3 moveToPoint:CGPointMake(230, 100)];
    [bezierPath3 addLineToPoint:CGPointMake(230, 250)];
    CGContextAddPath(context, bezierPath3.CGPath);
    CGContextStrokePath(context);
}
@end

Quartz 2D绘图_绘图_02

在添加路径之前可以对路径进行形变操作:平移、旋转、缩放

CGContextTranslateCTM(context, 100, -100);
CGContextRotateCTM(context, M_PI_4);
CGContextScaleCTM(context, 1, 2);
CGContextAddPath(context, bezierPath.CGPath);

位图上下文


位图上下文必须手动去开启,开启多大的上下文,就生成多大的图片, 往UIView上绘图只能在drawRect方法中绘图,绘制图片可以在任意地方进行绘制(因为图片上下文是自己手动开启的),手动开启还要手动关闭

示例1:图片水印:在图片上加的防止他人盗用的半透明的logo、文字、图标,来告诉图片的来源,版权问题,或者来广告

@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];

    // 1. 加载图片
    UIImage *targetImage = [UIImage imageNamed:@"girl"];

    // 2. 开启图片上下文(指定上下文的尺寸)
    // BOOL opaque: 是否不透明,当绘制的图片小于图片上下文的尺寸时,图片四周的填充颜色,NO:透明,YES:为不透明,即黑色
    UIGraphicsBeginImageContextWithOptions(targetImage.size, NO, 0);

    // 3. 把目标图片绘制到上下文中
    [targetImage drawAtPoint:CGPointZero];

    // 4. 将水印文字绘制到上下文中
    NSString *text = @"图片来自某网站";

    [text drawAtPoint:CGPointMake(10, 20) withAttributes:@{NSFontAttributeName : [UIFont systemFontOfSize:40]}];

    // 5. 获取图片
    UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();

    // 6. 关闭图片上下文
    UIGraphicsEndImageContext();

    self.imageView.image = newImage;
}
@end

示例2:绘制圆形图片:

+ (UIImage *)circleImageNamed:(NSString *)name {
    // 0. 加载图片
    UIImage *image = [UIImage imageNamed:name];

    // 1. 开启图片上下文
    UIGraphicsBeginImageContextWithOptions(image.size, NO, 0);

    // 2. 添加圆形剪裁路径
    UIBezierPath *circlePath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, image.size.width, image.size.height)];
    [circlePath addClip];

    // 3. 绘制图片
    [image drawAtPoint:CGPointZero];

    // 4. 获取图片
    UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();

    // 5. 结束图片上下文
    UIGraphicsEndImageContext();

    return newImage;
}

+ (UIImage *)circleImageWithBorderWidth:(CGFloat)boardWidth borderColor:(UIColor *)borderColor image:(UIImage *)image {

    // 1.开启图片上下文(图片上下文的尺寸要加上2被的边框宽度,目的是保证原图片不缩放)
    CGSize contextSize = CGSizeMake(image.size.width + 2 * boardWidth, image.size.height + 2 * boardWidth);
    UIGraphicsBeginImageContextWithOptions(contextSize, NO, 0);

    // 2.绘制一个填充圆,作为圆形图片的边框
    UIBezierPath *bigCirclePath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, contextSize.width, contextSize.height)];
    [borderColor set];
    [bigCirclePath fill];

    // 3.添加圆形剪裁
    UIBezierPath *clipPath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(boardWidth, boardWidth, image.size.width, image.size.height)];
    [clipPath addClip];

    // 4.绘制图片
    [image drawAtPoint:CGPointMake(boardWidth, boardWidth)];

    // 5.获取图片
    UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();

    // 6. 关闭图片上下文
    UIGraphicsEndImageContext();

    return newImage;
}

对UIView进行截图:将UIView保存成一张图片,UIView可以是UIImageView,也可以是普通的View,甚至是视图控制器的view

+ (UIImage *)imageWithView:(UIView *)view {
    // 1. 开启图片上下文
   UIGraphicsBeginImageContextWithOptions(view.bounds.size, NO, 0);

    // 2. 将view的图层渲染到图层上下文中
    CGContextRef context = UIGraphicsGetCurrentContext();
    [view.layer renderInContext:context];

    // 3. 获取图片
    UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();

    // 4. 关闭图片上下文
    UIGraphicsEndImageContext();

    //NSData *imageData = UIImagePNGRepresentation(newImage);
    //[imageData writeToFile:@"/Users/macmini/Documents/newImage.png" atomically:YES];

    return newImage;
}

CGContextClearRect(ctx, rect); // 擦除上下文当中的指定的区域

如果想将一个事件交给父视图处理,只需要设置当前视图userInteractionEnable = NO即可,不让当前视图接收事件

重绘:setNeedsDisplay(打一个标记,当视图刷新帧时去调用drawRect方法)

对于drawRect方法每次获取上下文UIGraphicsGetCurrentContext()时都是空的上下文,如果想绘制一个完整的图案(由多个路径组成)要么在同一个上下文中一口气绘制多条路径,要么是将多条路径分别保存到数组中,然后在同一个上下文中遍历数组中的所有路径进行绘制,不管怎样都必须在同一个上下文中进行绘制,因为在UIView中绘制必须在drawRect绘制,该方法每次获取的上下文都是空的,并不是上一次绘制的上下文!


CALayer:图层
在iOS中能够看的见的控件一般是UIView类型,如UIButton、UITextField、UIImageView, UIView之所以能够看的见是因为UIView内部有一个图层CALayer, 在创建UIView对象时,UIView会自动创建一个图层对象layer,当UIView需要显示到屏幕上时,系统调用drawRect方法进行绘图,并且将所有路径绘制到图层上,绘制完毕后,系统会将图层拷贝到屏幕上,于是就完成了UIView的显示, UIView中系统自动创建的图层被称为根图层root layer,自己手动创建的被称为非根图层

UIView本身不具备显示的功能,是它内部的图层具备显示的能力!

通过操作CALayer对象,可以很方便的调整UIView的一些外观属性,如 阴影shadow、边框border、圆角cornerRadius、maskToBounds、形变transform(只能对非根图层进行形变,对根层动画使用CoreAnimation框架)、位置position(view.center = view.layer.position;)、锚点anchorPoint(无论是旋转还是缩放都是围绕这锚点进行的)

CALayer具备展示能力,而UIView因为内部有一个CALayer所以才具备展示能力
CALayer是属于QuartzCore框架中,UIView是属于UIKit框架中
CALayer继承自NSObject,不具备事件处理能力,UIView继承自UIResponder,具备事件处理能力

- (void)viewDidLoad {
    [super viewDidLoad];

    // 具备显示但不具备事件交互
    CALayer *layer = [CALayer layer];
    layer.frame = CGRectMake(0, 0, 100, 100);
    layer.backgroundColor = [UIColor redColor].CGColor;
    layer.contents = (id)[UIImage imageNamed:@"girl"].CGImage;

    [self.view.layer addSublayer:layer];
}

锚点:anchorPoint,取值范围为0~1, 默认是(0.5, 0.5)
Core Animation核心动画框架是直接作用在CALayer上的,并非UIView上,核心动画是在后台线程中执行的,不会阻塞主线程

UIView和核心动画的区别:
1. 核心动画是作用在图层CALayer上,而不是作用在UIView上,发生动画时只是改变了layer的位置,并没有改变UIView的frame, 如果把UIView比作躯体,layer比作灵魂,使用核心动画相当于灵魂出窍
2. UIView动画会改变UIView的frame的。灵魂和身体是同步的

  • 因为核心动画是作用在图层上的,而图图层是不支持事件的,所以当不需要事件处理的时候可以使用核心动画,而UIView是支持事件处理的
  • 核心动画可以做更加复杂的动画,一般使用UIView做一些简单的动画