玩转iOS中的绘图(Quartz 2D基础篇)
前言: 本篇简单介绍使用Quartzs 2D来绘图.如果你在网上搜索关于iOS开发中对于图片的处理(缩放...),或者运用截取屏幕或者某一部分内容来实现一些动画交互(比如实现tableViewCell, collectionView的移动效果...), 以及自定义一些控件(比如圆形进度条...)那么大多数的处理都会涉及到绘图的处理, 也许很多功能在你需要实现的时候, 搜索一下可以找到一些解决问题的代码, 但是或许在使用很久之后还是不明白其中的知识点. 本篇大多内容源于对官方文档"Quartz 2D Programming Guide"的理解.
首先对于Quartz 2D就不多做介绍了,(处理路径的绘图, 透明度绘图, 遮盖,阴影, 颜色管理, 防锯齿渲染, 生成PDF...) 感兴趣的朋友可以查查相关的资料
1. 了解Quartz 2D的绘图机制
- 和现实中的绘图一样, 首先需要一张"画布", 在iOS中被称为 "图形上下文", 这个图形上下文包含了设备的信息等很多系统准备好的内容
- 在上下文中运用一些方法来设置我们要绘制的内容和属性,来实现绘图
-
Quartz 2D类似打印机, 对不同的内容的绘图顺序不同, 得到的结果就不相同
这里有一张官方的图的解释,很直接
painters_model.gif
2.获取上下文
- 重写UIView的drawRect()方法, 当这个view的内容需要更新的时候会调用这个方法, 所以系统在这个方法里面会默认提供一个上下文, 可以通过UIGraphicsGetCurrentContext() 获取到
- 在其他地方可以通过UIGraphicsBeginImageContextWithOptions()开启一个上下文, 然后通过 UIGraphicsGetCurrentContext() 可以获取到
- 关于drawRect()方法(处理不当时有人称为"内存恶魔")
// 调用setNeedsDisplay()这个函数会触发drawRect方法, 当这个函数被调用的时候, 系统会通知这个UIView整个界面需要重绘. 但是这个函数会直接返回, 但界面的变化不会马上返回, 需要等到下个生命周期进行重绘
// 如果只有一部分需要重绘 可以调用这个方法 setNeedsDisplayInRect(rect: CGRect)
// 这个方法里面应该只做与界面的绘制有关的工作, 不应该进行任何的数据处理或者程序的逻辑相关的任务, 以保证尽快执行完毕
3. 坐标系
- 和数学中的直角坐标系相同, 原点在左下角, UIKit中的坐标系的原点在左上角, 所以使用Quartz 2D绘图的时候要注意进行坐标系的转换
4. 内存管理
- 因为使用的C语言的东西, 编译器并没有给我们自动管理内存, 在oc中需要我们自己来管理, 即当调用含有create或者copy的函数获得上下文的时候, 需要在使用完后手动release, 但在swift中我们不用管理了(而且C语言的函数在swift中看上去也比较习惯)
5. 绘制我们需要的内容
-
5.1 绘制路径
- 首先需要我们画出路径 -> 指定起始点到另一个点, 然后两点之间可以使用直线或者曲线来连接, 就形成了一条路径, 例如使用下面的函数来实现
```
CGContextMoveToPoint() -> 指定起始点或者移动到新的点
CGContextAddLineToPoint() -> 从当前的点到指定的点之间画一条线
CGContextAddArc() -> 圆弧
CGContextAddArcToPoint() -> 会在当前点和指定点与坐标系平行的直线做切线画圆弧
CGContextAddCurveToPoint() -> 画曲线(bezier)
CGContextClosePath() -> 将起始点和结束点连接起来形成封闭的路径
CGContextAddEllipseInRect() -> 椭圆
```- 然后设置需要绘制的路径的属性 -> 设置颜色,透明度,宽度, 绘制模式...例如使用如下的函数来实现
```
CGContextSetLineWidth() -> 线宽度
CGContextSetLineJoin() -> 线的连接点的样式
CGContextSetLineCap() -> 端点的样式
CGContextSetLineDash() -> 虚线
CGContextSetStrokeColorWithColor() -> stroke模式时的颜色
CGContextSetFillColorWithColor() -> fill模式时的颜色
```
* 绘制内容, 注意会涉及到两种方式: fill 和 stroke, 这里解释一下两者的区别
- 当使用stroke方式 -> "描边"即只绘制路径
-
当使用fill方式"填充"即绘制路径包括的所有区域(所有路径都会被当作closePath处理)
-
Filling有两种模式
nonzero winding number(默认) -> 如果两个路径有重叠的时候, 绘制方向相同的话, 那么重叠部分的绘制可能不是我们希望的
even-odd -> 不受绘制方向的影响
CGContextEOFillPath
CGContextFillPath
CGContextFillRect
CGContextFillRects
CGContextFillEllipseInRectCGContextStrokePath(context) CGContextFillPath(context)
-
示例
stroke模式绘制线
```
override func drawRect(rect: CGRect) {
// Drawing code
// 获取当前上下文
let context = UIGraphicsGetCurrentContext()
let strokeColor = UIColor.blueColor()
// 设置stroke模式的颜色使用CGColor(这里涉及到颜色和颜色空间的概念)
CGContextSetStrokeColorWithColor(context, strokeColor.CGColor)
// 设置线的宽度
CGContextSetLineWidth(context, 5.0)
// 设置连接处样式...还可以设置很多其他的属性
CGContextSetLineJoin(context, CGLineJoin.Round)
// 起始点
CGContextMoveToPoint(context, 10.0, 10.0)
// 在点(10.0, 10.0)和(10.0, 80.0)之间画一条线 所以点(10.0, 80.0)被设置为下一个起始点
CGContextAddLineToPoint(context, 10.0, 80.0)
//在点(10.0, 80.0)和(80.0, 80.0)之间画一条线 所以点(80.0, 80.0)被设置为下一个起始点
CGContextAddLineToPoint(context, 80.0, 80.0)
// 设置为封闭路径, 会将首尾点连接起来
// CGContextClosePath(context)
// 绘制当前的路径
CGContextStrokePath(context)
}
```
不封闭路径png
封闭路径.png
fill模式绘制相同的路径
let fillColor = UIColor.blueColor()
// 设置fill模式的颜色使用CGColor(这里涉及到颜色和颜色空间的概念)
CGContextSetFillColorWithColor(context, fillColor.CGColor)
// 起始点
CGContextMoveToPoint(context, 10.0, 10.0)
// 在点(10.0, 10.0)和(10.0, 80.0)之间画一条线 所以点(10.0, 80.0)被设置为下一个起始点
CGContextAddLineToPoint(context, 10.0, 80.0)
//在点(10.0, 80.0)和(80.0, 80.0)之间画一条线 所以点(80.0, 80.0)被设置为下一个起始点
CGContextAddLineToPoint(context, 80.0, 80.0)
// CGContextClosePath(context)
// 绘制当前的路径
CGContextFillPath(context)
```
![fill方式.png](http:https://img.haomeiwen.com/i1271831/3729cb7a3c50b757.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
stroke模式绘制圆
// 设置stroke模式的颜色使用CGColor(这里涉及到颜色和颜色空间的概念)
CGContextSetStrokeColorWithColor(context, strokeColor.CGColor)
// 设置线的宽度
CGContextSetLineWidth(context, 5.0)
// 设置连接处样式
CGContextSetLineJoin(context, CGLineJoin.Round)
// 起始点
let circleCenter = CGPoint(x: rect.width * 0.5, y: rect.height * 0.5)
CGContextMoveToPoint(context, circleCenter.x*2, circleCenter.y)
// 画圆
// x -> 圆心的x坐标
// y -> 圆心的y坐标
// radius -> 圆的半径
// startAngle -> 起始角度 (弧度制) 所以上面调整了起始点
// endAngle -> 结束角度(弧度制)
CGContextAddArc(context, circleCenter.x, circleCenter.y, rect.width * 0.5, 0.0, CGFloat(M_PI) * 2, 0)
CGContextStrokePath(context)
![stroke圆png](http:https://img.haomeiwen.com/i1271831/80e1a3a80711373c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
fill方式绘制圆
// 设置fill模式的颜色使用CGColor(这里涉及到颜色和颜色空间的概念)
CGContextSetFillColorWithColor(context, fillColor.CGColor)
// 起始点
let circleCenter = CGPoint(x: rect.width * 0.5, y: rect.height * 0.5)
CGContextMoveToPoint(context, circleCenter.x*2, circleCenter.y)
// 画圆
// x -> 圆心的x坐标
// y -> 圆心的y坐标
// radius -> 圆的半径
// startAngle -> 起始角度 (弧度制) 所以上面调整了起始点
// endAngle -> 结束角度(弧度制)
CGContextAddArc(context, circleCenter.x, circleCenter.y, rect.width * 0.5, 0.0, CGFloat(M_PI) * 2, 0)
// 绘制当前的路径
CGContextFillPath(context)
![fill方式.png](http:https://img.haomeiwen.com/i1271831/da58c13499f817c6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
-----
#####仅仅介绍了上面的几种看上去很简单的绘制方法, 也许你会觉得没有什么实际用处, 但是实际上是很有用处的, 因为到现在为止, 你可以使用Quartzs 2D来实现自定义的各种圆形进度条了,(可能使用到stroke和fill的混合模式), 只需要在提供的progress属性设置时调用setNeedsDisplay()就可以触发drawRect()方法进行重绘, 而在这里面你就可以利用progress来改变绘制的内容了, 从而实现进度条的效果
---
> 限于篇幅和时间, 这篇中只是梳理了一些基本的绘图概念和绘制简单的图形(实际上看看API就可以类似的使用相关的方法绘制出其他的图形 -- 椭圆...), 而实际上还有比较多的东西需要研究, 以后有必要在补上. [Demo](https://github.com/jasnig/DrawingStudy)