在iOS中绘图技术主要有UIKit, Quartz 2D,Core Animation和OpenGL ES. 其中Core Animation提供动画实现. OpenGL ES是OpenGL针对嵌入式设备的简化版本, 可以绘制高性能的2D和3D图形. 这里重点介绍UIKit和Quartz 2D.
- UIKit: 高级别的图形接口, 提供了基于Objective-C和Swift的API. 能够访问绘图,动画,字体,图片等内容.
- Quartz 2D: iOS和OSX环境下的2D绘图引擎. 包括基于路径的绘图, 透明度绘图,遮盖, 阴影, 透明层, 颜色管理, 防锯齿渲染, 生成PDF, 以及PDF元数据的相关处理. Quartz 2D也被称为Core Graphics, 缩写前缀为CG.
绘制视图
在iOS上无论采用哪种绘图技术,绘制都发生在UIView对象的区域内. 可以在UIView的drawRect方法中实现自定义绘图.
视图绘制周期
系统会为视图设置一个重绘标识, 在RunLoop每次循环时, 绘图引擎会检查重绘标识, 以此判断是否有需要更新的内容. 如果需要重绘, 则调用drawRect方法.
也可以手动设置重绘标识:
- setNeedsDisplay: 重绘整个视图
- setNeedsDisplayInRect:重绘指定区域的视图
原则上尽量不要重复绘制全部视图, 以降低系统绘制开销.
以下几种情况会触发视图重绘:
- 当遮挡视图的其他视图被移动或删除操作的时候;
- 将视图的hidden属性声明设置为NO, 使其从隐藏状态变为可见;
- 将视图滚出屏幕, 然后再重新回到屏幕上;
- 显式调用视图的setNeedsDisplay或者setNeedsDisplayInRect方法;
填充与描边
UIKit提供了基础的绘图功能, 主要API有:
- UIRectFill: 矩形填充;
- UIRectFrame: 矩形描边;
- 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类中绘制图像主要的方法:
- drawAtPoint: 以指定点作为起始点进行绘制;
- drawInRect: 在指定区域内绘制;
- drawAsPatternInRect: 在指定的矩形里平铺图片,如果图片大小超出了指定的矩形, 形式上与drawAtPoint方法类似了, 如果图片大小小于指定的矩形,就会有平铺的效果;
NSString中绘制文本的主要方法如下:
- draw(at point: CGPoint, withAttributes attrs: [String : Any]?): 在指定点开始绘制文本;
- 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种基本图元: 点, 线段, 弧和贝塞尔曲线.
- 点: 点是二维空间中的一个位置, 不等同于像素, 一个点完全不占空间. 画一个点不会在屏幕上显示任何东西;
- 线段: 线段由起点和终点两个点定义. 线段没有面积,所依它们不能被填充.可以用一组线段或者曲线组成一个具有闭合路径的几何图形, 然后进行填充;
- 弧: 弧由一个圆心点、半径、起始角和结束角定义. 圆是弧的特例. 弧是占有一定面积的路径, 所以可以被填充、描边和描边填充出来;
- 贝塞尔: 任何一条曲线都可以通过与它相切的控制线两端的点的位置来定义. 贝塞尔曲线可以用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)