本章中迄今为止的绘制实施例中大多会产生一个UIImage
对象,主要是通过调用UIGraphicsBeginImageContextWithOptions
得到的图形上下文,生成由UIImageView
或知道如何以显示图像的任何界面对象显示的图片。但是,正如我已经解释过,一个UIView
提供了一个图形上下文;不管你在图形上下文中绘制什么界面都会把它显示出来。这主要是通过子类化UIView
并实现子类的drawRect:
方法。
例如假设我们有一个名为MyView
的UIView
子类。我们想实例化这个类,并添加这个实列到视图层次上。实现这个效果的一种方式是在nib
编辑器中拖入一个UIView
在属性编辑中设置class
为MyView
;另一种是使用代码创建MyView
的实例,并把它放到界面上的。
结果是MyView
的drawRect:
会被调用。这是你的子类,不管你画的是什么都将出现在MyView
的实例中在代码运行的时候。因为UIView
自己实现的drawRect:
方法中什么都不会做,所有没有必要调用super
。当drawRect:
被调用时,当前的图形上下文已经被设置为视图自己的图形上下文。你可以使用Core Graphics
函数或UIKit
的便利方法在上外文中绘制。
任何时候都不应该直接调用
drawRect:
!你希望通过调用drawRect:
来更新你的视图,对视图发送setNeedsDisplay
消息就可以。这会导致的drawRect
:在下一个适当的时刻被调用。另外,不要重写drawRect:
除非你确实有必要。例如:在UIImageView
的子类中重写drawRect:
是非法的,你不能把自己的绘制和UIImageView
进行结合。
实时绘制对初学者来说有点惊讶,她们可能担心绘图一个耗时的操作。这是一种合理的担忧,相同的绘制会发生在界面的许多地方,通过构造绘制命令为一个UIImage
,然后在视图的drawRect
中绘制UIImage
,这通过都比较高效比每次都执行相同的绘制。但一般情况下,你不应该过早的优化代码。绘图操作的代码可能会非常冗长但是非常快。此外,iOS
的绘图系统是非常高效的;除非确实有必要(或者通过setNeedsDisplay
消息)它才会调用drawRect:
,并且一旦一个view
绘制完成,它将被缓存起来,使得高速缓存的绘制可以重复使用,而不是每次都从新绘制。(苹果将这个绘制缓存做为视图的bitmap backing store
。)通过在drawRect:
中输出一些log信息你可以很容易知道这些;你可能会惊奇地发现你自定义的UIView
的drawRect:
代码在应用程序的整个生命周期中只被调用了一次!其实,在drawRect:
中直接绘制通常是提高效率的一种方式。这是因为它比绘制到屏幕外面然后复制这些像素到屏幕上更加高效。(这就是不要离屏渲染的原因).
绘制的内容是很宽泛的但是可以划分几个部分,你可以通过传入drawRect:
的 rect
参数以获得一些额外的效率。它指定了需要刷新的视图的区域。通常,这是该视图的整个边界;但如果你调用setNeedsDisplayInRect:
你可以设置刷新的CGRect
。您可以指定绘制到这些区域但即使你不这样做,你的绘制也会被剪切到这些区域,所以,虽然你不会花更少的时间绘制,系统自己会更加高效的绘制。
当在代码中创建一个自定义UIView子类的实例时,你可能会惊讶地发现该视图有一个黑色的背景:
let mv = MyView(frame: CGRectMake(20, 20, 150, 150))
self.view.addSubview(mv)
如果你期望是一个透明的背景,这会令你非常失望。初学者也会觉得相当混乱。黑色背景出现了,那么有两件事可以确定:
- 视图的
backgroundColor
是nil
. - 视图的
opaque
属性是true
.
不幸的是,在代码中创建一个UIView
的时候,这两个东西默认都是true
!所以,如果你不想要黑色的背景,则必须改变其中一个(或两者)。如果视图不会是不透明的,它的opaque
应该设置为false
,所以这可能是干净的解决方案:
let mv = MyView(frame: CGRectMake(20, 20, 150, 150))
self.view.addSubview(mv)
mv.opaque = false
或者,这是你自己的UIView
子类,你可以实现它的init(frame:)
(指定初始化方法)并设置它自己的opapue
属性为false
:
override func init(frame: CGRect) {
super.init(frame: frame)
self.opaque = false
}
如果用nib
创建一个UIView
,黑色背景的问题就不会出现。这是因为这样一个UIView
的backgroundColor
不会是nil
。nib
会给它分配一些实际的背景颜色,即使该颜色是UIColor.clearColor()
。
当然,如果一个视图用不透明绘制填充的所有区域或具有不透明背景颜色,你可以设置opaque
为true
,并获得一些绘图效率的提示。
Graphics Context Settings
当你在一个图形上下文绘制,绘图遵循上下文的当前设置。因此,程序总是先设置上下文的各项参数,然后菜绘制。例如,如果要画出一条红线,紧接着画一条蓝线,你会先设置上下文的线条颜色为红色,然后绘制的第一条线;然后将上下文的线条颜色设置为蓝色,然后绘制第二条。肉眼看来红线和蓝线是独立的线条,但实际上,在绘制每条线的时候,线条的颜色是作用于整个图形上下文的。不管你是否使用UIKit
的方法或Core Graphics
函数,都是这样的。
一个图形上下文在任何一个时刻都有一个状态,就是所有设置的总和;绘制就是在某时刻上下文状态下绘制的结果。图形上下文为你提供一个栈来操作上下文的所有的状态。每当你调用CGContextSaveGState
时间,上下文推动整个当前的状态压入堆栈;每次调用CGContextRestoreGState
时,上下文从堆栈的顶部检索状态,并设置为自己的状态。
因此,一种常见的模式是:
- 调用
CGContextSaveGState
。 - 设置上下文,从而改变它的状态。
- 绘制。
- 调用
CGContextRestoreGState
来恢复到上一个上下文状态设置。
你没有必要每次操作上下文的设置都这样做,因为设置不一定与其他或过去设置冲突。你可以很容易设置上下文的线条颜色为红色然后在设置为蓝色。然而你可能希望你的上下文设置在某些特定的情况下是不可撤消的。
构成一个图形上下文的状态,并且决定某一时刻绘制的行为和样式的那些上下文设置和绘图程序是类似的。下面是其中一些的,和确定它们样式的命令。我列出了一些Core Graphics
的函数,其次是一些调用它们的UIKit
方法:
-
Line thickness and dash style
CGContextSetLineWidth
,CGContextSetLineDash
(andUIBezierPath
lineWidth
,setLineDash:count:phase:
) -
Line end-cap style and join style
CGContextSetLineCap
,CGContextSetLineJoin
,CGContextSetMiterLimit
(andUIBezierPath
lineCapStyle
,lineJoinStyle
,miterLimit
) -
Line color or pattern
CGContextSetRGBStrokeColor
,CGContextSetGrayStrokeColor
,CGContextSetStrokeColorWithColor
,CGContextSetStrokePattern
(andUIColor setStroke
) -
Fill color or pattern
CGContextSetRGBFillColor
,CGContextSetGrayFillColor
,CGContextSetFillColorWithColor
,CGContextSetFillPattern
(andUIColor setFill
) -
Shadow
CGContextSetShadow
,CGContextSetShadowWithColor
-
Overall transparency and compositing
CGContextSetAlpha
,CGContextSetBlendMode
-
Anti-aliasing(抗锯齿)
CGContextSetShouldAntialias
其它设置包括:
-
Clipping area
裁剪区域之外的绘制不会在物理设备上绘制。 -
Transform (or “CTM,” for “current transform matrix”)
如何将你在后续的绘图命令中指定的点映射到物理设备的画布上的变化。
Paths and Shapes
通过一系列用于移动一个假想的笔的绘图指令,用来构建一个从点到点的路径。你必须先告诉笔自己的定位,设置当前点;然后你告诉它如何描绘出路径上每一个后续的点。路径上的每一块都开始于当前点;并且结束时它是新的当前点。
需要注意的是路径与它本身并不构成绘图!首先,你提供一个路径;然后你绘制它。绘制意味着路径描边或填充路径,或两者同时进行。这应该是绘图程序中一个熟悉的概念。
下面是一些你可以使用的路径绘制命令:
-
Position the current point
CGContextMoveToPoint
-
Trace a line
CGContextAddLineToPoint
,CGContextAddLines
-
Trace a rectangle
CGContextAddRect
,CGContextAddRects
-
Trace an ellipse or circle
CGContextAddEllipseInRect
-
Trace an arc
CGContextAddArcToPoint
,CGContextAddArc
-
Trace a Bezier curve with one or two control points
CGContextAddQuadCurveToPoint
,CGContextAddCurveToPoint
-
Close the current path
CGContextClosePath
。这个追加一条从路径的最后一个点至第一点的线。如果你要填充路径,你没有必要这么做,因为它已经为你做了。 -
Stroke or fill the current path
CGContextStrokePath
,CGContextFillPath
,CGContextEOFillPath
,CGContextDrawPath
。路径描边或填充路径会清楚当前路径。使用CGContextDrawPath
来同时描边和填充,因为如果你先用CGContextStrokePath
来描边,你永远都不能再填充它,因为路径已经在描边后被清除了。也有很多便利的方法使用一个命令来创建路径,描边或填充路径:- CGContextStrokeLineSegments
- CGContextStrokeRect
- CGContextStrokeRectWithWidth
- CGContextFillRect
- CGContextFillRects
- CGContextStrokeEllipseInRect
- CGContextFillEllipseInRect
路径可以合成,这意味着它可以包含多个独立的部分。例如,一个单一的路径可能包括两个单独的封闭的形状:矩形和圆形。当您在构建路径的中间调用CGContextMoveToPoint
(即构造路径之后,并没有描边或填充以清除它),你拿起虚拟画笔,并将其移动到一个新位置而不跟踪这一段,从而开始构造一段独立的路径。当你开始构造一个路径,这有可能是一个现有路径而且新路径可能被看作是现有路径的一部分,你可以调用CGContextBeginPath
来指定,这是一个不同的路径;苹果公司的许多例子都这样做,但在实践中我通常不觉得这有必要。
为了说明路径绘制命令的使用方法,我会绘制上图所示的箭头。这可能不是创建箭头的最佳方式,而且我刻意回避使用的便利的函数,但是这很清晰的显示了品种基本的典型的命令使用方式:
//obtain the current graphics context
let context = UIGraphicsGetCurrentContext()
CGContextMoveToPoint(context, 100, 100)
CGContextAddLineToPoint(context, 100, 19)
CGContextSetLineWidth(context, 20)
CGContextStrokePath(context)
CGContextSetFillColorWithColor(context, UIColor.redColor().CGColor)
CGContextMoveToPoint(context, 80, 25)
CGContextAddLineToPoint(context, 100, 0)
CGContextAddLineToPoint(context, 120, 25)
CGContextFillPath(context)
CGContextMoveToPoint(context, 90, 101)
CGContextAddLineToPoint(context, 100, 90)
CGContextAddLineToPoint(context, 110, 101)
CGContextSetBlendMode(context, .Clear)
CGContextFillPath(context)
如果需要重复使用或共享路径,可以将它封装为一个CGPath
。你可以使用CGContextCopyPath
来复制图形上下文的当前路径。即使没有图形上下文,你可以创建一个新的CGMutablePath
(调用CGPathCreateMutable
),并使用和CGContext
的路径构造函数相似的各种CGPath
函数来构建路径。而且还有很多用于创建基于简单几何图形或现有路径的CGPath函数:
- CGPathCreateWithRect
- CGPathCreateWithEllipseInRect
- CGPathCreateWithRoundedRect
- CGPathCreateCopyByStrokingPath
- CGPathCreateCopyByDashingPath
- CGPathCreateCopyByTransformingPath
UIKit
的UIBezierPath
类封装了CGPath
(CGPath
属性);它提供了和CGContext
和CGPath
相似的函数用于构建路径,如:
- init(rect:)
- init(ovalInRect:)
- init(roundedRect:cornerRadius:)
- moveToPoint:
- addLineToPoint:
- addArcWithCenter:radius:startAngle:endAngle:clockwise:
- addQuadCurveToPoint:controlPoint:
- addCurveToPoint:controlPoint1:controlPoint2:
- closePath
当调用UIBezierPath
实例方法填充或描边(fillWithBlendMode:alpha:
或者strokeWithBlendMode:alpha:
),当前图形上下文设置会被保存,包装的CGPath
会成为当前的图形上下文的路径然后被描边或填充,然后当前图形上下文的设置会被恢复。
因此,使用UIBezierPath结合UIColor,我们可以完全用UIKit的方法改写我们的箭头绘制例子:
let path = UIBezierPath()
path.moveToPoint(CGPointMake(100, 100))
path.AddLineToPoint(CGPointMake(100, 19))
path.lineWidth = 20
path.strokePath()
UIColor.redColor().set()
path.removeAllPoints()
path.moveToPoint(CGPointMake(80, 25))
path.addLineToPoint(CGPointMake(100, 0))
path.addLineToPoint(CGPointMake(120, 25))
path.fill()
path.removeAllPoints()
path.moveToPoint(CGPointMake(90, 101))
path.addLineToPoint(CGPointMake(100, 90))
path.addLineToPoint(CGPointMake(110, 101))
path.fillWithBlendMode(.Clear, alpha: 1.0)
上面代码没有调用Core Graphics
函数,因此使用Core Graphics
或者UIKit
取决于你自己。UIBezierPath
也是很有用的,当你想捕捉一个CGPath
并把它作为一个object
传递的时候;
Clipping
你可能会使用路径来遮罩(mask
)区域而不是通过填充或者路径描边,来使它们不会在以后被绘制。这就是clipping
。默认情况下,图形上下文的剪裁区域是整个图形上下文:你可以在上下文中的任何地方绘制。
裁剪区域的一个特征是把上下文作为一个整体,并且任何新的裁剪区域会于现有的裁剪区域相交;所以如果你想把你自己的裁剪区域从当前的上下文中删除,通过提前调用CGContextSaveGState
和CGContextRestoreGState
来实现。
我将使用裁剪(clipping
)而不是混合模式(blend mode
)来处理上面例子中的箭头尾部“切出”的三角形缺口。这有个小技巧,因为我们要裁剪的不是三角形的内部区域,但是三角形外面的区域。为了说明这一点,我们将使用由多个封闭区域组成的复合路径 -- 三角形和做为整体的绘图区域(可以通过CGContextGetClipBoundingBox
获得)。
填充复合路径或者用它来表达裁剪区域的时候,系统遵循以下两个规则之一:
-
Winding rule (非0缠绕规则)
填充或剪裁区域由在路径划分的每个区域的方向(顺时针或逆时针)表示。 -
Even-odd rule (EO--奇偶数规则)
由每个区域路径的简单计数表示填充或剪辑区域。
上面两个规则都是图形学方面来确定是否绘制子路径的方法。iOS默认使用非0缠绕规则。
我们的情况非常简单,所以更容易使用奇偶规则。因此我们通过CGContextEOClip
设置裁剪区域然后绘制箭头:
let context = UIGraphicsGetCurrentContext()
CGContextMoveToPoint(context, 90, 100)
CGContextAddLineToPoint(context, 100, 90)
CGContextAddLineToPoint(context, 110, 100)
CGContextClosePath(context)
CGContextAddRect(context, CGContextGetClipBondingBox(context))
CGContextEOClip(context)
//draw vertical line
CGContextMoveToPoint(context, 100, 100)
CGContextAddLineToPoint(context, 100, 19)
CGContextSetLineWidth(context, 20)
CGContextStrokePath(context)
// draw red triangle
CGContextSetFillColorWithColor(context, UIColor.redColor().CGColor)
CGContextMoveToPoint(context, 80, 25)
CGContextAddLineToPoint(context, 100, 0)
CGContextAddLineToPoint(context, 120, 25)
CGContextFillPath(context)
UIBezierPath
的裁剪命令是usesEvenOddFillRule
和addClip
。
| 上下文有多大 |
| :---------: |
|乍一看,似乎没有方法知道图形上下文的大小。通常情况下,这并不重要,因为图形上下文是你自己创建或者被一些你知道它大小的物体创建的,比如一个UIView
的图形上下文。但事实上,因为图形上下文的默认剪切区域是整个上下文,你可以使用CGContextGetClipBoundingBox
知道上下文的“边界”。|
Gradients
渐变可以很简单也可以很复杂。一个简单的渐变可以通过一个颜色点与另一个颜色点,再加(或可选)中间颜色点确定;那么渐变可以通过两个点线性或两个圆之间径向绘制。
你不能用一个渐变做为路径的填充颜色,但可以通过裁剪来限制路径的形状,这本质上是一样的。
为了说明,我将重绘我们的箭头,用线性渐变做为箭头的“轴”:
let context = UIGraphicsGetCurrentContext()!
CGContextSaveState(context)
CGContextMoveToPoint(context, 90, 100)
CGContextAddLineToPoint(context, 100, 90)
CGContextAddLineToPoint(context, 110, 100)
CGContextClosePath(context)
CGContextAddRect(context, CGContextGetClipBoundingBox(context))
CGContextEOClip(context)
//draw vertical line
CGContextMoveToPoint(context, 100, 100)
CGContextAddLineToPoint(context, 100, 19)
CGContextSetLineWidth(context, 20)
CGContextReplacePathWithStrokePath(context)
CGContextClip(context)
//draw gradient
let locs: [CGFloat] = [0.0, 0.5, 1.0]
let colors: [CGFloat] = [
0.8, 0.4, //start color, transparent light gray
0.1, 0.5, //intermediate color,
0.8, 0.4, //end color,
]
let sp = CGColorSpaceCreateDeviceGray()
let grad = CGGradientCreateWithColorComponents(sp, colors, locs, 3)
CGContextDrawLinearGradient(context, grad, CGPointMake(89, 0), CGPointMake(110, 0), [])
//done clipping
CGContextRestoreState(context)
//draw red triangle
CGContextSetFillColorWithColor(context, UIColor.redColor().CGColor)
CGContextMoveToPoint(context, 80, 25)
CGContextAddLineToPoint(context, 100, 0)
CGContextAddLineToPoint(context, 120, 25)
CGContextFillPath(context)
效果如下图:
对CGContextReplacePathWithStrokedPath
的调用会使用当前线宽和其他线条相关的上下文状态设置来假装绘制路径,但然后创建表示该描边路径之外的新路径。从而替换作为裁剪区域使用的粗线段。
我们创建渐变然后绘制它。该过程冗长但是简单;一切都是模式化的。我们把渐变描述成一个端点(0.0)到另一个端点(1.0)之间的连续的颜色点的位置数组,具有对应于每个位置的颜色的颜色的COM ponents沿;在这个例子中,我希望渐变在边缘处较亮在中间较暗,所以我用三个位置,而去较暗的位置在0.5处。我们还必须提供一个色彩空间;这将告诉渐变如何绘制我们的颜色分量。最后,创建渐变并将其绘制到指定位置。
Colors and Patterns
一种颜色是一个CGColor
。CGColor
不难使用,并且可以通过的UIColor
的init(CGColor:)
和CGColor
的方法在这两个之间相互转换。
一个模式(pattern
)也是一种颜色。您可以创建一个模式颜色然后描边或者填充它。最简单的方法是绘制模式颜色成一个小块状的UIImage
然后调用UIColor
的init(patternImage:)
创建颜色。为了说明这一点,我将创建横条纹的模式颜色并用它来画箭头,而不是一个坚实的红色:
// CGContextSetFillColorWithColor(con, UIColor.redColor().CGColor)
// not any more, we're going to paint with a pattern instead of red!
// create the pattern image tile
UIGraphicsBeginImageContextwithOptions(CGSizeMake(4, 4), false, 0)
let imcon = UIGraphicsGetCurrentContext()!
CGContextSetFillColorWithColor(imcon, UIColor.redColor().CGColor)
CGContextFillRect(imcon, CGRectMake(0, 0, 4, 4))
CGContextSetFillColorWithColor(imcon, UIColor.blueColor().CGColor)
CGContextFillRect(imcon, CGRectMake(0, 0, 4, 2))
let stripes = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
//paint the point of the arrow with it
let stripesPattern = UIColor(patternImage: stripes)
stripesPattern.setFill()
let p = UIBezierPath()
p.moveToPoint(CGPointMake(80, 25))
p.addLineToPoint(CGPointMake(100, 0))
p.addLineToPoint(CGPointMake(120, 25))
p.fill()
效果如下图:
Core Graphics
的CGPattern
相当的强大,而且也更加的复杂:
let sp2 = CGColorSpaceCreatePattern(nil)
CGContextSetFillColorSpace(con, sp2)
let drawStripes : CGPatternDrawPatternCallback = {
_, con in
CGContextSetFillColorWithColor(con!, UIColor.redColor().CGColor)
CGContextFillRect(con!, CGRectMake(0,0,4,4))
CGContextSetFillColorWithColor(con!, UIColor.blueColor().CGColor)
CGContextFillRect(con!, CGRectMake(0,0,4,2))
}
var callbacks = CGPatternCallbacks(version: 0, drawPattern: drawStripes, releaseInfo: nil)
let patt = CGPatternCreate(nil, CGRectMake(0,0,4,4),
CGAffineTransformIdentity, 4, 4,
.ConstantSpacingMinimalDistortion, true, &callbacks)
var alph : CGFloat = 1.0
CGContextSetFillPattern(con, patt, &alph)
CGContextMoveToPoint(con, 80, 25)
CGContextAddLineToPoint(con, 100, 0)
CGContextAddLineToPoint(con, 120, 25)
CGContextFillPath(con)
要理解这段代码可以倒着读。一切都围绕调用CGPatternCreate
。模式是在矩形“细胞”中的绘制。我们要说明细胞的尺寸(第二个参数)和细胞的原点之间的间距(第四和第五参数)。在这种情况下,该细胞大小是4×4,和每一个细胞恰好水平和垂直接触它的邻居。我们必须提供一个变换作用与细胞上(第三个参数);在这个例子中,我们没有做任何变换,所以我们提供identity transform
。我们提供一个平铺规则(第六个参数)。我们必须说明这是否是一个颜色模式或模板模式;这是一个颜色模式,所以第七个参数是true
。我们还有一个指针指向实际模式图案到细胞(第八参数)的回调函数。
其实我们要在这里提供一个CGPatternCallbacks
结构的指针做为第八个参数。这个结构包括数字0和两个函数指针,一个绘制模式到其细胞,另一个在模式被释放的时候调用。我们没有提供第二个函数,但是我们并不需要在这个简单的例子中管理内存。
正如你所看到的,实际的模式绘制代码(drawStripes
)是很简单的。唯一棘手的问题是,调用CGPatternCreate
时单元格必须大小一样,不然模式并不会以你期望的方式绘制成功。在这个例子中单元格大小是4×4。因此,我们用红色填充它,然后用蓝色填充它的下半部分。当这些单元格被水平和垂直平铺时,我们得到了你在上图中看到条纹。
通过CGPatternCreate
生成CGPattern
之后,调用CGContextSetFillPattern
而不是设置填充颜色,我们设置填充模式来填充路径(这个例子中的三角箭头)。CGContextSetFillPattern
第三个参数是指向一个CGFloat
的指针,所以我们必须事先设置好CGFloat
本身。第二个参数是CGPattern
。
剩下来唯一要解释的是代码的前两行。在调用CGContextSetFillPattern
之前,你必须设置上下文的填充色色彩空间设置为模式的色彩空间。如果你忘记这一点,调用CGContextSetFillPattern
会得到一个错误。上面的代码通过设置其填充颜色空间为模式颜色空间,这让图形上下文在一个不稳定的状态。如果我们后来尝试将填充颜色设置为正常颜色这会很麻烦。解决方案,像往常一样,是将代码封装在对CGContextSaveState
和CGContextRestoreState
的调用之间。
你可以在上图中看到,条纹并不完全适合箭头三角形:最底层的条纹有点像半个蓝色条纹。这是因为模式相对于你正在填充(或描边)的形状没有很好的缩放,但相对于作为一个整体的图形上下文它能很好的缩放。我们可以在绘制之前调用CGContextSetPatternPhase
移动模式的位置。
Graphics Context Transforms
正如一个UIView可以有一个转换(transform
),图形上下文也可以有。然而,将一个变换作用于图形上下文不会影响已经存在的绘制,它只会影响绘制完成后的坐标系统映射到图形上下文的区域转换。图形上下文的变换被称为CTM
(current transform matrix.
)。
充分利用CTM
让自己免于执行简单的计算是很常用的。你可以通过CGContextConcatCTM
来叠加CGAffineTransform
到现有的变换上;也有便利的函数用于平移,缩放或旋转变换到当前变换。
当你获得图形上下文时最基本的变换已经为你设置好;这就是系统能够映射上下文绘制坐标到屏幕坐标的原因。任何变换都被叠加到当前变换,所以基本变换和绘制都是有效的。在应用你的变换后可以通过封装你的代码在调用CGContextSaveGState
和CGContextRestoreGState
之间而回到基本变换。
例如,迄今为止我们一直硬编码我们的箭头位置:矩形的左上角处(80,0)。这是愚蠢的。它使代码难以理解,以及缺乏灵活性,很难重用。明智的做法是在(0,0)处绘制的箭头,通过现有代码从所有x值减去80。现在很容易在任何位置画出箭头,通过事先施加一个简单的平移变换,映射(0,0)到箭头左上角。因此,在(80,0)画出箭头,我们可以这样:
CGContextTranslateCTM(con, 80, 0)
// now draw the arrow at (0,0)
旋转变换特别有用,它可以让你在一个旋转的方向中绘制而不用关系任何关于几何的计算。然而这也有点棘手,因为是围绕原点旋转的,这通常不是你想要的。所以你必须先做一个平移变换,把原点映射到你要旋转的点。但旋转之后,为了搞清楚在哪里绘制你可能要做个反向平移变换。
为了说明这一点,下面的代码在多个角度重复的围绕它的尾巴转动来绘制箭头。由于箭头将被绘制多次,我会把绘制箭头封装成一个UIImage。这不仅是减少重复会使绘制效率更高;这也是因为我们希望整个箭头旋转,包括图案的条纹,这是实现这种效果最简单的方法:
func arrowImage() -> UIImage {
UIGraphicsBeginImageContextWithOptions(CGSizeMake(40, 100), false, 0)
let context = UIGraphicsGetCurrentContext()
//draw arrow into the image context
//draw it at (0, 0)! adjust all x-values by subtracting 80
// ... actual code omitted...
let im = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return im
}
我们只生成箭头图片一次,并把它存储在某个地方 - 我将使用属性为self.arrow
来获取它。在我们的drawRect:
方法种,我们多次绘制箭头图像:
override func drawRect(rect: CGRect) {
let context = UIGraphicsGetCurrentContext()
self.arrow.drawAtPoint(CGPointMake(0, 0))
for _ in 0 ..< 3 {
CGContextTranslateCTM(context, 20, 100)
CGContextRotateCTM(context, 30 * CGFloat(M_PI) / 180.0)
CGContextTranslateCTM(context, -20, -100)
self.arrow.drawAtPoint(CGPointMake(0, 0))
}
}
变换也是一种解决CGContextDrawImage
的“翻转”问题方案。我们可以旋转上下文而不是我们的绘制。从本质上讲,我们对上下文的坐标系统施加了一个翻转变换。首先移动上下文的顶部向下,然后通过给y
轴乘以-1
让y
轴向下:
CGContextTranslateCTM(context, 0, theHeight)
CGContextScaleCTM(context, 1.0, -1.0)
效果如下:
向下移动上下文的顶部(高度)多远取决于你打算如何绘制图像。
Shadows
在绘制前给上下文添加阴影值来给绘制添加阴影效果。阴影的位置被表示为一个CGSize
,其中对于两个值的正方向表示向下和向右。模糊值是一个开放式的正数;苹果并没有解释这是如何工作的,但实验表明,值12是很好的效果,值99太过于模糊而没有形状,更高的值会变得有问题。
下图显示了在绘制之前先给上下文添加阴影的效果:
let con = UIGraphicsGetCurrentContext()!
CGContextSetShadow(con, CGSizeMake(7, 7), 12)
self.arrow.drawAtPoint(CGPointMake(0,0)) // ... and so on
效果如下图:
从上图中不能很明显的发现我们每绘制都增加了一层阴影。因此,箭头能够在另一个箭头下面投下阴影。然而,我们希望所有的箭头集体投下一个阴影。实现这一目标的方法是使用一个透明层;这基本上是一个子上下文然后积累所有的绘制之后添加一个阴影。现在绘制阴影箭头代码如下所示:
let context = UIGraphicsGetCurrentContext()
CGContextSetShadow(context, CGSizeMake(7, 7), 12)
CGContextBeginTransparencyLayer(context, nil)
self.arrow.drawAtPoint(CGPointMake(0, 0))
for _ in 0 ..< 3 {
CGContextTranslateCTM(context, 20, 100)
CGContextRotateCTM(context, -20, 100)
CGContextTranslateCTM(context, -20, -100)
self.arrow.drawAtPoint(CGPointMake(0, 0))
}
CGContextEndTransparencyLayer(context)
Erasing
CGContextClearRect
函数能够擦除矩形内的所有绘制;结合裁剪,它可擦除任意形状的区域。结果是可以在现有的绘制中打洞
。
CGContextClearRect
的行为取决于上下文是否是透明的或不透明的。这在图像上下文中特别明显直观。如果图像背景是透明的 - UIGraphicsBeginImageContextWithOptions
的第二个参数是false
- CGContextClearRect
擦除绘制为透明背景;否则擦除绘制为黑色背景。
当在视图中直接绘制时(通过drawRect:
或drawLayer:inContext:
),如果视图的背景颜色是nil
或透明,甚至只有一点透明度,CGContextClearRect
擦除的结果将显示为透明,穿透视图的背景颜色打一个孔;如果背景颜色是完全不透明的,CGContextClearRect
的结果将是黑色的。这是因为,该视图的背景色确定该视图的图形上下文是否是透明的或不透明;因此,这基本上和我在前面段落中描述的行为相投。
下图显示左侧的蓝色正方形已被部分切掉为黑色,而在右侧的蓝色正方形已部分切掉为透明。然而这都是一样的UIView
子类,具有完全相同的绘制代码!该UIView
子类的drawRect:
是这样的:
let context = UIGraphicsGetCurrentContext()!
CGContextSetFillColorWithColor(context, UIColor.blueColor().CGColor)
CGContextFillRect(context, rect)
CGContextClearRect(context, CGRectMake(0, 0, 30, 30)))
效果如下:
上图中2种视图的差别是,第一个视图的backgroundColor
是纯红色而且alpha
值为1
,而第二个视图的backgroundColor
是纯红色alpha
值为0.99
。这种差异是眼睛完全察觉不到的(更不用说从未出现的红色,因为它被蓝色全部覆盖),但它完全改变CGContextClearRect
的效果。
Points and Pixels
点是通过一个无量纲位置的x坐标和y坐标表示的。当你在图形上下文种绘制时,可以指定在某些点处绘制,这和设备的分辨率没有关系,因为Core graphics
使用基本CTM
和反锯齿会很好的转换你的绘制到屏幕上。因此在上面的例子中,我只关注图形上下文中的点而忽略它们和屏幕像素的关系。
但是,确实存在像素。像素是物理的,整体的,现实世界中显示的基本单元。点介于像素之间。例如,如果有线宽为1为竖直的坐标轴描边,绘制的线会在路径的两边,这在单分辨率的屏幕上绘制的线看起来是2像素宽(因为该设备无法点亮半个像素)。
这种效果有时候是令人反感的,有人建议应该尝试偏移线的位置0.5,把线的中心和像素对齐。这个建议可能会有用,但它只是一些头脑简单的假设。一个更复杂的方法是获得UIView
的contentScaleFactor
属性。你可以除以这个数值从像素转换为点。最准确的方法来绘制一个垂直或水平线不是为路径描边,而是填充矩形。因此,下吗的UIView子类代码可以在任何设备上绘制一个完美的1个像素宽的垂直线:
CGContextFillRect(context, CGRectMake(100, 0, 1.0 / self.contentScaleFactor, 100))
Content Mode
一个视图自己绘制一些东西和其他仅仅有一个背景颜色和子视图的视图截然相反。这意味视图的contentMode属性就变得非常重要当它的大小改变的时候。正如我前面提到的,绘图系统将避免重新绘制一个视图,相反,它会使用以前的绘制操作(bitmap backing store
)缓存的结果。所以,如果视图大小改变时,系统可能会简单地伸展或收缩或移动缓存的绘制,如果你的contentMode设置指示它这样做。
说明这一点有点棘手,视图的内容来自于drawRect:
,因为我要为视图准备好它的内容(在drawRect:
),然后改变它的大小但是不让它被重绘(drawRect:
不能被再次调用)。下面是我的实现。当应用程序启动时,我将创建一个UIView
子类,MyView
,它知道如何绘制我们的箭头。然后,在窗口和界面已经显示之后我会使用一个延时函数来改变我们视图的大小:
delay(0.1) {
mv.bounds.size.height *= 2
}
我们把视图的高度变为双倍,而不会导致drawRect:
被调用。其结果是,该视图的绘制显示在其两倍高度。
迟早,drawRect
:将被调用,根据我们的代码绘制会被刷新。我们的代码没有指定箭头的高度是相对于视图的边界高度;它绘制的箭头在一个固定的高度。因而,箭头会缩回到原来的大小。
因此视图的contentMode属性经常于如何绘制自身向一致。我们的drawRect:
代码指示箭头的大小和位置相对于视图边界原点,在视图的左上角位置;所以我们可以设置contentMode
为.TopLeft
。另外,我们可以将其设置为.Redraw
;这将导致自动缓存内容被关闭 -当视图大小改变时,它的setNeedsDisplay
方法将被调用,最终引发drawRect:
重绘内容。