UIKit与Quartz 2D绘图技术概要(swift)

在iOS中绘图技术主要有UIKit, Quartz 2D,Core Animation和OpenGL ES. 其中Core Animation提供动画实现. OpenGL ES是OpenGL针对嵌入式设备的简化版本, 可以绘制高性能的2D和3D图形. 这里重点介绍UIKit和Quartz 2D.

  1. UIKit: 高级别的图形接口, 提供了基于Objective-C和Swift的API. 能够访问绘图,动画,字体,图片等内容.
  2. Quartz 2D: iOS和OSX环境下的2D绘图引擎. 包括基于路径的绘图, 透明度绘图,遮盖, 阴影, 透明层, 颜色管理, 防锯齿渲染, 生成PDF, 以及PDF元数据的相关处理. Quartz 2D也被称为Core Graphics, 缩写前缀为CG.

绘制视图

在iOS上无论采用哪种绘图技术,绘制都发生在UIView对象的区域内. 可以在UIView的drawRect方法中实现自定义绘图.

视图绘制周期

系统会为视图设置一个重绘标识, 在RunLoop每次循环时, 绘图引擎会检查重绘标识, 以此判断是否有需要更新的内容. 如果需要重绘, 则调用drawRect方法.

也可以手动设置重绘标识:

  • setNeedsDisplay: 重绘整个视图
  • setNeedsDisplayInRect:重绘指定区域的视图

原则上尽量不要重复绘制全部视图, 以降低系统绘制开销.

以下几种情况会触发视图重绘:

  1. 当遮挡视图的其他视图被移动或删除操作的时候;
  2. 将视图的hidden属性声明设置为NO, 使其从隐藏状态变为可见;
  3. 将视图滚出屏幕, 然后再重新回到屏幕上;
  4. 显式调用视图的setNeedsDisplay或者setNeedsDisplayInRect方法;

填充与描边

UIKit提供了基础的绘图功能, 主要API有:

  1. UIRectFill: 矩形填充;
  2. UIRectFrame: 矩形描边;
  3. UIBezierPath: 绘制常见路径, 包括线段, 弧形, 矩形, 圆角矩形和椭圆;

示例代码如下:

override func draw(_ rect: CGRect) {
    // Drawing code
    //为当前的图形上下文设置要填充的颜色
    UIColor.brown.setFill()
    //填充矩形
    UIRectFill(rect)

    //设置描边颜色
    UIColor.white.setStroke()
    let frame = CGRect(x: 20, y: 30, width: 100, height: 300)
    //描边
    UIRectFrame(frame)
}

绘制图像和文本

UIImage类中绘制图像主要的方法:

  1. drawAtPoint: 以指定点作为起始点进行绘制;
  2. drawInRect: 在指定区域内绘制;
  3. drawAsPatternInRect: 在指定的矩形里平铺图片,如果图片大小超出了指定的矩形, 形式上与drawAtPoint方法类似了, 如果图片大小小于指定的矩形,就会有平铺的效果;

NSString中绘制文本的主要方法如下:

  1. draw(at point: CGPoint, withAttributes attrs: [String : Any]?): 在指定点开始绘制文本;
  2. draw(in rect: CGRect, withAttributes attrs: [String : Any]?): 在制定区域内绘制文本;

示例如下:

override func draw(_ rect: CGRect) {

    let imgPath = Bundle.main.path(forResource: "cat", ofType: "png")
    guard (imgPath != nil)  else {
        return;
    }
    let img = UIImage(contentsOfFile: imgPath!)
    img?.draw(in: rect)

    let title = "我的小狗" as NSString
    let font = UIFont.systemFont(ofSize: 34)
    let attr = [NSFontAttributeName:font]
    let point = CGPoint(x:100,y:20)
    title.draw(at: point, withAttributes: attr)
}

Quartz图形上下文

想象在绘画时, 图像上下文便是你的手, 想画红色时,就拿起红色画笔,想画绿色就拿起绿色画笔. 当切换图形上下文的颜色及其它参数时, 就是在替换不同的蜡笔.

图形上下文包括了绘制命令所需要的信息, 定义了各种基本的绘制参数, 比如绘制使用的颜色、裁剪区域、线段的宽度及风格信息、字体信息、合成选项以及几个其他信息.

在调用drawRect方法之前, 视图对象会自动配置其绘制环境, 使代码立即执行进行绘制. 作为这些配置的一部分, UIView对象会对当前绘制环境创建一个图形上下文(对应于CGContextRef封装类型). 在前面的实例中就是采用这种默认方式的图形上下文.

