摘自 Programming iOS 10 一书
这里要转换一个观念, 就是所有的 UIView 实际只是空白画布.
下面就是需要绘制的几种情况:
-
创建自定义视图: 这个是一种最普遍的情况. 对 UIView 的绘制都是在
drawRect
方法中完成. 当然 Open GL ES 支持的视图不使用这个入口来绘制. -
创建 Image: 这也是一个普遍场景, 比如绘制背景什么的, 或是创建 PNG 或 JPEG 图像. 使用 UIKit 提供的 image context 即可开始绘制并最终获取到一副图像. 一般来说, 如果有些图像太大, 不能进行预置的话, 最好就在程序运行的时候绘制来使用.
-
创建 PDF: PDF 提供了在不同环境下的统一视觉样式.
-
使用 Core Graphics 底层功能: 虽然比 UIKit 提供的 API 更复杂, 但功能更强大也更灵活. 比如想要创建一张 image, 并按照每个 byte 的样式来调整, 这在 UIKit 上是很难的, 但 CG 中却可以很轻松操作.
1 关于图形上下文
iOS 中的绘图都是在 context 中进行的, 有不同的 context 可供绘制. 因为不同的 context 封装了不同输出设备的不同特性, 故编写绘制代码更为轻松, 因为不用去考虑不同的输出设备, 而只需要按照一套规则进行绘制.
2 关于绘制
绘制在大部分时候, 如果从上层来说, 都是绘制的 image. 另外也可以直接在 layer 上进行绘制.
3 关于 UIImageView
UIImageView 上有 isHighlighted 属性, 可以设置后显示它的 highlighted 对应设置的图片.
clipTobounds 属性可以避免当图像超过边界时仍然被显示的情况.
3.1 解释一个图像现象
当从代码创建一个 ImageView 时, 它的默认内容模式是 scaleToFill, 但实际上它显示的图片并没有被扩大, 而是它自己去适应图片的大小.
如果没有使用约束布局, 则图片框大小就是预设的大小, 但如果是使用约束的情况下, image 的大小就是图片框新的固有尺寸, 如果约束没有指定图片框大小的话(即只有位置的情况下), 不会报错, 且图片框大小自动变为图片大小.
且固有尺寸转换的约束的优先级会更低, 所以当有显式约束的情况下, 会先应用显式的约束.
3.2 关于 UIImage 的渲染模式
可以通过 withRenderingMode
生成新的图片, 带独立渲染模式的, 当设置为 alwaysTemplate
时, 在 UIImageView 中绘制的图片只会保留其 alpha 值而没有颜色值. 这时的颜色就是由 UIImageView 的 tintColor 提供了, 即底色被应用到 Template 渲染模式下的图片上. 而底色如果没有指定的话, 默认是从视图树从上往下继承获得的, 即最初的 tintColor 是在 window 上, 如果 window 没有设置底色, 则该颜色会继承自系统默认的蓝色. 如果对单个视图设置底色, 则它的子视图会继承这个颜色.
比如图片原始状态是这样的:
imageView.image = originalImg
image.png
当使用模板模式, 且设置了 tintColor 时, 就是如下的效果:
imageView.image = originalImg.withRenderingMode(.alwaysTemplate)
imageView.tintColor = UIColor.init(red: 0.5, green: 0.5, blue: 0.5, alpha: 1.0)
image.png
4 绘图上下文
如果不想直接嵌入图片到应用中, 还可以自己绘制. 这个时候就要用到所谓的 绘图上下文了(Graphics Context).
苹果提供上下文这个设施, 目的是帮助开发者以统一的方式使用绘图框架, 而不必去关心实际的输出设备. 因为实际输出设备的不同, 如果让开发者针对不同的输出设备进行不同的配置然后进行绘制, 显然不现实, 苹果将不同输出设备的信息通过上下文进行封装, 开发者只需要关注绘制, 而不用关心不同输出设备的不同.
有如下三种方式获取上下文:
-
自己创建一个 image 上下文: iOS 10 以前通过
UIGraphicsBeginImageContextWithOptions
获取, iOS 10 之后可以通过UIGraphicsImageRenderer
来获取. -
使用 Cocoa 创建的上下文: 在 UIView 上进行绘制时, 重写
draw(_:)
方法中获取. -
使用 Cocoa 提供的上下文: 当在 CALayer 上绘制时, 实现
draw(in:)
方法, 其中的 in 参数就是传入的上下文.
而关于 当前上下文 的概念:
-
自己创建 image 上下文时, 该上下文自动成为当前上下文.
-
重写 UIView 的
draw(_:)
方法时, 在其中的直接可以调用UIGraphicsGetCurrentContext
获取当前上下文, 即上下文已经存在了. -
实现 CALayer 的
draw(in:)
方法或其代理的draw(_:in:)
方法, 其中的 in 参数就是一个上下文, 但并非当前上下文, 你可以自己决定是否将其置为当前上下文.
在绘图时经常有疑惑的地方是: 在某些时候需要在当前上下文中绘制, 但有些时候只需要有一个上下文就可以绘制.
这个的答案就是:
-
当使用 UIKit 提供的绘图 API 时, 需要在当前上下文中绘制. 因为这些 API 没有传入上下文参数, 所以都会在当前上下文中绘制.
-
当使用 Core Graphics (Quartz) 底层绘图 API 时, 只需要有一个上下文即可, 因为绘制的 API 总是会传入 context 参数(OC), 而 swift 则是直接在 context 上调用绘制方法.
所以不要困惑, 这个并非什么规定, 只是 API 的使用方式不同罢了...
另外上面提到的 iOS 10 之后使用新的方法
UIGraphicsImageRenderer
创建 image 上下文的原因是 iPhone7 之后支持16位的更广的色域, 而之前的 UIGraphicsBeginImageContextWithOptions
获取到的上下文只支持 8 位的颜色. 而前者可以根据设备的不同自动处理颜色.
使用 UIGraphicsImageRenderer
的另外一个好处是它的 image
方法使用更方便, 可以在里面包装所有的绘制指令.
private func drawImage() {
// iOS 10 之后使用.
let imgRenderer = UIGraphicsImageRenderer.init(size: CGSize(width: 100, height: 100))
imgRenderer.image { (context) in
// 实际绘制代码.
}
}
另外 UIGraphicsImageRenderer
还可以输出 JPEG 或 PNG 格式的图片数据, 这样就可以保存到磁盘上了.
5 绘制图片
绘制图片的流程就是先获取一个 image 上下文, 然后在其中进行绘制, 最后获取绘制的结果图片, 保存到一个 image 对象中, 以后就可以在任意合适位置显示该 image 了.
另外 CG 框架中的 image 是 CGImage 类型, 而 UIImage 对象实际在内部封装了一个 CGImage 对象.
CGImage 只包含位图数据信息.
UIImage 内除了保护位图数据信息(CGImage对象)外, 还包含缩放, 旋转等其他信息.
通过 UIImage 的 cgImage 属性就可以获取 CGImage 对象.
UIImage 有一个初始化方法接收 CGImage 参数, 从而可以通过 CGImage 创建 UIImage.
CGImage 的特殊之处在于: 通过一张原始的 CGImage 可以再裁剪出另外一个 CGImage, 而 UIImage 不行, 另外还可以对 CGImage 应用一张 CGImage 作为蒙版使用.
这个先不展开讲.
5.1 截图
iOS 中可以将整个屏幕都绘制到当前上下文中, 调用 UIView 对象的
drawHierarchy(in:afterScreenUpdates:)
方法就可办到. 且该方法比 CALayer 的 render(in:)
方法速度快很多, 不过 CALayer 的 render 方法有其他的特殊用途.
这里还有一个更快的方法, 就是 UIView 对象或 UIScreen 对象的 snapshotView(afterScreenUpdates:)
方法, 这个方法的结果是一个 UIView 对象.
如果需要可伸缩的 image 截图, 则可以使用 resizableSnapshotView(from:afterScreenUpdates:withCapInsets:)
方法
6 UIView绘制
在 UIView 调用 draw()
方法时, 会在其中提供一个上下文, 只要在该上下文中绘制, 绘制的结果就会直接显示在视图中.
在 draw
方法中, 一般不用调用 super, 因为 UIView 类的 draw 中没有任何实现. 且该 view 的上下文已经被设置为当前上下文.
并且在 View 中绘制时, 不用担心性能问题, 因为绘图系统会自动缓存绘制并重用(bitmap backing store
技术), 并且只有当需要绘制的时候才会进行绘制.
draw 方法的时机为: 在自己的layoutSubview 之后, 在自己父视图的 layoutSubview 之后, 而本身的布局肯定是在父视图将自己布局之后, 所以也就是在父视图布局之后.
注意: 1) 不要手动去调用 draw 方法. 2) 注意某些视图的 draw 方法是不能被重写的, 比如 UIImageView. 3) 在 draw 方法中只写绘图代码, 不要带有其他的代码.
在上下文中绘制时, 绘制的参数是保存在图形状态中的(Graphics State), 而图形状态可用被保存和恢复, 而绘图系统中的套路也就是, 用一种设置绘制后, 可以再恢复到之前的设置.
故绘制不同设置的图形的整体方式就是:
-
调用
saveGState
, 然后修改到不同的上下文绘图设置. -
进行实际绘制.
-
调用
restoreGState
恢复上下文设置, 继续后面的绘制.
6.1 路径和形状
利用虚拟的笔, 可以在上下文中绘制 path. 但要明确一点: path 并非最终绘制的结果, 而是一个中间产物, 用于描述想要绘制的图像. 因为绘制的最终产物就是在 path 上描边或填充产生的.
另外 path 并非连续的, 且 path 可以由多个单独的 path 组成, 比如可以由两个完全不相邻的圆组成一个 path.
另外在 context 中构建的 path 可以通过 context 的path属性获取到.
在 UIKit 中的 UIBezierPath 实际是对 CG 框架底层 API 的一层封装, 可以利用 UIBezierPath 在当前上下文中进行绘制. 当每次调用贝塞尔路径的 fill
或 stroke
方法时, 当前上下文图形设置回被保存, 而内部的 CGPath 会被设置为当前上下文的 path, 然后被描边或填充, 随后当前上下文的设置会被恢复.
6.2 裁切 Clipping
最原始的状态下, 整个 context 上都是可以被绘制的. 但可以利用 clipping 来设定特殊的区域不被绘制.(实际上是设置一个闭合区域, 在该区域的某些部分可以绘制, 而其他部分不能绘制, 从而达到一些特定效果), 但只要设置了 clipping 区域后, 计算的区域外的所有点都无法被填充或绘制.
设置的 clipping 保存在图形状态中的, 意味着可以通过 saveGState 和 restoreGState 来控制 clipping 区域.
利用现有路径通过一定规则(只有当多个路径的闭合区域有交集时, 才会利用规则进行计算)形成一个 clipping path 后, 系统就会将之前的路径全部清理掉, 保留一个 clipping path 加入图形状态, 这个 clipping path 包裹的区域中才会绘制出内容.
特别是生成这个区域时, 计算方法有两种, evenOdd 和 winding. 奇偶规则和非零环绕规则, 两种规则计算的内部点各有不同, 而 clipping 区域即根据规则来最后形成的. 详见这篇文章. 总之就是 clipping 区域可以理解为填充区域(绘图区域), 即包含所有内部点的区域才进行填充或绘制.
比如两个矩形闭合路径:
-
使用奇偶规则的情况下: 如果有交集, 则二者的交集中的点不是内部点, 如果无交集, 则二者包围的区域都是内部点.
-
使用非零环绕情况下: 如果有交集, 会连同交集一起绘制, 如果没有, 则绘制内部点.
6.3 渐变
渐变指的是两端点间的颜色变化.
不能使用渐变来填充区域, 但可以通过 clipping 来达到同样的效果.
6.4 颜色和模式
颜色在 CG 框架中指的是 CGColor, 它可以和 UIColor 间相互转换.
模式(pattern) 同样是一种颜色类型. 首先创建一个颜色模式, 然后使用它来描边或填充.
6.4 上下文的坐标系变换
图形上下文也可以进行坐标变换, 且这个变换是记录在当前图形状态中的, 意味着可以随时保存和恢复.
主要就是调用上下文的 rotate
方法和 translateBy
方法.
6.5 阴影
如果要在当前上下文添加阴影, 只需要在绘制之前添加阴影值即可.
另外阴影的 blur 值设置为 12 比较好, 经验值. 它代表的是阴影边缘的软硬程度.
阴影的偏移 size 表示的是阴影效果相对于当前图形位置的偏移量, 两个正值表示阴影会出现在图形的右下方.
但有时需要阴影不要单纯地从图形中偏移出去, 因为有些组合图形需要的是符合实际组合情况的阴影, 这个时候就可以利用 transparency layer 来完成.
只需要把图形组合的过程和添加阴影的过程都包含在一个 transparency layer 语句块内:
private func drawThreeRectWithShadow() {
guard let ctx = UIGraphicsGetCurrentContext() else { return }
ctx.beginTransparencyLayer(auxiliaryInfo: nil)
let triangleRect = CGRect(x: 80, y: 100, width: 160, height: 100)
let shadowOffset = CGSize(width: -12, height: 24)
ctx.setShadow(offset: shadowOffset, blur: 12)
ctx.setFillColor(UIColor.red.withAlphaComponent(0.5).cgColor)
ctx.addRect(triangleRect)
ctx.fillPath()
ctx.saveGState()
ctx.setFillColor(UIColor.blue.withAlphaComponent(0.5).cgColor)
ctx.translateBy(x: 40, y: 200)
ctx.rotate(by: CGFloat.pi / -4.0)
ctx.addRect(CGRect(x: 0, y: 0, width: 80, height: 80))
ctx.fillPath()
ctx.restoreGState()
ctx.saveGState()
ctx.setFillColor(UIColor.green.withAlphaComponent(0.5).cgColor)
ctx.translateBy(x: 160, y: 200)
ctx.rotate(by: CGFloat.pi / -4.0)
ctx.addRect(CGRect(x: 0, y: 0, width: 80, height: 80))
ctx.fillPath()
ctx.restoreGState()
ctx.endTransparencyLayer()
}
6.6 擦除(Erasing)
利用擦除, 结合 clipping, 可以对复杂图形进行加工, 这个东西非常好用.
主要方法就是调用上下文的 clear(_:)
方法.
clear 方法的行为受上下文的是否透明控制. 如果透明, 则擦除后的区域是透明的, 如果不透明, 则擦除后的区域是黑洞. 而决定透明的因素就包括背景色. 即 View 的背景色如果是带透明的话, 则擦除后的颜色是 View 的背景色, 如果不带透明, 则擦除后的颜色是黑洞.
网友评论