也可以在drawRect方法中通过UIGraphicsGetCurrentContext()方法手动获取到图形上下文对象. 图形上下文仅对当前的drawRect:方法使用有效, 不要把图形上下文对象设置为成员变量.

下面的代码绘制了一个黑色描边, 红色填充的三角形:

override func draw(_ rect: CGRect) {
    // Drawing code
    let context = UIGraphicsGetCurrentContext()
    context?.move(to: CGPoint(x: 75, y: 10))
    context?.addLine(to: CGPoint(x: 10, y: 150))
    context?.addLine(to: CGPoint(x:160,y:150))

    //闭合路径
    context?.closePath()
    //设置描边色和填充色
    UIColor.black.setStroke()
    UIColor.red.setFill()

    context?.drawPath(using: .fillStroke)

}

Quartz路径

Quartz路径可以用来描述矩形, 圆, 以及其他想要的2D几何图形. 通过路径可以对几何图形进行各种处理. Quartz 2D中有4种基本图元: 点, 线段, 弧和贝塞尔曲线.

  1. 点: 点是二维空间中的一个位置, 不等同于像素, 一个点完全不占空间. 画一个点不会在屏幕上显示任何东西;
  2. 线段: 线段由起点和终点两个点定义. 线段没有面积,所依它们不能被填充.可以用一组线段或者曲线组成一个具有闭合路径的几何图形, 然后进行填充;
  3. 弧: 弧由一个圆心点、半径、起始角和结束角定义. 圆是弧的特例. 弧是占有一定面积的路径, 所以可以被填充、描边和描边填充出来;
  4. 贝塞尔: 任何一条曲线都可以通过与它相切的控制线两端的点的位置来定义. 贝塞尔曲线可以用4个点描述, 其中两个点描述两个端点, 另外两个描述每一端的切线;

Quartz坐标变换

不同的绘图系统对坐标系的定义有所区别.

坐标系

在iOS中Quartz坐标系和UIKit坐标系有所不同, 具体来说:

  • Quartz 2D坐标系: 原点在左下角, x向右为正, y向上为正;
  • UIKit坐标系: 原点在左上角, x向右为正, y向下为正;

2D图形的基本变换

2D图形的基本变换包括平移, 缩放, 旋转, 反射和仿射几种形式.

平移变换

平移是一物体从一个位置到另一个位置所做的直线移动. 如果要把一个位于P(x,y)的点移到新位置p’(x’,y’)时, 只要在原坐标加上平移距离Tx及Ty即可.

缩放变换

用来改变一物体大小的变换称为缩放变换. 如果要对一个多边形进行比例变换, 那么可把各顶点的坐标(x,y)均乘以比例因子Sx、Sy可以相等或不等. 如果比例因子数值小于1, 则物体尺寸减小; 大于1, 则使物体放大; Sx及Sy都等于1,则物体大小形状不变.

旋转变换

物体上的各点绕一固定点沿圆周路径转动称为旋转变换. 可以用旋转角表示旋转量的大小. 一个点由位置(x,y)旋转到(x’,y’), 从水平轴到(x’,y’)的角度即为旋转角.

反射变换

反射是用来产生物体的镜像的一种变换. 物体的镜像一般是相对于一个对称轴产生的, 因此反射变换可以分为x轴对称变换、y轴对称变换和坐标原点的对称变换.

下面的示例将使用Quartz坐标系绘制的”倒图”调整为正:

override func draw(_ rect: CGRect) {

    let imgPath = Bundle.main.path(forResource: "cat", ofType: "png")
    guard (imgPath != nil)  else {
        return;
    }
    let img = UIImage(contentsOfFile: imgPath!)

    let ctx = UIGraphicsGetCurrentContext()

    //注释掉下面两句代码, 图片会倒过来
    //平移变换
    ctx?.translateBy(x: 0, y: img!.size.height)
    //反射变换 - y轴对称变换
    ctx?.scaleBy(x: 1, y: -1)


    let rect = CGRect(x: 0, y: 0, width: img!.size.width, height: img!.size.height)

    ctx?.draw(img!.cgImage!, in: rect)

}

仿射变换

仿射(affine)变换也是一种2D坐标变换. 它可以重用变换, 经过多次变换(即多次的矩阵相乘), 每一种变换都可以用矩阵表示, 通过多次矩阵相乘得到最后结果.

例如上例中调整”倒图”的代码, 也可以使用仿射变换达到同样的效果:

var myAffine = CGAffineTransform(translationX: 0, y: img!.size.height)
myAffine = myAffine.scaledBy(x: 1, y: -1)
ctx?.concatenate(myAffine)